cables_dev/cables/src/core/webaudio.js
- /** @namespace WEBAUDIO */
-
- import { CONSTANTS } from "./constants.js";
-
- const WEBAUDIO = {};
-
- WEBAUDIO.toneJsInitialized = false;
-
- /*
- * External JSDoc definitions
- */
-
- /**
- * Part of the Web Audio API, the AudioBuffer interface represents a short audio asset residing in memory.
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer}
- */
-
- /**
- * Part of the Web Audio API, the AudioNode interface is a generic interface for representing an audio processing module.
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioNode}
- */
-
- /**
- * The AudioContext interface represents an audio-processing graph built from audio modules linked together
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioContext}
- */
-
- /**
- * Checks if a global audio context has been created and creates
- * it if necessary. This audio context can be used for native Web Audio as well as Tone.js ops.
- * Associates the audio context with Tone.js if it is being used
- * @param {CABLES.Op} op - The operator which needs the Audio Context
- */
- WEBAUDIO.createAudioContext = function (op)
- {
- window.AudioContext = window.AudioContext || window.webkitAudioContext;
- if (window.AudioContext)
- {
- if (!window.audioContext)
- {
- window.audioContext = new AudioContext();
- }
- // check if tone.js lib is being used
- if (window.Tone && !WEBAUDIO.toneJsInitialized)
- {
- // set current audio context in tone.js
- Tone.setContext(window.audioContext);
- WEBAUDIO.toneJsInitialized = true;
- }
- }
- else
- {
- if (op.patch.config.onError)op.logError("NO_WEBAUDIO", "Web Audio is not supported in this browser.");
- return;
- }
- return window.audioContext;
- };
-
- /**
- * Returns the audio context.
- * Before `createAudioContext` must have been called at least once.
- * It most cases you should use `createAudioContext`, which just returns the audio context
- * when it has been created already.
- */
- WEBAUDIO.getAudioContext = function ()
- {
- return window.audioContext;
- };
-
- /**
- * Creates an audio in port for the op with name `portName`
- * When disconnected it will disconnect the previous connected audio node
- * from the op's audio node.
- * @param {CABLES.Op} op - The operator to create the audio port in
- * @param {string} portName - The name of the port
- * @param {AudioNode} audioNode - The audionode incoming connections should connect to
- * @param {number} [inputChannelIndex=0] - If the audio node has multiple inputs, this is the index of the input channel to connect to
- * @returns {CABLES.Port|undefined} - The newly created audio in port or `undefined` if there was an error
- */
- WEBAUDIO.createAudioInPort = function (op, portName, audioNode, inputChannelIndex)
- {
- if (!op || !portName || !audioNode)
- {
- const msg = "ERROR: createAudioInPort needs three parameters, op, portName and audioNode";
- op.log(msg);
- throw new Error(msg);
- // return;
- }
- if (!inputChannelIndex)
- {
- inputChannelIndex = 0;
- }
- op.webAudio = op.webAudio || {};
- op.webAudio.audioInPorts = op.webAudio.audioInPorts || [];
- const port = op.inObject(portName);
- port.webAudio = {};
- port.webAudio.previousAudioInNode = null;
- port.webAudio.audioNode = audioNode;
-
- op.webAudio.audioInPorts[portName] = port;
-
- port.onChange = function ()
- {
- const audioInNode = port.get();
- // when port disconnected, disconnect audio nodes
- if (!audioInNode)
- {
- if (port.webAudio.previousAudioInNode)
- {
- try
- {
- if (port.webAudio.previousAudioInNode.disconnect) port.webAudio.previousAudioInNode.disconnect(port.webAudio.audioNode, 0, inputChannelIndex);
- op.setUiError("audioCtx", null);
- }
- catch (e)
- {
- try
- {
- port.webAudio.previousAudioInNode.disconnect(port.webAudio.audioNode);
- }
- catch (er)
- {
- op.log(
- "Disconnecting audio node with in/out port index, as well as without in/out-port-index did not work ",
- e,
- );
- if (e.printStackTrace)
- {
- e.printStackTrace();
- }
- throw e;
- }
- }
- }
- }
- else
- {
- try
- {
- if (audioInNode.connect)
- {
- audioInNode.connect(port.webAudio.audioNode, 0, inputChannelIndex);
- op.setUiError("audioCtx", null);
- }
- else op.setUiError("audioCtx", "The passed input is not an audio context. Please make sure you connect an audio context to the input.", 2);
- }
- catch (e)
- {
- op.log("Error: Failed to connect web audio node!", e);
- op.log("port.webAudio.audioNode", port.webAudio.audioNode);
- op.log("audioInNode: ", audioInNode);
- op.log("inputChannelIndex:", inputChannelIndex);
- op.log("audioInNode.connect: ", audioInNode.connect);
- throw e;
- }
- }
- port.webAudio.previousAudioInNode = audioInNode;
- };
- // TODO: Maybe add subtype to audio-node-object?
- return port;
- };
-
- /**
- * Sometimes it is necessary to replace a node of a port, if so all
- * connections to this node must be disconnected and connections to the new
- * node must be made.
- * Can be used for both Audio ports as well as AudioParam ports
- * if used with an AudioParam pass e.g. `synth.frequency` as newNode
- * @param {CABLES.Port} port - The port where the audio node needs to be replaced
- * @param oldNode
- * @param newNode
- */
- WEBAUDIO.replaceNodeInPort = function (port, oldNode, newNode)
- {
- const connectedNode = port.webAudio.previousAudioInNode;
- // check if connected
- if (connectedNode && connectedNode.disconnect)
- {
- try
- {
- connectedNode.disconnect(oldNode);
- }
- catch (e)
- {
- if (e.printStackTrace)
- {
- e.printStackTrace();
- }
- throw new Error("replaceNodeInPort: Could not disconnect old audio node. " + e.name + " " + e.message);
- }
- port.webAudio.audioNode = newNode;
- try
- {
- connectedNode.connect(newNode);
- }
- catch (e)
- {
- if (e.printStackTrace)
- {
- e.printStackTrace();
- }
- throw new Error("replaceNodeInPort: Could not connect to new node. " + e.name + " " + e.message);
- }
- }
- };
-
- /**
- * Creates an audio out port which takes care of (dis-)connecting on it’s own
- * @param {CABLES.op} op - The op to create an audio out port for
- * @param {string} portName - The name of the port to be created
- * @param {AudioNode} audioNode - The audio node to link to the port
- * @returns {(CABLES.Port|undefined)} - The newly created audio out port or `undefined` if there was an error
- */
- WEBAUDIO.createAudioOutPort = function (op, portName, audioNode)
- {
- if (!op || !portName || !audioNode)
- {
- const msg = "ERROR: createAudioOutPort needs three parameters, op, portName and audioNode";
- op.log(msg);
- throw new Error(msg);
- }
-
- const port = op.outObject(portName);
- // TODO: Maybe add subtype to audio-node-object?
- port.set(audioNode);
- return port;
- };
-
- /**
- * Creates an audio param in port for the op with name portName.
- * The port accepts other audio nodes as signals as well as values (numbers)
- * When the port is disconnected it will disconnect the previous connected audio node
- * from the op's audio node and restore the number value set before.
- * @param {CABLES.Op} op - The operator to create an audio param input port for
- * @param {string} portName - The name of the port to create
- * @param audioNode
- * @param options
- * @param defaultValue
- * @returns {(CABLES.Port|undefined)} - The newly created port, which takes care of (dis-)connecting on its own, or `undefined` if there was an error
- */
- WEBAUDIO.createAudioParamInPort = function (op, portName, audioNode, options, defaultValue)
- {
- if (!op || !portName || !audioNode)
- {
- op.log("ERROR: createAudioParamInPort needs three parameters, op, portName and audioNode");
- if (op && op.name) op.log("opname: ", op.name);
- op.log("portName", portName);
- op.log("audioNode: ", audioNode);
- return;
- }
- op.webAudio = op.webAudio || {};
- op.webAudio.audioInPorts = op.webAudio.audioInPorts || [];
- // var port = op.inObject(portName);
- const port = op.inDynamic(
- portName,
- [CONSTANTS.OP.OP_PORT_TYPE_VALUE, CONSTANTS.OP.OP_PORT_TYPE_OBJECT],
- options,
- defaultValue,
- );
- port.webAudio = {};
- port.webAudio.previousAudioInNode = null;
- port.webAudio.audioNode = audioNode;
-
- op.webAudio.audioInPorts[portName] = port;
-
- // port.onLinkChanged = function() {
- // op.log("onLinkChanged");
- // if(port.isLinked()) {
- //
- // if(port.links[0].portOut.type === CABLES.CONSTANTS.OP.OP_PORT_TYPE_) { // value
- //
- // } else if(port.links[0].portOut.type === CABLES.CONSTANTS.OP.OP_PORT_TYPE_OBJECT) { // object
- //
- // }
- // } else { // unlinked
- //
- // }
- // };
-
- port.onChange = function ()
- {
- const audioInNode = port.get();
- const node = port.webAudio.audioNode;
- const audioCtx = WEBAUDIO.getAudioContext();
-
- if (audioInNode != undefined)
- {
- if (typeof audioInNode === "object" && audioInNode.connect)
- {
- try
- {
- audioInNode.connect(node);
- }
- catch (e)
- {
- op.log("Could not connect audio node: ", e);
- if (e.printStackTrace)
- {
- e.printStackTrace();
- }
- throw e;
- }
- port.webAudio.previousAudioInNode = audioInNode;
- }
- else
- {
- // tone.js audio param
- if (node._param && node._param.minValue && node._param.maxValue)
- {
- if (audioInNode >= node._param.minValue && audioInNode <= node._param.maxValue)
- {
- try
- {
- if (node.setValueAtTime)
- {
- node.setValueAtTime(audioInNode, audioCtx.currentTime);
- }
- else
- {
- node.value = audioInNode;
- }
- }
- catch (e)
- {
- op.log("Possible AudioParam problem with tone.js op: ", e);
- if (e.printStackTrace)
- {
- e.printStackTrace();
- }
- throw e;
- }
- }
- else
- {
- op.log("Warning: The value for an audio parameter is out of range!");
- }
- } // native Web Audio param
- else if (node.minValue && node.maxValue)
- {
- if (audioInNode >= node.minValue && audioInNode <= node.maxValue)
- {
- try
- {
- if (node.setValueAtTime)
- {
- node.setValueAtTime(audioInNode, audioCtx.currentTime);
- }
- else
- {
- node.value = audioInNode;
- }
- }
- catch (e)
- {
- op.log(
- "AudioParam has minValue / maxValue defined, and value is in range, but setting the value failed! ",
- e,
- );
- if (e.printStackTrace)
- {
- e.printStackTrace();
- }
- throw e;
- }
- }
- else
- {
- op.log("Warning: The value for an audio parameter is out of range!");
- }
- } // no min-max values, try anyway
- else
- {
- try
- {
- if (node.setValueAtTime)
- {
- node.setValueAtTime(audioInNode, audioCtx.currentTime);
- }
- else
- {
- node.value = audioInNode;
- }
- }
- catch (e)
- {
- op.log("Possible AudioParam problem (without minValue / maxValue): ", e);
- if (e.printStackTrace)
- {
- e.printStackTrace();
- }
- throw e;
- }
- }
-
- if (port.webAudio.previousAudioInNode && port.webAudio.previousAudioInNode.disconnect)
- {
- try
- {
- port.webAudio.previousAudioInNode.disconnect(node);
- }
- catch (e)
- {
- op.log("Could not disconnect previous audio node: ", e);
- throw e;
- }
- port.webAudio.previousAudioInNode = undefined;
- }
- }
- }
- else
- {
- // disconnected
- if (port.webAudio.previousAudioInNode)
- {
- }
- }
- };
- return port;
- };
-
-
- /**
- * Loads an audio file and updates the loading indicators when cables is run in the editor.
- * @param {CABLES.Patch} patch - The cables patch, when called from inside an op this is `op.patch`
- * @param {string} url - The url of the audio file to load
- * @param {function} onFinished - The callback to be called when the loading is finished, passes the AudioBuffer
- * @param {function} onError - The callback when there was an error loading the file, the rror message is passed
- * @param loadingTask
- * @see {@link https://developer.mozilla.org/de/docs/Web/API/AudioContext/decodeAudioData}
- */
- WEBAUDIO.loadAudioFile = function (patch, url, onFinished, onError, loadingTask)
- {
- const audioContext = WEBAUDIO.createAudioContext();
-
- if (!audioContext) onError(new Error("No Audiocontext"));
-
- let loadingId = null;
- if (loadingTask || loadingTask === undefined)
- {
- loadingId = patch.loading.start("audio", url);
- if (patch.isEditorMode()) gui.jobs().start({ "id": "loadaudio" + loadingId, "title": " loading audio (" + url + ")" });
- }
- const request = new XMLHttpRequest();
-
- if (!url) return;
-
- request.open("GET", url, true);
- request.responseType = "arraybuffer";
-
- request.onload = function ()
- {
- patch.loading.finished(loadingId);
- if (patch.isEditorMode()) gui.jobs().finish("loadaudio" + loadingId);
-
- audioContext.decodeAudioData(request.response, onFinished, onError).catch((e) =>
- {
- onError(e);
- });
- };
- request.send();
- };
-
- /**
- * Checks if the passed time is a valid time to be used in any of the Tone.js ops.
- * @param {(string|number)} t - The time to check
- * @returns {boolean} - True if time is valid, false if not
- */
- WEBAUDIO.isValidToneTime = function (t)
- {
- try
- {
- const time = new Tone.Time(t);
- }
- catch (e)
- {
- return false;
- }
- return true;
- };
-
- /**
- * Checks if the passed note is a valid note to be used with Tone.js
- * @param {string} note - The note to be checked, e.g. `"C4"`
- * @returns {boolean} - True if the note is a valid note, false otherwise
- */
- WEBAUDIO.isValidToneNote = function (note)
- {
- try
- {
- Tone.Frequency(note);
- }
- catch (e)
- {
- return false;
- }
- return true;
- };
-
- export { WEBAUDIO };