Tone.js/Tone/source/buffer/BufferSource.js
2019-06-19 09:54:47 -04:00

411 lines
10 KiB
JavaScript

import Tone from "../core/Tone";
import "../core/Buffer";
import "../source/Source";
import "../core/Gain";
import "../core/AudioNode";
/**
* @class Wrapper around the native BufferSourceNode.
* @extends {Tone.AudioNode}
* @param {AudioBuffer|Tone.Buffer} buffer The buffer to play
* @param {Function} onload The callback to invoke when the
* buffer is done playing.
*/
const ToneBufferSource = function() {
var options = Tone.defaults(arguments, ["buffer", "onload"], ToneBufferSource);
Tone.AudioNode.call(this, options);
/**
* The callback to invoke after the
* buffer source is done playing.
* @type {Function}
*/
this.onended = options.onended;
/**
* The time that the buffer was started.
* @type {Number}
* @private
*/
this._startTime = -1;
/**
* An additional flag if the actual BufferSourceNode
* has been started. b/c stopping an unstarted buffer
* will throw it into an invalid state
* @type {Boolean}
* @private
*/
this._sourceStarted = false;
/**
* Flag if the source has already been stopped
* @type {Boolean}
* @private
*/
this._sourceStopped = false;
/**
* The time that the buffer is scheduled to stop.
* @type {Number}
* @private
*/
this._stopTime = -1;
/**
* The gain node which envelopes the BufferSource
* @type {Tone.Gain}
* @private
*/
this._gainNode = this.output = new Tone.Gain(0);
/**
* The buffer source
* @type {AudioBufferSourceNode}
* @private
*/
this._source = this.context.createBufferSource();
Tone.connect(this._source, this._gainNode);
this._source.onended = this._onended.bind(this);
/**
* The private buffer instance
* @type {Tone.Buffer}
* @private
*/
this._buffer = new Tone.Buffer(options.buffer, options.onload);
/**
* The playbackRate of the buffer
* @type {Positive}
* @signal
*/
this.playbackRate = new Tone.Param({
param : this._source.playbackRate,
units : Tone.Type.Positive,
value : options.playbackRate,
});
/**
* The fadeIn time of the amplitude envelope.
* @type {Time}
*/
this.fadeIn = options.fadeIn;
/**
* The fadeOut time of the amplitude envelope.
* @type {Time}
*/
this.fadeOut = options.fadeOut;
/**
* The curve applied to the fades, either "linear" or "exponential"
* @type {String}
*/
this.curve = options.curve;
/**
* The onended timeout
* @type {Number}
* @private
*/
this._onendedTimeout = -1;
//set some values initially
this.loop = options.loop;
this.loopStart = options.loopStart;
this.loopEnd = options.loopEnd;
};
// Tone.extend(ToneBufferSource, Tone.AudioNode);
/**
* The defaults
* @const
* @type {Object}
*/
// ToneBufferSource.defaults = {
// "onended" : Tone.noOp,
// "onload" : Tone.noOp,
// "loop" : false,
// "loopStart" : 0,
// "loopEnd" : 0,
// "fadeIn" : 0,
// "fadeOut" : 0,
// "curve" : "linear",
// "playbackRate" : 1,
// };
/**
* Returns the playback state of the source, either "started" or "stopped".
* @type {Tone.State}
* @readOnly
* @memberOf ToneBufferSource#
* @name state
*/
Object.defineProperty(ToneBufferSource.prototype, "state", {
get : function() {
return this.getStateAtTime(this.now());
},
});
/**
* Get the playback state at the given time
* @param {Time} time The time to test the state at
* @return {Tone.State} The playback state.
*/
ToneBufferSource.prototype.getStateAtTime = function(time) {
time = this.toSeconds(time);
if (this._startTime !== -1 &&
this._startTime <= time &&
(this._stopTime === -1 || time < this._stopTime) &&
!this._sourceStopped) {
return Tone.State.Started;
} else {
return Tone.State.Stopped;
}
};
/**
* Start the buffer
* @param {Time} [startTime=now] When the player should start.
* @param {Time} [offset=0] The offset from the beginning of the sample
* to start at.
* @param {Time=} duration How long the sample should play. If no duration
* is given, it will default to the full length
* of the sample (minus any offset)
* @param {Gain} [gain=1] The gain to play the buffer back at.
* @return {ToneBufferSource} this
*/
ToneBufferSource.prototype.start = function(time, offset, duration, gain) {
this.log("start", time, offset, duration, gain);
this.assert(this._startTime === -1, "can only be started once");
this.assert(this.buffer.loaded, "buffer is either not set or not loaded");
this.assert(!this._sourceStopped, "source is already stopped");
time = this.toSeconds(time);
//if it's a loop the default offset is the loopstart point
if (this.loop) {
offset = Tone.defaultArg(offset, this.loopStart);
} else {
//otherwise the default offset is 0
offset = Tone.defaultArg(offset, 0);
}
offset = this.toSeconds(offset);
//make sure the offset is not less than 0
offset = Math.max(offset, 0);
gain = Tone.defaultArg(gain, 1);
//apply a fade in envelope
var fadeInTime = this.toSeconds(this.fadeIn);
if (fadeInTime > 0) {
this._gainNode.gain.setValueAtTime(0, time);
if (this.curve === "linear") {
this._gainNode.gain.linearRampToValueAtTime(gain, time + fadeInTime);
} else {
this._gainNode.gain.exponentialApproachValueAtTime(gain, time, fadeInTime);
}
} else {
this._gainNode.gain.setValueAtTime(gain, time);
}
this._startTime = time;
//if a duration is given, schedule a stop
if (Tone.isDefined(duration)) {
var computedDur = this.toSeconds(duration);
//make sure it's never negative
computedDur = Math.max(computedDur, 0);
this.stop(time + computedDur);
}
//start the buffer source
if (this.loop) {
//modify the offset if it's greater than the loop time
var loopEnd = this.loopEnd || this.buffer.duration;
var loopStart = this.loopStart;
var loopDuration = loopEnd - loopStart;
//move the offset back
if (offset >= loopEnd) {
offset = ((offset - loopStart) % loopDuration) + loopStart;
}
}
this._source.buffer = this.buffer.get();
this._source.loopEnd = this.loopEnd || this.buffer.duration;
if (offset < this.buffer.duration) {
this._sourceStarted = true;
this._source.start(time, offset);
}
return this;
};
/**
* Stop the buffer.
* @param {Time=} time The time the buffer should stop.
* @return {ToneBufferSource} this
*/
ToneBufferSource.prototype.stop = function(time) {
this.log("stop", time);
this.assert(this.buffer.loaded, "buffer is either not set or not loaded");
this.assert(!this._sourceStopped, "source is already stopped");
time = this.toSeconds(time);
//if the event has already been scheduled, clear it
if (this._stopTime !== -1) {
this.cancelStop();
}
//the fadeOut time
var fadeOutTime = this.toSeconds(this.fadeOut);
//cancel the previous curve
this._stopTime = time + fadeOutTime;
if (fadeOutTime > 0) {
//start the fade out curve at the given time
if (this.curve === "linear") {
this._gainNode.gain.linearRampTo(0, fadeOutTime, time);
} else {
this._gainNode.gain.targetRampTo(0, fadeOutTime, time);
}
} else {
//stop any ongoing ramps, and set the value to 0
this._gainNode.gain.cancelAndHoldAtTime(time);
this._gainNode.gain.setValueAtTime(0, time);
}
Tone.context.clearTimeout(this._onendedTimeout);
this._onendedTimeout = Tone.context.setTimeout(this._onended.bind(this), this._stopTime - this.now());
return this;
};
/**
* Cancel a scheduled stop event
* @return {ToneBufferSource} this
*/
ToneBufferSource.prototype.cancelStop = function() {
if (this._startTime !== -1 && !this._sourceStopped) {
//cancel the stop envelope
var fadeInTime = this.toSeconds(this.fadeIn);
this._gainNode.gain.cancelScheduledValues(this._startTime + fadeInTime + this.sampleTime);
this.context.clearTimeout(this._onendedTimeout);
this._stopTime = -1;
}
return this;
};
/**
* Internal callback when the buffer is ended.
* Invokes `onended` and disposes the node.
* @private
*/
ToneBufferSource.prototype._onended = function() {
if (!this._sourceStopped) {
this._sourceStopped = true;
//allow additional time for the exponential curve to fully decay
var additionalTail = this.curve === "exponential" ? this.fadeOut * 2 : 0;
if (this._sourceStarted && this._stopTime !== -1) {
this._source.stop(this._stopTime + additionalTail);
}
this.onended(this);
//dispose the source after it's come to a stop
setTimeout(function() {
//if it hasn't already been disposed
if (this._source) {
this._source.disconnect();
this._gainNode.disconnect();
}
}.bind(this), additionalTail * 1000 + 100);
}
};
/**
* If loop is true, the loop will start at this position.
* @memberOf ToneBufferSource#
* @type {Time}
* @name loopStart
*/
Object.defineProperty(ToneBufferSource.prototype, "loopStart", {
get : function() {
return this._source.loopStart;
},
set : function(loopStart) {
this._source.loopStart = this.toSeconds(loopStart);
},
});
/**
* If loop is true, the loop will end at this position.
* @memberOf ToneBufferSource#
* @type {Time}
* @name loopEnd
*/
Object.defineProperty(ToneBufferSource.prototype, "loopEnd", {
get : function() {
return this._source.loopEnd;
},
set : function(loopEnd) {
this._source.loopEnd = this.toSeconds(loopEnd);
},
});
/**
* The audio buffer belonging to the player.
* @memberOf ToneBufferSource#
* @type {Tone.Buffer}
* @name buffer
*/
Object.defineProperty(ToneBufferSource.prototype, "buffer", {
get : function() {
return this._buffer;
},
set : function(buffer) {
this._buffer.set(buffer);
},
});
/**
* If the buffer should loop once it's over.
* @memberOf ToneBufferSource#
* @type {Boolean}
* @name loop
*/
Object.defineProperty(ToneBufferSource.prototype, "loop", {
get : function() {
return this._source.loop;
},
set : function(loop) {
this._source.loop = loop;
this.cancelStop();
},
});
/**
* Clean up.
* @return {ToneBufferSource} this
*/
ToneBufferSource.prototype.dispose = function() {
if (!this._wasDisposed) {
this._wasDisposed = true;
Tone.AudioNode.prototype.dispose.call(this);
this.onended = null;
this._source.onended = null;
this._source.disconnect();
this._source = null;
this._gainNode.dispose();
this._gainNode = null;
this._buffer.dispose();
this._buffer = null;
this._startTime = -1;
this.playbackRate = null;
Tone.context.clearTimeout(this._onendedTimeout);
}
return this;
};