not really known
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

746 lines
23 KiB

  1. /**
  2. * Tutorials:
  3. * http://www.html5rocks.com/en/tutorials/webaudio/games/
  4. * http://www.html5rocks.com/en/tutorials/webaudio/positional_audio/ <- +1 as it is three.js
  5. * http://www.html5rocks.com/en/tutorials/webaudio/intro/
  6. *
  7. * Spec:
  8. * https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html
  9. *
  10. * Chromium Demo:
  11. * http://chromium.googlecode.com/svn/trunk/samples/audio/index.html <- running page
  12. * http://code.google.com/p/chromium/source/browse/trunk/samples/audio/ <- source
  13. */
  14. /**
  15. * Notes on removing tQuery dependancy
  16. * * some stuff depends on tQuery
  17. * * find which one
  18. * * tQuery.Webaudio got a world link for the listener
  19. * * do a plugin with followListener(world), unfollowListener(world)
  20. * * namespace become WebAudio.* instead of WebAudio.*
  21. */
  22. //////////////////////////////////////////////////////////////////////////////////
  23. //////////////////////////////////////////////////////////////////////////////////
  24. //////////////////////////////////////////////////////////////////////////////////
  25. // WebAudio //
  26. //////////////////////////////////////////////////////////////////////////////////
  27. //////////////////////////////////////////////////////////////////////////////////
  28. //////////////////////////////////////////////////////////////////////////////////
  29. window.AudioContext = window.AudioContext || window.webkitAudioContext;
  30. /**
  31. * Main class to handle webkit audio
  32. *
  33. * TODO make the clip detector from http://www.html5rocks.com/en/tutorials/webaudio/games/
  34. *
  35. * @class Handle webkit audio API
  36. *
  37. * @param {tQuery.World} [world] the world on which to run
  38. */
  39. WebAudio = function () {
  40. // sanity check - the api MUST be available
  41. if (WebAudio.isAvailable === false) {
  42. this._addRequiredMessage();
  43. // Throw an error to stop execution
  44. throw new Error('WebAudio API is required and not available.')
  45. }
  46. // create the context
  47. this._ctx = new AudioContext();
  48. // setup internal variable
  49. this._muted = false;
  50. this._volume = 1;
  51. // setup the end of the node chain
  52. // TODO later code the clipping detection from http://www.html5rocks.com/en/tutorials/webaudio/games/
  53. this._gainNode = this._ctx.createGain();
  54. this._compressor = this._ctx.createDynamicsCompressor();
  55. this._gainNode.connect(this._compressor);
  56. this._compressor.connect(this._ctx.destination);
  57. // init page visibility
  58. this._pageVisibilityCtor();
  59. };
  60. /**
  61. * vendor.js way to make plugins ala jQuery
  62. * @namespace
  63. */
  64. WebAudio.fn = WebAudio.prototype;
  65. /**
  66. * destructor
  67. */
  68. WebAudio.prototype.destroy = function () {
  69. this._pageVisibilityDtor();
  70. };
  71. /**
  72. * @return {Boolean} true if it is available or not
  73. */
  74. WebAudio.isAvailable = window.AudioContext ? true : false;
  75. //////////////////////////////////////////////////////////////////////////////////
  76. // comment //
  77. //////////////////////////////////////////////////////////////////////////////////
  78. WebAudio.prototype._addRequiredMessage = function (parent) {
  79. // handle defaults arguements
  80. parent = parent || document.body;
  81. // message directly taken from Detector.js
  82. var domElement = document.createElement('div');
  83. domElement.style.fontFamily = 'monospace';
  84. domElement.style.fontSize = '13px';
  85. domElement.style.textAlign = 'center';
  86. domElement.style.background = '#eee';
  87. domElement.style.color = '#000';
  88. domElement.style.padding = '1em';
  89. domElement.style.width = '475px';
  90. domElement.style.margin = '5em auto 0';
  91. domElement.innerHTML = [
  92. 'Your browser does not seem to support <a href="https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html">WebAudio API</a>.<br />',
  93. 'Try with <a href="https://www.google.com/intl/en/chrome/browser/">Chrome Browser</a>.'
  94. ].join('\n');
  95. // add it to the parent
  96. parent.appendChild(domElement);
  97. }
  98. //////////////////////////////////////////////////////////////////////////////////
  99. // //
  100. //////////////////////////////////////////////////////////////////////////////////
  101. /**
  102. * get the audio context
  103. *
  104. * @returns {AudioContext} the audio context
  105. */
  106. WebAudio.prototype.context = function () {
  107. return this._ctx;
  108. };
  109. /**
  110. * Create a sound
  111. *
  112. * @returns {WebAudio.Sound} the sound just created
  113. */
  114. WebAudio.prototype.createSound = function () {
  115. var webaudio = this;
  116. var sound = new WebAudio.Sound(webaudio);
  117. return sound;
  118. }
  119. /**
  120. * return the entry node in the master node chains
  121. */
  122. WebAudio.prototype._entryNode = function () {
  123. //return this._ctx.destination;
  124. return this._gainNode;
  125. }
  126. //////////////////////////////////////////////////////////////////////////////////
  127. // volume/mute //
  128. //////////////////////////////////////////////////////////////////////////////////
  129. /**
  130. * getter/setter on the volume
  131. */
  132. WebAudio.prototype.volume = function (value) {
  133. if (value === undefined) return this._volume;
  134. // update volume
  135. this._volume = value;
  136. // update actual volume IIF not muted
  137. if (this._muted === false) {
  138. this._gainNode.gain.value = this._volume;
  139. }
  140. // return this for chained API
  141. return this;
  142. };
  143. /**
  144. * getter/setter for mute
  145. */
  146. WebAudio.prototype.mute = function (value) {
  147. if (value === undefined) return this._muted;
  148. this._muted = value;
  149. this._gainNode.gain.value = this._muted ? 0 : this._volume;
  150. return this; // for chained API
  151. }
  152. /**
  153. * to toggle the mute
  154. */
  155. WebAudio.prototype.toggleMute = function () {
  156. if (this.mute()) this.mute(false);
  157. else this.mute(true);
  158. }
  159. //////////////////////////////////////////////////////////////////////////////////
  160. // pageVisibility //
  161. //////////////////////////////////////////////////////////////////////////////////
  162. WebAudio.prototype._pageVisibilityCtor = function () {
  163. // shim to handle browser vendor
  164. this._pageVisibilityEventStr = (document.hidden !== undefined ? 'visibilitychange' :
  165. (document.mozHidden !== undefined ? 'mozvisibilitychange' :
  166. (document.msHidden !== undefined ? 'msvisibilitychange' :
  167. (document.webkitHidden !== undefined ? 'webkitvisibilitychange' :
  168. console.assert(false, "Page Visibility API unsupported")
  169. ))));
  170. this._pageVisibilityDocumentStr = (document.hidden !== undefined ? 'hidden' :
  171. (document.mozHidden !== undefined ? 'mozHidden' :
  172. (document.msHidden !== undefined ? 'msHidden' :
  173. (document.webkitHidden !== undefined ? 'webkitHidden' :
  174. console.assert(false, "Page Visibility API unsupported")
  175. ))));
  176. // event handler for visibilitychange event
  177. this._$pageVisibilityCallback = function () {
  178. var isHidden = document[this._pageVisibilityDocumentStr] ? true : false;
  179. this.mute(isHidden ? true : false);
  180. }.bind(this);
  181. // bind the event itself
  182. document.addEventListener(this._pageVisibilityEventStr, this._$pageVisibilityCallback, false);
  183. }
  184. WebAudio.prototype._pageVisibilityDtor = function () {
  185. // unbind the event itself
  186. document.removeEventListener(this._pageVisibilityEventStr, this._$pageVisibilityCallback, false);
  187. }
  188. /**
  189. * Constructor
  190. *
  191. * @class builder to generate nodes chains. Used in WebAudio.Sound
  192. * @param {AudioContext} audioContext the audio context
  193. */
  194. WebAudio.NodeChainBuilder = function (audioContext) {
  195. console.assert(audioContext instanceof AudioContext);
  196. this._context = audioContext;
  197. this._firstNode = null;
  198. this._lastNode = null;
  199. this._nodes = {};
  200. };
  201. /**
  202. * creator
  203. *
  204. * @param {webkitAudioContext} audioContext the context
  205. * @return {WebAudio.NodeChainBuider} just created object
  206. */
  207. WebAudio.NodeChainBuilder.create = function (audioContext) {
  208. return new WebAudio.NodeChainBuilder(audioContext);
  209. }
  210. /**
  211. * destructor
  212. */
  213. WebAudio.NodeChainBuilder.prototype.destroy = function () {
  214. };
  215. //////////////////////////////////////////////////////////////////////////////////
  216. // getters //
  217. //////////////////////////////////////////////////////////////////////////////////
  218. /**
  219. * getter for the nodes
  220. */
  221. WebAudio.NodeChainBuilder.prototype.nodes = function () {
  222. return this._nodes;
  223. }
  224. /**
  225. * @returns the first node of the chain
  226. */
  227. WebAudio.NodeChainBuilder.prototype.first = function () {
  228. return this._firstNode;
  229. }
  230. /**
  231. * @returns the last node of the chain
  232. */
  233. WebAudio.NodeChainBuilder.prototype.last = function () {
  234. return this._lastNode;
  235. }
  236. //////////////////////////////////////////////////////////////////////////////////
  237. // //
  238. //////////////////////////////////////////////////////////////////////////////////
  239. /**
  240. * add a node to the chain
  241. * @param {[type]} node [description]
  242. * @param {[type]} properties [description]
  243. */
  244. WebAudio.NodeChainBuilder.prototype._addNode = function (node, properties) {
  245. // update this._bufferSourceDst - needed for .cloneBufferSource()
  246. var lastIsBufferSource = this._lastNode && ('playbackRate' in this._lastNode) ? true : false;
  247. if (lastIsBufferSource) this._bufferSourceDst = node;
  248. // connect this._lastNode to node if suitable
  249. if (this._lastNode !== null) this._lastNode.connect(node);
  250. // update this._firstNode && this._lastNode
  251. if (this._firstNode === null) this._firstNode = node;
  252. this._lastNode = node;
  253. // apply properties to the node
  254. for (var property in properties) {
  255. node[property] = properties[property];
  256. }
  257. // for chained API
  258. return this;
  259. };
  260. //////////////////////////////////////////////////////////////////////////////////
  261. // creator for each type of nodes //
  262. //////////////////////////////////////////////////////////////////////////////////
  263. /**
  264. * Clone the bufferSource. Used just before playing a sound
  265. * @returns {AudioBufferSourceNode} the clone AudioBufferSourceNode
  266. */
  267. WebAudio.NodeChainBuilder.prototype.cloneBufferSource = function () {
  268. console.assert(this._nodes.bufferSource, "no buffersource presents. Add one.");
  269. var orig = this._nodes.bufferSource;
  270. var clone = this._context.createBufferSource()
  271. clone.buffer = orig.buffer;
  272. clone.playbackRate = orig.playbackRate;
  273. clone.loop = orig.loop;
  274. clone.connect(this._bufferSourceDst);
  275. return clone;
  276. }
  277. /**
  278. * add a bufferSource
  279. *
  280. * @param {Object} [properties] properties to set in the created node
  281. */
  282. WebAudio.NodeChainBuilder.prototype.bufferSource = function (properties) {
  283. var node = this._context.createBufferSource()
  284. this._nodes.bufferSource = node;
  285. return this._addNode(node, properties)
  286. };
  287. /**
  288. * add a createMediaStreamSource
  289. *
  290. * @param {Object} [properties] properties to set in the created node
  291. */
  292. WebAudio.NodeChainBuilder.prototype.mediaStreamSource = function (stream, properties) {
  293. // console.assert( stream instanceof LocalMediaStream )
  294. var node = this._context.createMediaStreamSource(stream)
  295. this._nodes.bufferSource = node;
  296. return this._addNode(node, properties)
  297. };
  298. /**
  299. * add a createMediaElementSource
  300. * @param {HTMLElement} element the element to add
  301. * @param {Object} [properties] properties to set in the created node
  302. */
  303. WebAudio.NodeChainBuilder.prototype.mediaElementSource = function (element, properties) {
  304. console.assert(element instanceof HTMLAudioElement || element instanceof HTMLVideoElement)
  305. var node = this._context.createMediaElementSource(element)
  306. this._nodes.bufferSource = node;
  307. return this._addNode(node, properties)
  308. };
  309. /**
  310. * add a panner
  311. *
  312. * @param {Object} [properties] properties to set in the created node
  313. */
  314. WebAudio.NodeChainBuilder.prototype.panner = function (properties) {
  315. var node = this._context.createPanner()
  316. this._nodes.panner = node;
  317. return this._addNode(node, properties)
  318. };
  319. /**
  320. * add a analyser
  321. *
  322. * @param {Object} [properties] properties to set in the created node
  323. */
  324. WebAudio.NodeChainBuilder.prototype.analyser = function (properties) {
  325. var node = this._context.createAnalyser()
  326. this._nodes.analyser = node;
  327. return this._addNode(node, properties)
  328. };
  329. /**
  330. * add a gainNode
  331. *
  332. * @param {Object} [properties] properties to set in the created node
  333. */
  334. WebAudio.NodeChainBuilder.prototype.gainNode = function (properties) {
  335. var node = this._context.createGain()
  336. this._nodes.gainNode = node;
  337. return this._addNode(node, properties)
  338. };
  339. /**
  340. * sound instance
  341. *
  342. * @class Handle one sound for WebAudio
  343. *
  344. * @param {tQuery.World} [world] the world on which to run
  345. * @param {WebAudio.NodeChainBuilder} [nodeChain] the nodeChain to use
  346. */
  347. WebAudio.Sound = function (webaudio, nodeChain) {
  348. this._webaudio = webaudio;
  349. this._context = this._webaudio.context();
  350. console.assert(this._webaudio instanceof WebAudio);
  351. // create a default NodeChainBuilder if needed
  352. if (nodeChain === undefined) {
  353. nodeChain = new WebAudio.NodeChainBuilder(this._context)
  354. .bufferSource().gainNode().analyser().panner();
  355. }
  356. // setup this._chain
  357. console.assert(nodeChain instanceof WebAudio.NodeChainBuilder);
  358. this._chain = nodeChain;
  359. // connect this._chain.last() node to this._webaudio._entryNode()
  360. this._chain.last().connect(this._webaudio._entryNode());
  361. // create some alias
  362. this._source = this._chain.nodes().bufferSource;
  363. this._gainNode = this._chain.nodes().gainNode;
  364. this._analyser = this._chain.nodes().analyser;
  365. this._panner = this._chain.nodes().panner;
  366. // sanity check
  367. console.assert(this._source, "no bufferSource: not yet supported")
  368. console.assert(this._gainNode, "no gainNode: not yet supported")
  369. console.assert(this._analyser, "no analyser: not yet supported")
  370. console.assert(this._panner, "no panner: not yet supported")
  371. };
  372. WebAudio.Sound.create = function (webaudio, nodeChain) {
  373. return new WebAudio.Sound(webaudio, nodeChain);
  374. }
  375. /**
  376. * destructor
  377. */
  378. WebAudio.Sound.prototype.destroy = function () {
  379. // disconnect from this._webaudio
  380. this._chain.last().disconnect();
  381. // destroy this._chain
  382. this._chain.destroy();
  383. this._chain = null;
  384. };
  385. /**
  386. * vendor.js way to make plugins ala jQuery
  387. * @namespace
  388. */
  389. WebAudio.Sound.fn = WebAudio.Sound.prototype;
  390. //////////////////////////////////////////////////////////////////////////////////
  391. // //
  392. //////////////////////////////////////////////////////////////////////////////////
  393. /**
  394. * getter of the chain nodes
  395. */
  396. WebAudio.Sound.prototype.nodes = function () {
  397. return this._chain.nodes();
  398. };
  399. /**
  400. * @returns {Boolean} true if the sound is playable, false otherwise
  401. */
  402. WebAudio.Sound.prototype.isPlayable = function () {
  403. return this._source.buffer ? true : false;
  404. };
  405. /**
  406. * play the sound
  407. *
  408. * @param {Number} [time] time when to play the sound
  409. */
  410. WebAudio.Sound.prototype.play = function (time) {
  411. // handle parameter polymorphism
  412. if (time === undefined) time = 0;
  413. // if not yet playable, ignore
  414. // - usefull when the sound download isnt yet completed
  415. if (this.isPlayable() === false) return;
  416. // clone the bufferSource
  417. var clonedNode = this._chain.cloneBufferSource();
  418. // set the noteOn
  419. clonedNode.start(time);
  420. // create the source object
  421. var source = {
  422. node: clonedNode,
  423. stop: function (time) {
  424. if (time === undefined) time = 0;
  425. this.node.stop(time);
  426. return source; // for chained API
  427. }
  428. }
  429. // return it
  430. return source;
  431. };
  432. /**
  433. * getter/setter on the volume
  434. *
  435. * @param {Number} [value] the value to set, if not provided, get current value
  436. */
  437. WebAudio.Sound.prototype.volume = function (value) {
  438. if (value === undefined) return this._gainNode.gain.value;
  439. this._gainNode.gain.value = value;
  440. return this; // for chained API
  441. };
  442. /**
  443. * getter/setter on the loop
  444. *
  445. * @param {Number} [value] the value to set, if not provided, get current value
  446. */
  447. WebAudio.Sound.prototype.loop = function (value) {
  448. if (value === undefined) return this._source.loop;
  449. this._source.loop = value;
  450. return this; // for chained API
  451. };
  452. /**
  453. * getter/setter on the source buffer
  454. *
  455. * @param {Number} [value] the value to set, if not provided, get current value
  456. */
  457. WebAudio.Sound.prototype.buffer = function (value) {
  458. if (value === undefined) return this._source.buffer;
  459. this._source.buffer = value;
  460. return this; // for chained API
  461. };
  462. /**
  463. * Set parameter for the pannerCone
  464. *
  465. * @param {Number} innerAngle the inner cone hangle in radian
  466. * @param {Number} outerAngle the outer cone hangle in radian
  467. * @param {Number} outerGain the gain to apply when in the outerCone
  468. */
  469. WebAudio.Sound.prototype.pannerCone = function (innerAngle, outerAngle, outerGain) {
  470. this._panner.coneInnerAngle = innerAngle * 180 / Math.PI;
  471. this._panner.coneOuterAngle = outerAngle * 180 / Math.PI;
  472. this._panner.coneOuterGain = outerGain;
  473. return this; // for chained API
  474. };
  475. /**
  476. * getter/setter on the pannerConeInnerAngle
  477. *
  478. * @param {Number} value the angle in radian
  479. */
  480. WebAudio.Sound.prototype.pannerConeInnerAngle = function (value) {
  481. if (value === undefined) return this._panner.coneInnerAngle / 180 * Math.PI;
  482. this._panner.coneInnerAngle = value * 180 / Math.PI;
  483. return this; // for chained API
  484. };
  485. /**
  486. * getter/setter on the pannerConeOuterAngle
  487. *
  488. * @param {Number} value the angle in radian
  489. */
  490. WebAudio.Sound.prototype.pannerConeOuterAngle = function (value) {
  491. if (value === undefined) return this._panner.coneOuterAngle / 180 * Math.PI;
  492. this._panner.coneOuterAngle = value * 180 / Math.PI;
  493. return this; // for chained API
  494. };
  495. /**
  496. * getter/setter on the pannerConeOuterGain
  497. *
  498. * @param {Number} value the value
  499. */
  500. WebAudio.Sound.prototype.pannerConeOuterGain = function (value) {
  501. if (value === undefined) return this._panner.coneOuterGain;
  502. this._panner.coneOuterGain = value;
  503. return this; // for chained API
  504. };
  505. /**
  506. * compute the amplitude of the sound (not sure at all it is the proper term)
  507. *
  508. * @param {Number} width the number of frequencyBin to take into account
  509. * @returns {Number} return the amplitude of the sound
  510. */
  511. WebAudio.Sound.prototype.amplitude = function (width) {
  512. // handle paramerter
  513. width = width !== undefined ? width : 2;
  514. // inint variable
  515. var analyser = this._analyser;
  516. var freqByte = new Uint8Array(analyser.frequencyBinCount);
  517. // get the frequency data
  518. analyser.getByteFrequencyData(freqByte);
  519. // compute the sum
  520. var sum = 0;
  521. for (var i = 0; i < width; i++) {
  522. sum += freqByte[i];
  523. }
  524. // complute the amplitude
  525. var amplitude = sum / (width * 256 - 1);
  526. // return ampliture
  527. return amplitude;
  528. }
  529. /**
  530. * Generate a sinusoid buffer.
  531. * FIXME should likely be in a plugin
  532. */
  533. WebAudio.Sound.prototype.tone = function (hertz, seconds) {
  534. // handle parameter
  535. hertz = hertz !== undefined ? hertz : 200;
  536. seconds = seconds !== undefined ? seconds : 1;
  537. // set default value
  538. var nChannels = 1;
  539. var sampleRate = 44100;
  540. var amplitude = 2;
  541. // create the buffer
  542. var buffer = this._webaudio.context().createBuffer(nChannels, seconds * sampleRate, sampleRate);
  543. var fArray = buffer.getChannelData(0);
  544. // fill the buffer
  545. for (var i = 0; i < fArray.length; i++) {
  546. var time = i / buffer.sampleRate;
  547. var angle = hertz * time * Math.PI;
  548. fArray[i] = Math.sin(angle) * amplitude;
  549. }
  550. // set the buffer
  551. this.buffer(buffer);
  552. return this; // for chained API
  553. }
  554. /**
  555. * Put this function is .Sound with getByt as private callback
  556. */
  557. WebAudio.Sound.prototype.makeHistogram = function (nBar) {
  558. // get analyser node
  559. var analyser = this._analyser;
  560. // allocate the private histo if needed - to avoid allocating at every frame
  561. //this._privHisto = this._privHisto || new Float32Array(analyser.frequencyBinCount);
  562. this._privHisto = this._privHisto || new Uint8Array(analyser.frequencyBinCount);
  563. // just an alias
  564. var freqData = this._privHisto;
  565. // get the data
  566. //analyser.getFloatFrequencyData(freqData);
  567. analyser.getByteFrequencyData(freqData);
  568. //analyser.getByteTimeDomainData(freqData);
  569. /**
  570. * This should be in imageprocessing.js almost
  571. */
  572. var makeHisto = function (srcArr, dstLength) {
  573. var barW = Math.floor(srcArr.length / dstLength);
  574. var nBar = Math.floor(srcArr.length / barW);
  575. var arr = []
  576. for (var x = 0, arrIdx = 0; x < srcArr.length; arrIdx++) {
  577. var sum = 0;
  578. for (var i = 0; i < barW; i++, x++) {
  579. sum += srcArr[x];
  580. }
  581. var average = sum / barW;
  582. arr[arrIdx] = average;
  583. }
  584. return arr;
  585. }
  586. // build the histo
  587. var histo = makeHisto(freqData, nBar);
  588. // return it
  589. return histo;
  590. }
  591. //////////////////////////////////////////////////////////////////////////////////
  592. // //
  593. //////////////////////////////////////////////////////////////////////////////////
  594. /**
  595. * Load a sound
  596. *
  597. * @param {String} url the url of the sound to load
  598. * @param {Function} onSuccess function to notify once the url is loaded (optional)
  599. * @param {Function} onError function to notify if an error occurs (optional)
  600. */
  601. WebAudio.Sound.prototype.load = function (url, onSuccess, onError) {
  602. // handle default arguments
  603. onError = onError || function () {
  604. console.warn("unable to load sound " + url);
  605. };
  606. // try to load the user
  607. this._loadAndDecodeSound(url, function (buffer) {
  608. this._source.buffer = buffer;
  609. onSuccess && onSuccess(this);
  610. }.bind(this), function () {
  611. onError && onError(this);
  612. }.bind(this));
  613. return this; // for chained API
  614. };
  615. /**
  616. * Load and decode a sound
  617. *
  618. * @param {String} url the url where to get the sound
  619. * @param {Function} onLoad the function called when the sound is loaded and decoded (optional)
  620. * @param {Function} onError the function called when an error occured (optional)
  621. */
  622. WebAudio.Sound.prototype._loadAndDecodeSound = function (url, onLoad, onError) {
  623. var context = this._context;
  624. context.decodeAudioData(url, function (buffer) {
  625. console.log("OK")
  626. onLoad && onLoad(buffer);
  627. }, function () {
  628. console.log("KO")
  629. onError && onError();
  630. });
  631. }
  632. /**
  633. * gowiththeflow.js - a javascript flow control micro library
  634. * https://github.com/jeromeetienne/gowiththeflow.js
  635. */
  636. WebAudio.Flow = function () {
  637. var self, stack = [], timerId = setTimeout(function () {
  638. timerId = null;
  639. self._next();
  640. }, 0);
  641. return self = {
  642. destroy: function () {
  643. timerId && clearTimeout(timerId);
  644. },
  645. par: function (callback, isSeq) {
  646. if (isSeq || !(stack[stack.length - 1] instanceof Array)) stack.push([]);
  647. stack[stack.length - 1].push(callback);
  648. return self;
  649. }, seq: function (callback) {
  650. return self.par(callback, true);
  651. },
  652. _next: function (err, result) {
  653. var errors = [], results = [], callbacks = stack.shift() || [], nbReturn = callbacks.length, isSeq = nbReturn == 1;
  654. for (var i = 0; i < callbacks.length; i++) {
  655. (function (fct, index) {
  656. fct(function (error, result) {
  657. errors[index] = error;
  658. results[index] = result;
  659. if (--nbReturn == 0) self._next(isSeq ? errors[0] : errors, isSeq ? results[0] : results)
  660. }, err, result)
  661. })(callbacks[i], i);
  662. }
  663. }
  664. }
  665. };