Tone.js/Tone/core/Tone.js
tboie ba1900dab6 Tone.Disconnect Fix (#483)
-use while loop with a flag
-fixes #461
2019-05-14 21:40:23 -04:00

851 lines
22 KiB
JavaScript

/**
* Tone.js
* @author Yotam Mann
* @license http://opensource.org/licenses/MIT MIT License
* @copyright 2014-2019 Yotam Mann
*/
import version from "../version";
///////////////////////////////////////////////////////////////////////////
// TONE
///////////////////////////////////////////////////////////////////////////
/**
* @class Tone is the base class of all other classes.
* @constructor
*/
var Tone = function(){
if (!(this instanceof Tone)){
throw new Error("constructor needs to be called with the 'new' keyword");
}
};
/**
* @memberOf Tone#
* @returns {String} returns the name of the class as a string
*/
Tone.prototype.toString = function(){
for (var className in Tone){
var isLetter = className[0].match(/^[A-Z]$/);
var sameConstructor = Tone[className] === this.constructor;
if (Tone.isFunction(Tone[className]) && isLetter && sameConstructor){
return className;
}
}
return "Tone";
};
/**
* @memberOf Tone#
* disconnect and dispose
* @returns {Tone} this
*/
Tone.prototype.dispose = function(){
return this;
};
///////////////////////////////////////////////////////////////////////////
// GET/SET
///////////////////////////////////////////////////////////////////////////
/**
* Set the parameters at once. Either pass in an
* object mapping parameters to values, or to set a
* single parameter, by passing in a string and value.
* The last argument is an optional ramp time which
* will ramp any signal values to their destination value
* over the duration of the rampTime.
* @param {Object|String} params
* @param {Number=} value
* @returns {Tone} this
* @memberOf Tone#
* @example
* //set values using an object
* filter.set({
* "frequency" : 300,
* "type" : highpass
* });
* @example
* filter.set("type", "highpass");
* @example
* //ramp to the value 220 over 3 seconds.
* oscillator.set({
* "frequency" : 220
* }, 3);
*/
Tone.prototype.set = function(params, value){
if (Tone.isString(params)){
var tmpObj = {};
tmpObj[params] = value;
params = tmpObj;
}
paramLoop:
for (var attr in params){
value = params[attr];
var parent = this;
if (attr.indexOf(".") !== -1){
var attrSplit = attr.split(".");
for (var i = 0; i < attrSplit.length - 1; i++){
parent = parent[attrSplit[i]];
if (parent instanceof Tone){
attrSplit.splice(0, i+1);
var innerParam = attrSplit.join(".");
parent.set(innerParam, value);
continue paramLoop;
}
}
attr = attrSplit[attrSplit.length - 1];
}
var param = parent[attr];
if (Tone.isUndef(param)){
continue;
}
if ((Tone.Signal && param instanceof Tone.Signal) ||
(Tone.Param && param instanceof Tone.Param)){
if (param.value !== value){
param.value = value;
}
} else if (param instanceof AudioParam){
if (param.value !== value){
param.value = value;
}
} else if (Tone.TimeBase && param instanceof Tone.TimeBase){
parent[attr] = value;
} else if (param instanceof Tone){
param.set(value);
} else if (param !== value){
parent[attr] = value;
}
}
return this;
};
/**
* Get the object's attributes. Given no arguments get
* will return all available object properties and their corresponding
* values. Pass in a single attribute to retrieve or an array
* of attributes. The attribute strings can also include a "."
* to access deeper properties.
* @memberOf Tone#
* @example
* osc.get();
* //returns {"type" : "sine", "frequency" : 440, ...etc}
* @example
* osc.get("type");
* //returns { "type" : "sine"}
* @example
* //use dot notation to access deep properties
* synth.get(["envelope.attack", "envelope.release"]);
* //returns {"envelope" : {"attack" : 0.2, "release" : 0.4}}
* @param {Array=|string|undefined} params the parameters to get, otherwise will return
* all available.
* @returns {Object}
*/
Tone.prototype.get = function(params){
if (Tone.isUndef(params)){
params = this._collectDefaults(this.constructor);
} else if (Tone.isString(params)){
params = [params];
}
var ret = {};
for (var i = 0; i < params.length; i++){
var attr = params[i];
var parent = this;
var subRet = ret;
if (attr.indexOf(".") !== -1){
var attrSplit = attr.split(".");
for (var j = 0; j < attrSplit.length - 1; j++){
var subAttr = attrSplit[j];
subRet[subAttr] = subRet[subAttr] || {};
subRet = subRet[subAttr];
parent = parent[subAttr];
}
attr = attrSplit[attrSplit.length - 1];
}
var param = parent[attr];
if (Tone.isObject(params[attr])){
subRet[attr] = param.get();
} else if (Tone.Signal && param instanceof Tone.Signal){
subRet[attr] = param.value;
} else if (Tone.Param && param instanceof Tone.Param){
subRet[attr] = param.value;
} else if (param instanceof AudioParam){
subRet[attr] = param.value;
} else if (param instanceof Tone){
subRet[attr] = param.get();
} else if (!Tone.isFunction(param) && Tone.isDefined(param)){
subRet[attr] = param;
}
}
return ret;
};
/**
* collect all of the default attributes in one
* @private
* @param {Function} constr the constructor to find the defaults from
* @return {Array} all of the attributes which belong to the class
*/
Tone.prototype._collectDefaults = function(constr){
var ret = [];
if (Tone.isDefined(constr.defaults)){
ret = Object.keys(constr.defaults);
}
if (Tone.isDefined(constr._super)){
var superDefs = this._collectDefaults(constr._super);
//filter out repeats
for (var i = 0; i < superDefs.length; i++){
if (ret.indexOf(superDefs[i]) === -1){
ret.push(superDefs[i]);
}
}
}
return ret;
};
///////////////////////////////////////////////////////////////////////////
// DEFAULTS
///////////////////////////////////////////////////////////////////////////
/**
* @memberOf Tone
* @param {Array} values The arguments array
* @param {Array} keys The names of the arguments
* @param {Function|Object} constr The class constructor
* @return {Object} An object composed of the defaults between the class' defaults
* and the passed in arguments.
*/
Tone.defaults = function(values, keys, constr){
var options = {};
if (values.length === 1 && Tone.isObject(values[0])){
options = values[0];
} else {
for (var i = 0; i < keys.length; i++){
options[keys[i]] = values[i];
}
}
if (Tone.isDefined(constr.defaults)){
return Tone.defaultArg(options, constr.defaults);
} else if (Tone.isObject(constr)){
return Tone.defaultArg(options, constr);
} else {
return options;
}
};
/**
* If the `given` parameter is undefined, use the `fallback`.
* If both `given` and `fallback` are object literals, it will
* return a deep copy which includes all of the parameters from both
* objects. If a parameter is undefined in given, it will return
* the fallback property.
* <br><br>
* WARNING: if object is self referential, it will go into an an
* infinite recursive loop.
* @memberOf Tone
* @param {*} given
* @param {*} fallback
* @return {*}
*/
Tone.defaultArg = function(given, fallback){
if (Tone.isObject(given) && Tone.isObject(fallback)){
var ret = {};
//make a deep copy of the given object
for (var givenProp in given){
ret[givenProp] = Tone.defaultArg(fallback[givenProp], given[givenProp]);
}
for (var fallbackProp in fallback){
ret[fallbackProp] = Tone.defaultArg(given[fallbackProp], fallback[fallbackProp]);
}
return ret;
} else {
return Tone.isUndef(given) ? fallback : given;
}
};
///////////////////////////////////////////////////////////////////////////
// DEBUGGING
///////////////////////////////////////////////////////////////////////////
/**
* Print the outputs to the console log for debugging purposes.
* Prints the contents only if either the object has a property
* called `debug` set to true, or a variable called TONE_DEBUG_CLASS
* is set to the name of the class.
* @example
* //prints all logs originating from Tone.OscillatorNode
* Tone.global.TONE_DEBUG_CLASS = "OscillatorNode"
* @param {*} args Any arguments to print to the console.
* @private
*/
Tone.prototype.log = function(){
//if the object is either set to debug = true
//or if there is a string on the Tone.global.with the class name
if (this.debug || this.toString() === Tone.global.TONE_DEBUG_CLASS){
var args = Array.from(arguments);
args.unshift(this.toString()+":");
// eslint-disable-next-line no-console
console.log.apply(undefined, args);
}
};
/**
* Assert that the statement is true, otherwise invoke the error.
* @param {Boolean} statement
* @param {String} error The message which is passed into an Error
* @private
*/
Tone.prototype.assert = function(statement, error){
if (!statement){
throw new Error(error);
}
};
///////////////////////////////////////////////////////////////////////////
// CONNECTIONS
///////////////////////////////////////////////////////////////////////////
/**
* connect together all of the arguments in series
* @param {...(AudioParam|Tone|AudioNode)} nodes
* @returns {Tone}
* @memberOf Tone
* @static
*/
Tone.connectSeries = function(){
var currentUnit = arguments[0];
for (var i = 1; i < arguments.length; i++){
var toUnit = arguments[i];
Tone.connect(currentUnit, toUnit);
currentUnit = toUnit;
}
return Tone;
};
/**
* Connect two nodes together so that signal flows from the
* first node to the second. The second node can be an AudioParam.
* Optionally specific the input and output channels.
* @param {(AudioNode|Tone.AudioNode)} srcNode The source node
* @param {(AudioNode|Tone.AudioNode|AudioParam|Tone.AudioParam)} dstNode The destination node
* @param {number} [outputNumber=0] The output channel of the srcNode
* @param {number} [inputNumber=0] The input channel of the dstNode
*/
Tone.connect = function(srcNode, dstNode, outputNumber, inputNumber){
//resolve the input of the dstNode
while (Tone.isDefined(dstNode.input)){
if (Tone.isArray(dstNode.input)){
inputNumber = Tone.defaultArg(inputNumber, 0);
dstNode = dstNode.input[inputNumber];
inputNumber = 0;
} else if (dstNode.input){
dstNode = dstNode.input;
}
}
//make the connection
if (dstNode instanceof AudioParam){
srcNode.connect(dstNode, outputNumber);
} else if (dstNode instanceof AudioNode){
srcNode.connect(dstNode, outputNumber, inputNumber);
}
return Tone;
};
/**
* Disconnect a node from all nodes or optionally include a destination node and input/output channels.
* @param {(AudioNode|Tone.AudioNode)} srcNode The source node
* @param {?(AudioNode|Tone.AudioNode|AudioParam|Tone.AudioParam)} dstNode The destination node
* @param {?number} [outputNumber=0] The output channel of the srcNode
* @param {?number} [inputNumber=0] The input channel of the dstNode
*/
Tone.disconnect = function(srcNode, dstNode, outputNumber, inputNumber){
if (dstNode){
//resolve the input of the dstNode
var bDone = false;
while (!bDone){
if (Tone.isArray(dstNode.input)){
if (Tone.isDefined(inputNumber)){
Tone.disconnect(srcNode, dstNode.input[inputNumber], outputNumber);
} else {
dstNode.input.forEach(function(dstNode){
//ignore errors from connections that aren't there
try {
Tone.disconnect(srcNode, dstNode, outputNumber);
// eslint-disable-next-line
} catch (e) {}
});
}
bDone = true;
} else if (dstNode.input){
dstNode = dstNode.input;
} else {
bDone = true;
}
}
//make the connection
if (dstNode instanceof AudioParam){
srcNode.disconnect(dstNode, outputNumber);
} else if (dstNode instanceof AudioNode){
srcNode.disconnect(dstNode, outputNumber, inputNumber);
}
} else {
srcNode.disconnect();
}
return Tone;
};
///////////////////////////////////////////////////////////////////////////
// TYPE CHECKING
///////////////////////////////////////////////////////////////////////////
/**
* Test if the arg is undefined
* @param {*} arg the argument to test
* @returns {Boolean} true if the arg is undefined
* @static
* @memberOf Tone
*/
Tone.isUndef = function(val){
return typeof val === "undefined";
};
/**
* Test if the arg is not undefined
* @param {*} arg the argument to test
* @returns {Boolean} true if the arg is undefined
* @static
* @memberOf Tone
*/
Tone.isDefined = function(val){
return !Tone.isUndef(val);
};
/**
* Test if the arg is a function
* @param {*} arg the argument to test
* @returns {Boolean} true if the arg is a function
* @static
* @memberOf Tone
*/
Tone.isFunction = function(val){
return typeof val === "function";
};
/**
* Test if the argument is a number.
* @param {*} arg the argument to test
* @returns {Boolean} true if the arg is a number
* @static
* @memberOf Tone
*/
Tone.isNumber = function(arg){
return (typeof arg === "number");
};
/**
* Test if the given argument is an object literal (i.e. `{}`);
* @param {*} arg the argument to test
* @returns {Boolean} true if the arg is an object literal.
* @static
* @memberOf Tone
*/
Tone.isObject = function(arg){
return (Object.prototype.toString.call(arg) === "[object Object]" && arg.constructor === Object);
};
/**
* Test if the argument is a boolean.
* @param {*} arg the argument to test
* @returns {Boolean} true if the arg is a boolean
* @static
* @memberOf Tone
*/
Tone.isBoolean = function(arg){
return (typeof arg === "boolean");
};
/**
* Test if the argument is an Array
* @param {*} arg the argument to test
* @returns {Boolean} true if the arg is an array
* @static
* @memberOf Tone
*/
Tone.isArray = function(arg){
return (Array.isArray(arg));
};
/**
* Test if the argument is a string.
* @param {*} arg the argument to test
* @returns {Boolean} true if the arg is a string
* @static
* @memberOf Tone
*/
Tone.isString = function(arg){
return (typeof arg === "string");
};
/**
* Test if the argument is in the form of a note in scientific pitch notation.
* e.g. "C4"
* @param {*} arg the argument to test
* @returns {Boolean} true if the arg is a string
* @static
* @memberOf Tone
*/
Tone.isNote = function(arg){
return Tone.isString(arg) && /^([a-g]{1}(?:b|#|x|bb)?)(-?[0-9]+)/i.test(arg);
};
/**
* An empty function.
* @static
*/
Tone.noOp = function(){};
/**
* Make the property not writable. Internal use only.
* @private
* @param {String} property the property to make not writable
*/
Tone.prototype._readOnly = function(property){
if (Array.isArray(property)){
for (var i = 0; i < property.length; i++){
this._readOnly(property[i]);
}
} else {
Object.defineProperty(this, property, {
"writable" : false,
"enumerable" : true,
});
}
};
/**
* Make an attribute writeable. Interal use only.
* @private
* @param {String} property the property to make writable
*/
Tone.prototype._writable = function(property){
if (Array.isArray(property)){
for (var i = 0; i < property.length; i++){
this._writable(property[i]);
}
} else {
Object.defineProperty(this, property, {
"writable" : true,
});
}
};
/**
* Possible play states.
* @enum {String}
*/
Tone.State = {
"Started" : "started",
"Stopped" : "stopped",
"Paused" : "paused",
};
/**
* A reference to the global context, `global` or `Tone.global.
*/
Tone.global = Tone.isUndef(global) ? window : global;
///////////////////////////////////////////////////////////////////////////
// CONVERSIONS
///////////////////////////////////////////////////////////////////////////
/**
* Equal power gain scale. Good for cross-fading.
* @param {NormalRange} percent (0-1)
* @return {Number} output gain (0-1)
* @static
* @memberOf Tone
*/
Tone.equalPowerScale = function(percent){
var piFactor = 0.5 * Math.PI;
return Math.sin(percent * piFactor);
};
/**
* Convert decibels into gain.
* @param {Decibels} db
* @return {Number}
* @static
* @memberOf Tone
*/
Tone.dbToGain = function(db){
return Math.pow(10, db / 20);
};
/**
* Convert gain to decibels.
* @param {Number} gain (0-1)
* @return {Decibels}
* @static
* @memberOf Tone
*/
Tone.gainToDb = function(gain){
return 20 * (Math.log(gain) / Math.LN10);
};
/**
* Convert an interval (in semitones) to a frequency ratio.
* @param {Interval} interval the number of semitones above the base note
* @return {Number} the frequency ratio
* @static
* @memberOf Tone
* @example
* tone.intervalToFrequencyRatio(0); // 1
* tone.intervalToFrequencyRatio(12); // 2
* tone.intervalToFrequencyRatio(-12); // 0.5
*/
Tone.intervalToFrequencyRatio = function(interval){
return Math.pow(2, (interval/12));
};
///////////////////////////////////////////////////////////////////////////
// TIMING
///////////////////////////////////////////////////////////////////////////
/**
* Return the current time of the AudioContext clock plus
* the lookAhead.
* @return {Number} the currentTime from the AudioContext
* @memberOf Tone#
*/
Tone.prototype.now = function(){
return Tone.context.now();
};
/**
* Return the current time of the AudioContext clock plus
* the lookAhead.
* @return {Number} the currentTime from the AudioContext
* @static
* @memberOf Tone
*/
Tone.now = function(){
return Tone.context.now();
};
/**
* Return the current time of the AudioContext clock without
* any lookAhead.
* @return {Number} the currentTime from the AudioContext
* @memberOf Tone#
*/
Tone.prototype.immediate = function(){
return Tone.context.currentTime;
};
/**
* Return the current time of the AudioContext clock without
* any lookAhead.
* @return {Number} the currentTime from the AudioContext
* @memberOf Tone
*/
Tone.immediate = function(){
return Tone.context.currentTime;
};
///////////////////////////////////////////////////////////////////////////
// INHERITANCE
///////////////////////////////////////////////////////////////////////////
/**
* have a child inherit all of Tone's (or a parent's) prototype
* to inherit the parent's properties, make sure to call
* Parent.call(this) in the child's constructor
*
* based on closure library's inherit function
*
* @memberOf Tone
* @static
* @param {Function} child
* @param {Function=} parent (optional) parent to inherit from
* if no parent is supplied, the child
* will inherit from Tone
*/
Tone.extend = function(child, parent){
if (Tone.isUndef(parent)){
parent = Tone;
}
function TempConstructor(){}
TempConstructor.prototype = parent.prototype;
child.prototype = new TempConstructor();
/** @override */
child.prototype.constructor = child;
child._super = parent;
};
///////////////////////////////////////////////////////////////////////////
// CONTEXT
///////////////////////////////////////////////////////////////////////////
/**
* The shared AudioContext
* @type {Tone.Context}
* @private
*/
Tone._audioContext = null;
/**
* Most browsers will not play _any_ audio until a user
* clicks something (like a play button). Invoke this method
* on a click or keypress event handler to start the audio context.
* More about the Autoplay policy [here](https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#webaudio)
* @memberOf Tone
* @static
* @return {Promise} This promise is resolved when the audio context is started.
* @example
* document.querySelector('#playbutton').addEventListener('click', () => Tone.start())
*/
Tone.start = function(){
return Tone.context.resume();
};
/**
* A static pointer to the audio context accessible as Tone.context.
* @type {Tone.Context}
* @name context
* @memberOf Tone
*/
Object.defineProperty(Tone, "context", {
"get" : function(){
return Tone._audioContext;
},
"set" : function(context){
if (context.isContext){
Tone._audioContext = context;
} else {
Tone._audioContext = new Tone.Context(context);
}
//initialize the new audio context
Tone.Context.emit("init", Tone._audioContext);
}
});
/**
* The AudioContext
* @type {Tone.Context}
* @name context
* @memberOf Tone#
* @readOnly
*/
Object.defineProperty(Tone.prototype, "context", {
"get" : function(){
return Tone.context;
}
});
/**
* Tone automatically creates a context on init, but if you are working
* with other libraries which also create an AudioContext, it can be
* useful to set your own. If you are going to set your own context,
* be sure to do it at the start of your code, before creating any objects.
* @static
* @param {AudioContext} ctx The new audio context to set
*/
Tone.setContext = function(ctx){
Tone.context = ctx;
};
///////////////////////////////////////////////////////////////////////////
// ATTRIBUTES
///////////////////////////////////////////////////////////////////////////
/**
* The number of seconds of 1 processing block (128 samples)
* @type {Number}
* @name blockTime
* @memberOf Tone
* @static
* @readOnly
*/
Object.defineProperty(Tone.prototype, "blockTime", {
"get" : function(){
return 128 / this.context.sampleRate;
}
});
/**
* The duration in seconds of one sample.
* @type {Number}
* @name sampleTime
* @memberOf Tone
* @static
* @readOnly
*/
Object.defineProperty(Tone.prototype, "sampleTime", {
"get" : function(){
return 1 / this.context.sampleRate;
}
});
/**
* Whether or not all the technologies that Tone.js relies on are supported by the current browser.
* @type {Boolean}
* @name supported
* @memberOf Tone
* @readOnly
* @static
*/
Object.defineProperty(Tone, "supported", {
"get" : function(){
var hasAudioContext = Tone.global.hasOwnProperty("AudioContext") || Tone.global.hasOwnProperty("webkitAudioContext");
var hasPromises = Tone.global.hasOwnProperty("Promise");
return hasAudioContext && hasPromises;
}
});
/**
* Boolean value if the audio context has been initialized.
* @type {Boolean}
* @memberOf Tone
* @static
* @name initialized
* @readOnly
*/
Object.defineProperty(Tone, "initialized", {
"get" : function(){
return Boolean(Tone.context);
}
});
/**
* Get the context when it becomes available
* @param {Function} resolve Callback when the context is initialized
* @return {Tone}
*/
Tone.getContext = function(resolve){
if (Tone.initialized){
resolve(Tone.context);
} else {
var resCallback = function(){
resolve(Tone.context);
Tone.Context.off("init", resCallback);
};
Tone.Context.on("init", resCallback);
}
return Tone;
};
/**
* The version number
* @type {String}
* @static
*/
Tone.version = version;
export default Tone;