Tone.js/Tone/source/Player.js
Yotam Mann a4cc79cfde stopped state doesn't wait for on ended event
the oneneded event is inconsistent when the source is not connected
directly to Master (such as in the case of an LFO).

Fixes #34
2014-10-08 11:59:31 -04:00

290 lines
7.2 KiB
JavaScript

define(["Tone/core/Tone", "Tone/source/Source"], function(Tone){
"use strict";
/**
* @class Audio file player with start, loop, stop.
*
* @constructor
* @extends {Tone.Source}
* @param {string=} url if a url is passed in, it will be loaded
* and invoke the callback if it also passed
* in.
* @param {function(Tone.Player)=} onload callback to be invoked
* once the url is loaded
*/
Tone.Player = function(){
Tone.Source.call(this);
var options = this.optionsObject(arguments, ["url", "onload"], Tone.Player.defaults);
/**
* @private
* @type {AudioBufferSourceNode}
*/
this._source = null;
/**
* the buffer
* @private
* @type {AudioBuffer}
*/
this._buffer = null;
/**
* the duration of the buffer once it's been loaded
* @type {number}
* @readOnly
*/
this.duration = 0;
/**
* if the buffer should loop once it's over
* @type {boolean}
*/
this.loop = options.loop;
/**
* if 'loop' is true, the loop will start at this position
*
* @type {Tone.Time}
*/
this.loopStart = options.loopStart;
/**
* if 'loop' is true, the loop will end at this position
*
* @type {Tone.Time}
*/
this.loopEnd = options.loopEnd;
/**
* the playback rate
* @private
* @type {number}
*/
this._playbackRate = 1;
/**
* enabling retrigger will allow a player to be restarted
* before the the previous 'start' is done playing
*
* @type {boolean}
*/
this.retrigger = options.retrigger;
/**
* set a callback function to invoke when the sample is over
*
* @type {function}
*/
this.onended = options.onended;
//if there is a url, load it.
if (!this.isUndef(options.url)){
this.load(options.url, options.onload);
}
};
Tone.extend(Tone.Player, Tone.Source);
/**
* the default parameters
*
* @static
* @const
* @type {Object}
*/
Tone.Player.defaults = {
"onended" : function(){},
"loop" : false,
"loopStart" : 0,
"loopEnd" : -1,
"retrigger" : false
};
/**
* makes an xhr reqest for the selected url
* Load the audio file as an audio buffer.
* Decodes the audio asynchronously and invokes
* the callback once the audio buffer loads.
*
* @param {string} url the url of the buffer to load.
* filetype support depends on the
* browser.
* @param {function(Tone.Player)=} callback
*/
Tone.Player.prototype.load = function(url, callback){
if (!this._buffer){
var request = new XMLHttpRequest();
request.open("GET", url, true);
request.responseType = "arraybuffer";
// decode asynchronously
var self = this;
request.onload = function() {
self.context.decodeAudioData(request.response, function(buff) {
self.setBuffer(buff);
if (callback){
callback(self);
}
});
};
//send the request
request.send();
} else {
if (callback){
callback(this);
}
}
};
/**
* set the buffer
*
* @param {AudioBuffer} buffer the buffer which the player will play.
* note: if you switch the buffer after
* the player is already started, it will not
* take effect until the next time the player
* is started.
*/
Tone.Player.prototype.setBuffer = function(buffer){
this._buffer = buffer;
this.duration = buffer.duration;
};
/**
* play the buffer between the desired positions
*
* @param {Tone.Time=} startTime
* @param {Tone.Time=} offset
* @param {Tone.Time=} duration
*/
Tone.Player.prototype.start = function(startTime, offset, duration){
if (this.state === Tone.Source.State.STOPPED || this.retrigger){
if (this._buffer){
this.state = Tone.Source.State.STARTED;
//if it's a loop the default offset is the loopstart point
if (this.loop){
offset = this.defaultArg(offset, this.loopStart);
} else {
//otherwise the default offset is 0
offset = this.defaultArg(offset, 0);
}
duration = this.defaultArg(duration, this._buffer.duration - offset);
//make the source
this._source = this.context.createBufferSource();
this._source.buffer = this._buffer;
//set the looping properties
if (this.loop){
this._source.loop = this.loop;
this._source.loopStart = this.toSeconds(this.loopStart);
if (this.loopEnd !== -1){
this._source.loopEnd = this.toSeconds(this.loopEnd);
}
}
//and other properties
this._source.playbackRate.value = this._playbackRate;
this._source.onended = this._onended.bind(this);
this.chain(this._source, this.output);
//start it
this._source.start(this.toSeconds(startTime), this.toSeconds(offset), this.toSeconds(duration));
}
}
};
/**
* Stop playback.
*
* @param {Tone.Time} time
*/
Tone.Player.prototype.stop = function(time){
if (this.state === Tone.Source.State.STARTED) {
if (this._buffer && this._source){
this.state = Tone.Source.State.STOPPED;
this._source.stop(this.toSeconds(time));
}
}
};
/**
* internal call when the buffer is done playing
*
* @private
*/
Tone.Player.prototype._onended = function(){
this.state = Tone.Source.State.STOPPED;
this.onended();
};
/**
* set the rate at which the file plays
*
* @param {number} rate
* @param {Tone.Time=} rampTime (optional) the amount of time it takes to
* reach the rate
*/
Tone.Player.prototype.setPlaybackRate = function(rate, rampTime){
this._playbackRate = rate;
if (this._source) {
if (rampTime){
this._source.playbackRate.exponentialRampToValueAtTime(rate, this.toSeconds(rampTime));
} else {
this._source.playbackRate.value = rampTime;
}
}
};
/**
* set the loop start position
* @param {Tone.Time} loopStart the start time
*/
Tone.Player.prototype.setLoopStart = function(loopStart){
this.loopStart = loopStart;
};
/**
* set the loop end position
* @param {Tone.Time} loopEnd the loop end time
*/
Tone.Player.prototype.setLoopEnd = function(loopEnd){
this.loopEnd = loopEnd;
};
/**
* set the loop start and end
* @param {Tone.Time} loopStart the loop end time
* @param {Tone.Time} loopEnd the loop end time
*/
Tone.Player.prototype.setLoopPoints = function(loopStart, loopEnd){
this.setLoopStart(loopStart);
this.setLoopEnd(loopEnd);
};
/**
* set the parameters at once
* @param {Object} params
*/
Tone.Player.prototype.set = function(params){
if (!this.isUndef(params.playbackRate)) this.setPlaybackRate(params.playbackRate);
if (!this.isUndef(params.onended)) this.onended = params.onended;
if (!this.isUndef(params.loop)) this.loop = params.loop;
if (!this.isUndef(params.loopStart)) this.setLoopStart(params.loopStart);
if (!this.isUndef(params.loopEnd)) this.setLoopEnd(params.loopEnd);
Tone.Source.prototype.set.call(this, params);
};
/**
* dispose and disconnect
*/
Tone.Player.prototype.dispose = function(){
Tone.Source.prototype.dispose.call(this);
if (this._source !== null){
this._source.disconnect();
this._source = null;
}
this._buffer = null;
};
return Tone.Player;
});