mirror of
https://github.com/Tonejs/Tone.js
synced 2024-11-16 16:48:00 +00:00
4dd5c0a464
integrates TickSource to track position even with playbackRate changes Fixes #292 and Fixes #307
515 lines
14 KiB
JavaScript
515 lines
14 KiB
JavaScript
define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source/TickSource",
|
|
"Tone/source/BufferSource"], function(Tone){
|
|
|
|
"use strict";
|
|
|
|
/**
|
|
* @class Tone.Player is an audio file player with start, loop, and stop functions.
|
|
*
|
|
* @constructor
|
|
* @extends {Tone.Source}
|
|
* @param {string|AudioBuffer} url Either the AudioBuffer or the url from
|
|
* which to load the AudioBuffer
|
|
* @param {Function=} onload The function to invoke when the buffer is loaded.
|
|
* Recommended to use Tone.Buffer.on('load') instead.
|
|
* @example
|
|
* var player = new Tone.Player("./path/to/sample.mp3").toMaster();
|
|
* //play as soon as the buffer is loaded
|
|
* player.autostart = true;
|
|
*/
|
|
Tone.Player = function(url){
|
|
|
|
var options;
|
|
if (url instanceof Tone.Buffer && url.loaded){
|
|
url = url.get();
|
|
options = Tone.Player.defaults;
|
|
} else {
|
|
options = Tone.defaults(arguments, ["url", "onload"], Tone.Player);
|
|
}
|
|
Tone.Source.call(this, options);
|
|
|
|
/**
|
|
* If the file should play as soon
|
|
* as the buffer is loaded.
|
|
* @type {Boolean}
|
|
* @example
|
|
* //will play as soon as it's loaded
|
|
* var player = new Tone.Player({
|
|
* "url" : "./path/to/sample.mp3",
|
|
* "autostart" : true,
|
|
* }).toMaster();
|
|
*/
|
|
this.autostart = options.autostart;
|
|
|
|
/**
|
|
* the buffer
|
|
* @private
|
|
* @type {Tone.Buffer}
|
|
*/
|
|
this._buffer = new Tone.Buffer({
|
|
"url" : options.url,
|
|
"onload" : this._onload.bind(this, options.onload),
|
|
"reverse" : options.reverse
|
|
});
|
|
if (url instanceof AudioBuffer){
|
|
this._buffer.set(url);
|
|
}
|
|
|
|
/**
|
|
* if the buffer should loop once it's over
|
|
* @type {Boolean}
|
|
* @private
|
|
*/
|
|
this._loop = options.loop;
|
|
|
|
/**
|
|
* if 'loop' is true, the loop will start at this position
|
|
* @type {Time}
|
|
* @private
|
|
*/
|
|
this._loopStart = options.loopStart;
|
|
|
|
/**
|
|
* if 'loop' is true, the loop will end at this position
|
|
* @type {Time}
|
|
* @private
|
|
*/
|
|
this._loopEnd = options.loopEnd;
|
|
|
|
/**
|
|
* the playback rate
|
|
* @private
|
|
* @type {Number}
|
|
*/
|
|
this._playbackRate = options.playbackRate;
|
|
|
|
/**
|
|
* The elapsed time counter.
|
|
* @type {Tone.TickSource}
|
|
* @private
|
|
*/
|
|
this._elapsedTime = new Tone.TickSource(options.playbackRate);
|
|
|
|
/**
|
|
* Enabling retrigger will allow a player to be restarted
|
|
* before the the previous 'start' is done playing. Otherwise,
|
|
* successive calls to Tone.Player.start will only start
|
|
* the sample if it had played all the way through.
|
|
* @type {Boolean}
|
|
*/
|
|
this.retrigger = options.retrigger;
|
|
|
|
/**
|
|
* 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;
|
|
};
|
|
|
|
Tone.extend(Tone.Player, Tone.Source);
|
|
|
|
/**
|
|
* the default parameters
|
|
* @static
|
|
* @const
|
|
* @type {Object}
|
|
*/
|
|
Tone.Player.defaults = {
|
|
"onload" : Tone.noOp,
|
|
"playbackRate" : 1,
|
|
"loop" : false,
|
|
"autostart" : false,
|
|
"loopStart" : 0,
|
|
"loopEnd" : 0,
|
|
"retrigger" : false,
|
|
"reverse" : false,
|
|
"fadeIn" : 0,
|
|
"fadeOut" : 0
|
|
};
|
|
|
|
/**
|
|
* Load the audio file as an audio buffer.
|
|
* Decodes the audio asynchronously and invokes
|
|
* the callback once the audio buffer loads.
|
|
* Note: this does not need to be called if a url
|
|
* was passed in to the constructor. Only use this
|
|
* if you want to manually load a new url.
|
|
* @param {string} url The url of the buffer to load.
|
|
* Filetype support depends on the
|
|
* browser.
|
|
* @param {Function=} callback The function to invoke once
|
|
* the sample is loaded.
|
|
* @returns {Promise}
|
|
*/
|
|
Tone.Player.prototype.load = function(url, callback){
|
|
return this._buffer.load(url, this._onload.bind(this, callback));
|
|
};
|
|
|
|
/**
|
|
* Internal callback when the buffer is loaded.
|
|
* @private
|
|
*/
|
|
Tone.Player.prototype._onload = function(callback){
|
|
callback = Tone.defaultArg(callback, Tone.noOp);
|
|
callback(this);
|
|
if (this.autostart){
|
|
this.start();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Play the buffer at the given startTime. Optionally add an offset
|
|
* and/or duration which will play the buffer from a position
|
|
* within the buffer for the given duration.
|
|
*
|
|
* @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)
|
|
* @returns {Tone.Player} this
|
|
* @memberOf Tone.Player#
|
|
* @method start
|
|
* @name start
|
|
*/
|
|
|
|
/**
|
|
* Internal start method
|
|
* @private
|
|
*/
|
|
Tone.Player.prototype._start = function(startTime, offset, duration){
|
|
//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);
|
|
}
|
|
|
|
//compute the values in seconds
|
|
offset = this.toSeconds(offset);
|
|
var computedDuration = Tone.defaultArg(duration, Math.max(this._buffer.duration - offset, 0));
|
|
computedDuration = this.toSeconds(computedDuration);
|
|
startTime = this.toSeconds(startTime);
|
|
|
|
//start the elapsed time counter
|
|
this._elapsedTime.start(startTime, offset);
|
|
|
|
//make the source
|
|
var source = new Tone.BufferSource({
|
|
"buffer" : this._buffer,
|
|
"loop" : this._loop,
|
|
"loopStart" : this._loopStart,
|
|
"loopEnd" : this._loopEnd,
|
|
"playbackRate" : this._playbackRate,
|
|
"fadeIn" : this.fadeIn,
|
|
"fadeOut" : this.fadeOut,
|
|
}).connect(this.output);
|
|
|
|
//set the looping properties
|
|
if (!this._loop && !this._synced){
|
|
//if it's not looping, set the state change at the end of the sample
|
|
this._state.setStateAtTime(Tone.State.Stopped, startTime + computedDuration / this._playbackRate);
|
|
}
|
|
|
|
var event = this._state.get(startTime);
|
|
event.source = source;
|
|
|
|
//start it
|
|
if (this._loop && Tone.isUndef(duration)){
|
|
source.start(startTime, offset);
|
|
} else {
|
|
source.start(startTime, offset, computedDuration);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Stop playback.
|
|
* @private
|
|
* @param {Time} [time=now]
|
|
* @returns {Tone.Player} this
|
|
*/
|
|
Tone.Player.prototype._stop = function(time){
|
|
time = this.toSeconds(time);
|
|
this._elapsedTime.stop(time);
|
|
this._state.forEachFrom(0, function(event){
|
|
if (event.source){
|
|
event.source.stop(time);
|
|
}
|
|
});
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Stop and then restart the player from the beginning (or offset)
|
|
* @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)
|
|
* @returns {Tone.Player} this
|
|
*/
|
|
Tone.Player.prototype.restart = function(time, offset, duration){
|
|
if (!this.retrigger){
|
|
this.stop(time);
|
|
}
|
|
this._start(time, offset, duration);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Seek to a specific time in the player's buffer. If the
|
|
* source is no longer playing at that time, it will stop.
|
|
* If you seek to a time that
|
|
* @param {Time} offset The time to seek to.
|
|
* @param {Time=} time The time for the seek event to occur.
|
|
* @return {Tone.Player} this
|
|
* @example
|
|
* source.start(0.2);
|
|
* source.stop(0.4);
|
|
*/
|
|
Tone.Player.prototype.seek = function(offset, time){
|
|
time = this.toSeconds(time);
|
|
if (this._state.getValueAtTime(time) === Tone.State.Started){
|
|
offset = this.toSeconds(offset);
|
|
// if it's currently playing, stop it
|
|
this._stop(time);
|
|
//restart it at the given time
|
|
this._start(time, offset);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Set the loop start and end. Will only loop if loop is
|
|
* set to true.
|
|
* @param {Time} loopStart The loop end time
|
|
* @param {Time} loopEnd The loop end time
|
|
* @returns {Tone.Player} this
|
|
* @example
|
|
* //loop 0.1 seconds of the file.
|
|
* player.setLoopPoints(0.2, 0.3);
|
|
* player.loop = true;
|
|
*/
|
|
Tone.Player.prototype.setLoopPoints = function(loopStart, loopEnd){
|
|
this.loopStart = loopStart;
|
|
this.loopEnd = loopEnd;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* If loop is true, the loop will start at this position.
|
|
* @memberOf Tone.Player#
|
|
* @type {Time}
|
|
* @name loopStart
|
|
*/
|
|
Object.defineProperty(Tone.Player.prototype, "loopStart", {
|
|
get : function(){
|
|
return this._loopStart;
|
|
},
|
|
set : function(loopStart){
|
|
this._loopStart = loopStart;
|
|
//get the current source
|
|
var event = this._state.get(this.now());
|
|
if (event && event.source){
|
|
event.source.loopStart = this.toSeconds(loopStart);
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* If loop is true, the loop will end at this position.
|
|
* @memberOf Tone.Player#
|
|
* @type {Time}
|
|
* @name loopEnd
|
|
*/
|
|
Object.defineProperty(Tone.Player.prototype, "loopEnd", {
|
|
get : function(){
|
|
return this._loopEnd;
|
|
},
|
|
set : function(loopEnd){
|
|
this._loopEnd = loopEnd;
|
|
//get the current source
|
|
var event = this._state.get(this.now());
|
|
if (event && event.source){
|
|
event.source.loopEnd = this.toSeconds(loopEnd);
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* The audio buffer belonging to the player.
|
|
* @memberOf Tone.Player#
|
|
* @type {Tone.Buffer}
|
|
* @name buffer
|
|
*/
|
|
Object.defineProperty(Tone.Player.prototype, "buffer", {
|
|
get : function(){
|
|
return this._buffer;
|
|
},
|
|
set : function(buffer){
|
|
this._buffer.set(buffer);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* If the buffer should loop once it's over.
|
|
* @memberOf Tone.Player#
|
|
* @type {Boolean}
|
|
* @name loop
|
|
*/
|
|
Object.defineProperty(Tone.Player.prototype, "loop", {
|
|
get : function(){
|
|
return this._loop;
|
|
},
|
|
set : function(loop){
|
|
//if no change, do nothing
|
|
if (this._loop === loop){
|
|
return;
|
|
}
|
|
this._loop = loop;
|
|
var now = this.now();
|
|
//get the current source
|
|
var event = this._state.get(now);
|
|
if (event && event.source){
|
|
event.source.loop = loop;
|
|
if (!loop){
|
|
//stop the playback on the next cycle
|
|
this._stopAtNextIteration(now);
|
|
} else {
|
|
//remove the next stopEvent
|
|
var stopEvent = this._state.getNextState(Tone.State.Stopped, now);
|
|
if (stopEvent){
|
|
event.source.cancelStop();
|
|
this._state.cancel(stopEvent.time);
|
|
this._elapsedTime.cancel(stopEvent.time);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Schedules a stop event at the next full iteration. Used
|
|
* for scheduling stop when the loop state or playbackRate changes
|
|
* @param {Number} now The current time
|
|
* @private
|
|
*/
|
|
Tone.Player.prototype._stopAtNextIteration = function(now){
|
|
if (this._state.getValueAtTime(now) === Tone.State.Started){
|
|
var nextStop = this._state.getNextState(Tone.State.Stopped, now);
|
|
var position = this._elapsedTime.getTicksAtTime(now);
|
|
var iterations = Math.max(Math.ceil(position / this.buffer.duration), 1);
|
|
var stopTime = this._elapsedTime.getTimeOfTick(iterations * this.buffer.duration, nextStop ? nextStop.time - this.sampleTime : Infinity);
|
|
this.stop(stopTime);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The playback speed. 1 is normal speed. This is not a signal because
|
|
* Safari and iOS currently don't support playbackRate as a signal.
|
|
* @memberOf Tone.Player#
|
|
* @type {Number}
|
|
* @name playbackRate
|
|
*/
|
|
Object.defineProperty(Tone.Player.prototype, "playbackRate", {
|
|
get : function(){
|
|
return this._playbackRate;
|
|
},
|
|
set : function(rate){
|
|
this._playbackRate = rate;
|
|
var now = this.now();
|
|
this._elapsedTime.frequency.setValueAtTime(rate, now);
|
|
var lastStop = this._state.getLastState(Tone.State.Stopped, now);
|
|
var intervalStart = lastStop ? lastStop.time : 0;
|
|
//if it's not looping
|
|
if (!this._loop){
|
|
this._stopAtNextIteration(now);
|
|
}
|
|
//set all the sources
|
|
this._state.forEachFrom(intervalStart, function(event){
|
|
if (event.source){
|
|
event.source.playbackRate.setValueAtTime(rate, now);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* The current playback position of the buffer.
|
|
* @memberOf Tone.Player#
|
|
* @type {Number}
|
|
* @name position
|
|
*/
|
|
Object.defineProperty(Tone.Player.prototype, "position", {
|
|
get : function(){
|
|
var now = this.now();
|
|
if (this._state.getValueAtTime(now) === Tone.State.Started && this.loaded){
|
|
var duration = this.buffer.duration;
|
|
var position = this._elapsedTime.getTicksAtTime(now);
|
|
return position % duration;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* The direction the buffer should play in
|
|
* @memberOf Tone.Player#
|
|
* @type {Boolean}
|
|
* @name reverse
|
|
*/
|
|
Object.defineProperty(Tone.Player.prototype, "reverse", {
|
|
get : function(){
|
|
return this._buffer.reverse;
|
|
},
|
|
set : function(rev){
|
|
this._buffer.reverse = rev;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* If all the buffer is loaded
|
|
* @memberOf Tone.Player#
|
|
* @type {Boolean}
|
|
* @name loaded
|
|
* @readOnly
|
|
*/
|
|
Object.defineProperty(Tone.Player.prototype, "loaded", {
|
|
get : function(){
|
|
return this._buffer.loaded;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Dispose and disconnect.
|
|
* @return {Tone.Player} this
|
|
*/
|
|
Tone.Player.prototype.dispose = function(){
|
|
//disconnect all of the players
|
|
this._state.forEach(function(event){
|
|
if (event.source){
|
|
event.source.disconnect();
|
|
event.source = null;
|
|
}
|
|
});
|
|
Tone.Source.prototype.dispose.call(this);
|
|
this._buffer.dispose();
|
|
this._buffer = null;
|
|
this._elapsedTime.dispose();
|
|
this._elapsedTime = null;
|
|
return this;
|
|
};
|
|
|
|
return Tone.Player;
|
|
});
|