import Tone from "../core/Tone"; import "../core/Emitter"; import "../core/Timeline"; import "../shim/AudioContext"; var AudioContextProperties = ["baseLatency", "destination", "currentTime", "sampleRate", "listener", "state"]; var AudioContextMethods = ["suspend", "close", "resume", "getOutputTimestamp", "createMediaElementSource", "createMediaStreamSource", "createMediaStreamDestination", "createBuffer", "decodeAudioData", "createBufferSource", "createConstantSource", "createGain", "createDelay", "createBiquadFilter", "createIIRFilter", "createWaveShaper", "createPanner", "createConvolver", "createDynamicsCompressor", "createAnalyser", "createScriptProcessor", "createStereoPanner", "createOscillator", "createPeriodicWave", "createChannelSplitter", "createChannelMerger", "audioWorklet"]; /** * @class Wrapper around the native AudioContext. * @extends {Tone.Emitter} * @param {AudioContext=} context optionally pass in a context */ Tone.Context = function(){ Tone.Emitter.call(this); var options = Tone.defaults(arguments, ["context"], Tone.Context); if (!options.context){ options.context = new Tone.global.AudioContext(); if (!options.context){ throw new Error("could not create AudioContext. Possibly too many AudioContexts running already."); } } this._context = options.context; //make sure it's not an AudioContext wrapper while (this._context.rawContext){ this._context = this._context.rawContext; } // extend all of the properties AudioContextProperties.forEach(function(prop){ this._defineProperty(this._context, prop); }.bind(this)); // extend all of the methods AudioContextMethods.forEach(function(method){ this._defineMethod(this._context, method); }.bind(this)); /** * The default latency hint * @type {String} * @private */ this._latencyHint = options.latencyHint; /** * An object containing all of the constants AudioBufferSourceNodes * @type {Object} * @private */ this._constants = {}; /////////////////////////////////////////////////////////////////////// // WORKER /////////////////////////////////////////////////////////////////////// /** * The amount of time events are scheduled * into the future * @type {Number} */ this.lookAhead = options.lookAhead; /** * A reference to the actual computed update interval * @type {Number} * @private */ this._computedUpdateInterval = 0; /** * A reliable callback method * @private * @type {Ticker} */ this._ticker = new Ticker(this.emit.bind(this, "tick"), options.clockSource, options.updateInterval); /////////////////////////////////////////////////////////////////////// // TIMEOUTS /////////////////////////////////////////////////////////////////////// /** * All of the setTimeout events. * @type {Tone.Timeline} * @private */ this._timeouts = new Tone.Timeline(); /** * The timeout id counter * @private * @type {Number} */ this._timeoutIds = 0; this.on("tick", this._timeoutLoop.bind(this)); //forward state change events this._context.onstatechange = function(e){ this.emit("statechange", e); }.bind(this); }; Tone.extend(Tone.Context, Tone.Emitter); Tone.Emitter.mixin(Tone.Context); /** * defaults * @static * @type {Object} */ Tone.Context.defaults = { "clockSource" : "worker", "latencyHint" : "interactive", "lookAhead" : 0.1, "updateInterval" : 0.03 }; /** * Is an instanceof Tone.Context * @type {Boolean} */ Tone.Context.prototype.isContext = true; /** * Define a property on this Tone.Context. * This is used to extend the native AudioContext * @param {AudioContext} context * @param {String} prop * @private */ Tone.Context.prototype._defineProperty = function(context, prop){ if (Tone.isUndef(this[prop])){ Object.defineProperty(this, prop, { "get" : function(){ return context[prop]; }, "set" : function(val){ context[prop] = val; } }); } }; /** * Define a method on this Tone.Context. * This is used to extend the native AudioContext * @param {AudioContext} context * @param {String} prop * @private */ Tone.Context.prototype._defineMethod = function(context, prop){ if (Tone.isUndef(this[prop])){ Object.defineProperty(this, prop, { "get" : function(){ return context[prop].bind(context); } }); } }; /** * The current audio context time * @return {Number} */ Tone.Context.prototype.now = function(){ return this._context.currentTime + this.lookAhead; }; /** * The audio output destination. Alias for Tone.Master * @readyOnly * @type {Tone.Master} */ Object.defineProperty(Tone.Context.prototype, "destination", { "get" : function(){ if (!this.master){ return this._context.destination; } else { return this.master; } } }); /** * Starts the audio context from a suspended state. This is required * to initially start the AudioContext. * @return {Promise} */ Tone.Context.prototype.resume = function(){ if (this._context.state === "suspended" && this._context instanceof AudioContext){ return this._context.resume(); } else { return Promise.resolve(); } }; /** * Promise which is invoked when the context is running. * Tries to resume the context if it's not started. * @return {Promise} */ Tone.Context.prototype.close = function(){ var closePromise = Promise.resolve(); //never close the global Tone.Context if (this !== Tone.global.TONE_AUDIO_CONTEXT){ closePromise = this.rawContext.close(); } return closePromise.then(function(){ Tone.Context.emit("close", this); }.bind(this)); }; /** * Generate a looped buffer at some constant value. * @param {Number} val * @return {BufferSourceNode} */ Tone.Context.prototype.getConstant = function(val){ if (this._constants[val]){ return this._constants[val]; } else { var buffer = this._context.createBuffer(1, 128, this._context.sampleRate); var arr = buffer.getChannelData(0); for (var i = 0; i < arr.length; i++){ arr[i] = val; } var constant = this._context.createBufferSource(); constant.channelCount = 1; constant.channelCountMode = "explicit"; constant.buffer = buffer; constant.loop = true; constant.start(0); this._constants[val] = constant; return constant; } }; /** * The private loop which keeps track of the context scheduled timeouts * Is invoked from the clock source * @private */ Tone.Context.prototype._timeoutLoop = function(){ var now = this.now(); while (this._timeouts && this._timeouts.length && this._timeouts.peek().time <= now){ this._timeouts.shift().callback(); } }; /** * A setTimeout which is gaurenteed by the clock source. * Also runs in the offline context. * @param {Function} fn The callback to invoke * @param {Seconds} timeout The timeout in seconds * @returns {Number} ID to use when invoking Tone.Context.clearTimeout */ Tone.Context.prototype.setTimeout = function(fn, timeout){ this._timeoutIds++; var now = this.now(); this._timeouts.add({ "callback" : fn, "time" : now + timeout, "id" : this._timeoutIds }); return this._timeoutIds; }; /** * Clears a previously scheduled timeout with Tone.context.setTimeout * @param {Number} id The ID returned from setTimeout * @return {Tone.Context} this */ Tone.Context.prototype.clearTimeout = function(id){ this._timeouts.forEach(function(event){ if (event.id === id){ this.remove(event); } }); return this; }; /** * How often the Web Worker callback is invoked. * This number corresponds to how responsive the scheduling * can be. Context.updateInterval + Context.lookAhead gives you the * total latency between scheduling an event and hearing it. * @type {Number} * @memberOf Tone.Context# * @name updateInterval */ Object.defineProperty(Tone.Context.prototype, "updateInterval", { "get" : function(){ return this._ticker.updateInterval; }, "set" : function(interval){ this._ticker.updateInterval = interval; } }); /** * The unwrapped AudioContext. * @type {AudioContext} * @memberOf Tone.Context# * @name rawContext * @readOnly */ Object.defineProperty(Tone.Context.prototype, "rawContext", { "get" : function(){ return this._context; } }); /** * What the source of the clock is, either "worker" (Web Worker [default]), * "timeout" (setTimeout), or "offline" (none). * @type {String} * @memberOf Tone.Context# * @name clockSource */ Object.defineProperty(Tone.Context.prototype, "clockSource", { "get" : function(){ return this._ticker.type; }, "set" : function(type){ this._ticker.type = type; } }); /** * The type of playback, which affects tradeoffs between audio * output latency and responsiveness. * * In addition to setting the value in seconds, the latencyHint also * accepts the strings "interactive" (prioritizes low latency), * "playback" (prioritizes sustained playback), "balanced" (balances * latency and performance), and "fastest" (lowest latency, might glitch more often). * @type {String|Seconds} * @memberOf Tone.Context# * @name latencyHint * @example * //set the lookAhead to 0.3 seconds * Tone.context.latencyHint = 0.3; */ Object.defineProperty(Tone.Context.prototype, "latencyHint", { "get" : function(){ return this._latencyHint; }, "set" : function(hint){ var lookAhead = hint; this._latencyHint = hint; if (Tone.isString(hint)){ switch (hint){ case "interactive" : lookAhead = 0.1; this._context.latencyHint = hint; break; case "playback" : lookAhead = 0.8; this._context.latencyHint = hint; break; case "balanced" : lookAhead = 0.25; this._context.latencyHint = hint; break; case "fastest" : this._context.latencyHint = "interactive"; lookAhead = 0.01; break; } } this.lookAhead = lookAhead; this.updateInterval = lookAhead/3; } }); /** * Unlike other dispose methods, this returns a Promise * which executes when the context is closed and disposed * @returns {Promise} this */ Tone.Context.prototype.dispose = function(){ return this.close().then(function(){ Tone.Emitter.prototype.dispose.call(this); this._ticker.dispose(); this._ticker = null; this._timeouts.dispose(); this._timeouts = null; for (var con in this._constants){ this._constants[con].disconnect(); } this._constants = null; }.bind(this)); }; /** * @class A class which provides a reliable callback using either * a Web Worker, or if that isn't supported, falls back to setTimeout. * @private */ var Ticker = function(callback, type, updateInterval){ /** * Either "worker" or "timeout" * @type {String} * @private */ this._type = type; /** * The update interval of the worker * @private * @type {Number} */ this._updateInterval = updateInterval; /** * The callback to invoke at regular intervals * @type {Function} * @private */ this._callback = Tone.defaultArg(callback, Tone.noOp); //create the clock source for the first time this._createClock(); }; /** * The possible ticker types * @private * @type {Object} */ Ticker.Type = { "Worker" : "worker", "Timeout" : "timeout", "Offline" : "offline" }; /** * Generate a web worker * @return {WebWorker} * @private */ Ticker.prototype._createWorker = function(){ //URL Shim Tone.global.URL = Tone.global.URL || Tone.global.webkitURL; var blob = new Blob([ //the initial timeout time "var timeoutTime = "+(this._updateInterval * 1000).toFixed(1)+";" + //onmessage callback "self.onmessage = function(msg){" + " timeoutTime = parseInt(msg.data);" + "};" + //the tick function which posts a message //and schedules a new tick "function tick(){" + " setTimeout(tick, timeoutTime);" + " self.postMessage('tick');" + "}" + //call tick initially "tick();" ]); var blobUrl = URL.createObjectURL(blob); var worker = new Worker(blobUrl); worker.onmessage = this._callback.bind(this); this._worker = worker; }; /** * Create a timeout loop * @private */ Ticker.prototype._createTimeout = function(){ this._timeout = setTimeout(function(){ this._createTimeout(); this._callback(); }.bind(this), this._updateInterval * 1000); }; /** * Create the clock source. * @private */ Ticker.prototype._createClock = function(){ if (this._type === Ticker.Type.Worker){ try { this._createWorker(); } catch (e){ // workers not supported, fallback to timeout this._type = Ticker.Type.Timeout; this._createClock(); } } else if (this._type === Ticker.Type.Timeout){ this._createTimeout(); } }; /** * @memberOf Ticker# * @type {Number} * @name updateInterval * @private */ Object.defineProperty(Ticker.prototype, "updateInterval", { "get" : function(){ return this._updateInterval; }, "set" : function(interval){ this._updateInterval = Math.max(interval, 128/44100); if (this._type === Ticker.Type.Worker){ this._worker.postMessage(Math.max(interval * 1000, 1)); } } }); /** * The type of the ticker, either a worker or a timeout * @memberOf Ticker# * @type {Number} * @name type * @private */ Object.defineProperty(Ticker.prototype, "type", { "get" : function(){ return this._type; }, "set" : function(type){ this._disposeClock(); this._type = type; this._createClock(); } }); /** * Clean up the current clock source * @private */ Ticker.prototype._disposeClock = function(){ if (this._timeout){ clearTimeout(this._timeout); this._timeout = null; } if (this._worker){ this._worker.terminate(); this._worker.onmessage = null; this._worker = null; } }; /** * Clean up * @private */ Ticker.prototype.dispose = function(){ this._disposeClock(); this._callback = null; }; // set the audio context initially, and if one is not already created if (Tone.supported && !Tone.initialized){ if (!Tone.global.TONE_AUDIO_CONTEXT){ Tone.global.TONE_AUDIO_CONTEXT = new Tone.Context(); } Tone.context = Tone.global.TONE_AUDIO_CONTEXT; // log on first initialization // allow optional silencing of this log if (!Tone.global.TONE_SILENCE_LOGGING){ var prefix = "v"; if (Tone.version === "dev"){ prefix = ""; } var printString = " * Tone.js " + prefix + Tone.version + " * "; // eslint-disable-next-line no-console console.log("%c" + printString, "background: #000; color: #fff"); } } else if (!Tone.supported && !Tone.global.TONE_SILENCE_LOGGING){ // eslint-disable-next-line no-console console.warn("This browser does not support Tone.js"); } export default Tone.Context;