Home Reference Source

cables_dev/cables/src/core/webaudio.js

  1. /** @namespace WEBAUDIO */
  2.  
  3. import { CONSTANTS } from "./constants.js";
  4.  
  5. const WEBAUDIO = {};
  6.  
  7. WEBAUDIO.toneJsInitialized = false;
  8.  
  9. /*
  10. * External JSDoc definitions
  11. */
  12.  
  13. /**
  14. * Part of the Web Audio API, the AudioBuffer interface represents a short audio asset residing in memory.
  15. * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer}
  16. */
  17.  
  18. /**
  19. * Part of the Web Audio API, the AudioNode interface is a generic interface for representing an audio processing module.
  20. * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioNode}
  21. */
  22.  
  23. /**
  24. * The AudioContext interface represents an audio-processing graph built from audio modules linked together
  25. * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioContext}
  26. */
  27.  
  28. /**
  29. * Checks if a global audio context has been created and creates
  30. * it if necessary. This audio context can be used for native Web Audio as well as Tone.js ops.
  31. * Associates the audio context with Tone.js if it is being used
  32. * @param {CABLES.Op} op - The operator which needs the Audio Context
  33. */
  34. WEBAUDIO.createAudioContext = function (op)
  35. {
  36. window.AudioContext = window.AudioContext || window.webkitAudioContext;
  37. if (window.AudioContext)
  38. {
  39. if (!window.audioContext)
  40. {
  41. window.audioContext = new AudioContext();
  42. }
  43. // check if tone.js lib is being used
  44. if (window.Tone && !WEBAUDIO.toneJsInitialized)
  45. {
  46. // set current audio context in tone.js
  47. Tone.setContext(window.audioContext);
  48. WEBAUDIO.toneJsInitialized = true;
  49. }
  50. }
  51. else
  52. {
  53. if (op.patch.config.onError)op.logError("NO_WEBAUDIO", "Web Audio is not supported in this browser.");
  54. return;
  55. }
  56. return window.audioContext;
  57. };
  58.  
  59. /**
  60. * Returns the audio context.
  61. * Before `createAudioContext` must have been called at least once.
  62. * It most cases you should use `createAudioContext`, which just returns the audio context
  63. * when it has been created already.
  64. */
  65. WEBAUDIO.getAudioContext = function ()
  66. {
  67. return window.audioContext;
  68. };
  69.  
  70. /**
  71. * Creates an audio in port for the op with name `portName`
  72. * When disconnected it will disconnect the previous connected audio node
  73. * from the op's audio node.
  74. * @param {CABLES.Op} op - The operator to create the audio port in
  75. * @param {string} portName - The name of the port
  76. * @param {AudioNode} audioNode - The audionode incoming connections should connect to
  77. * @param {number} [inputChannelIndex=0] - If the audio node has multiple inputs, this is the index of the input channel to connect to
  78. * @returns {CABLES.Port|undefined} - The newly created audio in port or `undefined` if there was an error
  79. */
  80. WEBAUDIO.createAudioInPort = function (op, portName, audioNode, inputChannelIndex)
  81. {
  82. if (!op || !portName || !audioNode)
  83. {
  84. const msg = "ERROR: createAudioInPort needs three parameters, op, portName and audioNode";
  85. op.log(msg);
  86. throw new Error(msg);
  87. // return;
  88. }
  89. if (!inputChannelIndex)
  90. {
  91. inputChannelIndex = 0;
  92. }
  93. op.webAudio = op.webAudio || {};
  94. op.webAudio.audioInPorts = op.webAudio.audioInPorts || [];
  95. const port = op.inObject(portName);
  96. port.webAudio = {};
  97. port.webAudio.previousAudioInNode = null;
  98. port.webAudio.audioNode = audioNode;
  99.  
  100. op.webAudio.audioInPorts[portName] = port;
  101.  
  102. port.onChange = function ()
  103. {
  104. const audioInNode = port.get();
  105. // when port disconnected, disconnect audio nodes
  106. if (!audioInNode)
  107. {
  108. if (port.webAudio.previousAudioInNode)
  109. {
  110. try
  111. {
  112. if (port.webAudio.previousAudioInNode.disconnect) port.webAudio.previousAudioInNode.disconnect(port.webAudio.audioNode, 0, inputChannelIndex);
  113. op.setUiError("audioCtx", null);
  114. }
  115. catch (e)
  116. {
  117. try
  118. {
  119. port.webAudio.previousAudioInNode.disconnect(port.webAudio.audioNode);
  120. }
  121. catch (er)
  122. {
  123. op.log(
  124. "Disconnecting audio node with in/out port index, as well as without in/out-port-index did not work ",
  125. e,
  126. );
  127. if (e.printStackTrace)
  128. {
  129. e.printStackTrace();
  130. }
  131. throw e;
  132. }
  133. }
  134. }
  135. }
  136. else
  137. {
  138. try
  139. {
  140. if (audioInNode.connect)
  141. {
  142. audioInNode.connect(port.webAudio.audioNode, 0, inputChannelIndex);
  143. op.setUiError("audioCtx", null);
  144. }
  145. else op.setUiError("audioCtx", "The passed input is not an audio context. Please make sure you connect an audio context to the input.", 2);
  146. }
  147. catch (e)
  148. {
  149. op.log("Error: Failed to connect web audio node!", e);
  150. op.log("port.webAudio.audioNode", port.webAudio.audioNode);
  151. op.log("audioInNode: ", audioInNode);
  152. op.log("inputChannelIndex:", inputChannelIndex);
  153. op.log("audioInNode.connect: ", audioInNode.connect);
  154. throw e;
  155. }
  156. }
  157. port.webAudio.previousAudioInNode = audioInNode;
  158. };
  159. // TODO: Maybe add subtype to audio-node-object?
  160. return port;
  161. };
  162.  
  163. /**
  164. * Sometimes it is necessary to replace a node of a port, if so all
  165. * connections to this node must be disconnected and connections to the new
  166. * node must be made.
  167. * Can be used for both Audio ports as well as AudioParam ports
  168. * if used with an AudioParam pass e.g. `synth.frequency` as newNode
  169. * @param {CABLES.Port} port - The port where the audio node needs to be replaced
  170. * @param oldNode
  171. * @param newNode
  172. */
  173. WEBAUDIO.replaceNodeInPort = function (port, oldNode, newNode)
  174. {
  175. const connectedNode = port.webAudio.previousAudioInNode;
  176. // check if connected
  177. if (connectedNode && connectedNode.disconnect)
  178. {
  179. try
  180. {
  181. connectedNode.disconnect(oldNode);
  182. }
  183. catch (e)
  184. {
  185. if (e.printStackTrace)
  186. {
  187. e.printStackTrace();
  188. }
  189. throw new Error("replaceNodeInPort: Could not disconnect old audio node. " + e.name + " " + e.message);
  190. }
  191. port.webAudio.audioNode = newNode;
  192. try
  193. {
  194. connectedNode.connect(newNode);
  195. }
  196. catch (e)
  197. {
  198. if (e.printStackTrace)
  199. {
  200. e.printStackTrace();
  201. }
  202. throw new Error("replaceNodeInPort: Could not connect to new node. " + e.name + " " + e.message);
  203. }
  204. }
  205. };
  206.  
  207. /**
  208. * Creates an audio out port which takes care of (dis-)connecting on it’s own
  209. * @param {CABLES.op} op - The op to create an audio out port for
  210. * @param {string} portName - The name of the port to be created
  211. * @param {AudioNode} audioNode - The audio node to link to the port
  212. * @returns {(CABLES.Port|undefined)} - The newly created audio out port or `undefined` if there was an error
  213. */
  214. WEBAUDIO.createAudioOutPort = function (op, portName, audioNode)
  215. {
  216. if (!op || !portName || !audioNode)
  217. {
  218. const msg = "ERROR: createAudioOutPort needs three parameters, op, portName and audioNode";
  219. op.log(msg);
  220. throw new Error(msg);
  221. }
  222.  
  223. const port = op.outObject(portName);
  224. // TODO: Maybe add subtype to audio-node-object?
  225. port.set(audioNode);
  226. return port;
  227. };
  228.  
  229. /**
  230. * Creates an audio param in port for the op with name portName.
  231. * The port accepts other audio nodes as signals as well as values (numbers)
  232. * When the port is disconnected it will disconnect the previous connected audio node
  233. * from the op's audio node and restore the number value set before.
  234. * @param {CABLES.Op} op - The operator to create an audio param input port for
  235. * @param {string} portName - The name of the port to create
  236. * @param audioNode
  237. * @param options
  238. * @param defaultValue
  239. * @returns {(CABLES.Port|undefined)} - The newly created port, which takes care of (dis-)connecting on its own, or `undefined` if there was an error
  240. */
  241. WEBAUDIO.createAudioParamInPort = function (op, portName, audioNode, options, defaultValue)
  242. {
  243. if (!op || !portName || !audioNode)
  244. {
  245. op.log("ERROR: createAudioParamInPort needs three parameters, op, portName and audioNode");
  246. if (op && op.name) op.log("opname: ", op.name);
  247. op.log("portName", portName);
  248. op.log("audioNode: ", audioNode);
  249. return;
  250. }
  251. op.webAudio = op.webAudio || {};
  252. op.webAudio.audioInPorts = op.webAudio.audioInPorts || [];
  253. // var port = op.inObject(portName);
  254. const port = op.inDynamic(
  255. portName,
  256. [CONSTANTS.OP.OP_PORT_TYPE_VALUE, CONSTANTS.OP.OP_PORT_TYPE_OBJECT],
  257. options,
  258. defaultValue,
  259. );
  260. port.webAudio = {};
  261. port.webAudio.previousAudioInNode = null;
  262. port.webAudio.audioNode = audioNode;
  263.  
  264. op.webAudio.audioInPorts[portName] = port;
  265.  
  266. // port.onLinkChanged = function() {
  267. // op.log("onLinkChanged");
  268. // if(port.isLinked()) {
  269. //
  270. // if(port.links[0].portOut.type === CABLES.CONSTANTS.OP.OP_PORT_TYPE_) { // value
  271. //
  272. // } else if(port.links[0].portOut.type === CABLES.CONSTANTS.OP.OP_PORT_TYPE_OBJECT) { // object
  273. //
  274. // }
  275. // } else { // unlinked
  276. //
  277. // }
  278. // };
  279.  
  280. port.onChange = function ()
  281. {
  282. const audioInNode = port.get();
  283. const node = port.webAudio.audioNode;
  284. const audioCtx = WEBAUDIO.getAudioContext();
  285.  
  286. if (audioInNode != undefined)
  287. {
  288. if (typeof audioInNode === "object" && audioInNode.connect)
  289. {
  290. try
  291. {
  292. audioInNode.connect(node);
  293. }
  294. catch (e)
  295. {
  296. op.log("Could not connect audio node: ", e);
  297. if (e.printStackTrace)
  298. {
  299. e.printStackTrace();
  300. }
  301. throw e;
  302. }
  303. port.webAudio.previousAudioInNode = audioInNode;
  304. }
  305. else
  306. {
  307. // tone.js audio param
  308. if (node._param && node._param.minValue && node._param.maxValue)
  309. {
  310. if (audioInNode >= node._param.minValue && audioInNode <= node._param.maxValue)
  311. {
  312. try
  313. {
  314. if (node.setValueAtTime)
  315. {
  316. node.setValueAtTime(audioInNode, audioCtx.currentTime);
  317. }
  318. else
  319. {
  320. node.value = audioInNode;
  321. }
  322. }
  323. catch (e)
  324. {
  325. op.log("Possible AudioParam problem with tone.js op: ", e);
  326. if (e.printStackTrace)
  327. {
  328. e.printStackTrace();
  329. }
  330. throw e;
  331. }
  332. }
  333. else
  334. {
  335. op.log("Warning: The value for an audio parameter is out of range!");
  336. }
  337. } // native Web Audio param
  338. else if (node.minValue && node.maxValue)
  339. {
  340. if (audioInNode >= node.minValue && audioInNode <= node.maxValue)
  341. {
  342. try
  343. {
  344. if (node.setValueAtTime)
  345. {
  346. node.setValueAtTime(audioInNode, audioCtx.currentTime);
  347. }
  348. else
  349. {
  350. node.value = audioInNode;
  351. }
  352. }
  353. catch (e)
  354. {
  355. op.log(
  356. "AudioParam has minValue / maxValue defined, and value is in range, but setting the value failed! ",
  357. e,
  358. );
  359. if (e.printStackTrace)
  360. {
  361. e.printStackTrace();
  362. }
  363. throw e;
  364. }
  365. }
  366. else
  367. {
  368. op.log("Warning: The value for an audio parameter is out of range!");
  369. }
  370. } // no min-max values, try anyway
  371. else
  372. {
  373. try
  374. {
  375. if (node.setValueAtTime)
  376. {
  377. node.setValueAtTime(audioInNode, audioCtx.currentTime);
  378. }
  379. else
  380. {
  381. node.value = audioInNode;
  382. }
  383. }
  384. catch (e)
  385. {
  386. op.log("Possible AudioParam problem (without minValue / maxValue): ", e);
  387. if (e.printStackTrace)
  388. {
  389. e.printStackTrace();
  390. }
  391. throw e;
  392. }
  393. }
  394.  
  395. if (port.webAudio.previousAudioInNode && port.webAudio.previousAudioInNode.disconnect)
  396. {
  397. try
  398. {
  399. port.webAudio.previousAudioInNode.disconnect(node);
  400. }
  401. catch (e)
  402. {
  403. op.log("Could not disconnect previous audio node: ", e);
  404. throw e;
  405. }
  406. port.webAudio.previousAudioInNode = undefined;
  407. }
  408. }
  409. }
  410. else
  411. {
  412. // disconnected
  413. if (port.webAudio.previousAudioInNode)
  414. {
  415. }
  416. }
  417. };
  418. return port;
  419. };
  420.  
  421.  
  422. /**
  423. * Loads an audio file and updates the loading indicators when cables is run in the editor.
  424. * @param {CABLES.Patch} patch - The cables patch, when called from inside an op this is `op.patch`
  425. * @param {string} url - The url of the audio file to load
  426. * @param {function} onFinished - The callback to be called when the loading is finished, passes the AudioBuffer
  427. * @param {function} onError - The callback when there was an error loading the file, the rror message is passed
  428. * @param loadingTask
  429. * @see {@link https://developer.mozilla.org/de/docs/Web/API/AudioContext/decodeAudioData}
  430. */
  431. WEBAUDIO.loadAudioFile = function (patch, url, onFinished, onError, loadingTask)
  432. {
  433. const audioContext = WEBAUDIO.createAudioContext();
  434.  
  435. if (!audioContext) onError(new Error("No Audiocontext"));
  436.  
  437. let loadingId = null;
  438. if (loadingTask || loadingTask === undefined)
  439. {
  440. loadingId = patch.loading.start("audio", url);
  441. if (patch.isEditorMode()) gui.jobs().start({ "id": "loadaudio" + loadingId, "title": " loading audio (" + url + ")" });
  442. }
  443. const request = new XMLHttpRequest();
  444.  
  445. if (!url) return;
  446.  
  447. request.open("GET", url, true);
  448. request.responseType = "arraybuffer";
  449.  
  450. request.onload = function ()
  451. {
  452. patch.loading.finished(loadingId);
  453. if (patch.isEditorMode()) gui.jobs().finish("loadaudio" + loadingId);
  454.  
  455. audioContext.decodeAudioData(request.response, onFinished, onError).catch((e) =>
  456. {
  457. onError(e);
  458. });
  459. };
  460. request.send();
  461. };
  462.  
  463. /**
  464. * Checks if the passed time is a valid time to be used in any of the Tone.js ops.
  465. * @param {(string|number)} t - The time to check
  466. * @returns {boolean} - True if time is valid, false if not
  467. */
  468. WEBAUDIO.isValidToneTime = function (t)
  469. {
  470. try
  471. {
  472. const time = new Tone.Time(t);
  473. }
  474. catch (e)
  475. {
  476. return false;
  477. }
  478. return true;
  479. };
  480.  
  481. /**
  482. * Checks if the passed note is a valid note to be used with Tone.js
  483. * @param {string} note - The note to be checked, e.g. `"C4"`
  484. * @returns {boolean} - True if the note is a valid note, false otherwise
  485. */
  486. WEBAUDIO.isValidToneNote = function (note)
  487. {
  488. try
  489. {
  490. Tone.Frequency(note);
  491. }
  492. catch (e)
  493. {
  494. return false;
  495. }
  496. return true;
  497. };
  498.  
  499. export { WEBAUDIO };