Tone.js/Tone/source/GrainPlayer.js
2018-03-05 12:25:33 -05:00

322 lines
7.5 KiB
JavaScript

define(["Tone/core/Tone", "Tone/source/Source", "Tone/core/Buffer", "Tone/source/BufferSource"], function(Tone){
/**
* @class Tone.GrainPlayer implements [granular synthesis](https://en.wikipedia.org/wiki/Granular_synthesis).
* Granular Synthesis enables you to adjust pitch and playback rate independently. The grainSize is the
* amount of time each small chunk of audio is played for and the overlap is the
* amount of crossfading transition time between successive grains.
* @extends {Tone.Source}
* @param {String|Tone.Buffer} url The url to load, or the Tone.Buffer to play.
* @param {Function=} callback The callback to invoke after the url is loaded.
*/
Tone.GrainPlayer = function(){
var options = Tone.defaults(arguments, ["url", "onload"], Tone.GrainPlayer);
Tone.Source.call(this, options);
/**
* The audio buffer belonging to the player.
* @type {Tone.Buffer}
*/
this.buffer = new Tone.Buffer(options.url, options.onload);
/**
* Create a repeating tick to schedule
* the grains.
* @type {Tone.Clock}
* @private
*/
this._clock = new Tone.Clock(this._tick.bind(this), options.grainSize);
/**
* @type {Number}
* @private
*/
this._loopStart = 0;
/**
* @type {Number}
* @private
*/
this._loopEnd = 0;
/**
* All of the currently playing BufferSources
* @type {Array}
* @private
*/
this._activeSources = [];
/**
* @type {Number}
* @private
*/
this._playbackRate = options.playbackRate;
/**
* @type {Number}
* @private
*/
this._grainSize = options.grainSize;
/**
* @private
* @type {Number}
*/
this._overlap = options.overlap;
/**
* Adjust the pitch independently of the playbackRate.
* @type {Cents}
*/
this.detune = options.detune;
//setup
this.overlap = options.overlap;
this.loop = options.loop;
this.playbackRate = options.playbackRate;
this.grainSize = options.grainSize;
this.loopStart = options.loopStart;
this.loopEnd = options.loopEnd;
this.reverse = options.reverse;
this._clock.on("stop", this._onstop.bind(this));
};
Tone.extend(Tone.GrainPlayer, Tone.Source);
/**
* the default parameters
* @static
* @const
* @type {Object}
*/
Tone.GrainPlayer.defaults = {
"onload" : Tone.noOp,
"overlap" : 0.1,
"grainSize" : 0.2,
"playbackRate" : 1,
"detune" : 0,
"loop" : false,
"loopStart" : 0,
"loopEnd" : 0,
"reverse" : false
};
/**
* 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.GrainPlayer} this
* @memberOf Tone.GrainPlayer#
* @method start
* @name start
*/
/**
* Internal start method
* @param {Time} time
* @param {Time} offset
* @private
*/
Tone.GrainPlayer.prototype._start = function(time, offset, duration){
offset = Tone.defaultArg(offset, 0);
offset = this.toSeconds(offset);
time = this.toSeconds(time);
this._offset = offset;
this._clock.start(time);
if (duration){
this.stop(time + this.toSeconds(duration));
}
};
/**
* Internal start method
* @param {Time} time
* @private
*/
Tone.GrainPlayer.prototype._stop = function(time){
this._clock.stop(time);
};
/**
* Invoked when the clock is stopped
* @param {Number} time
* @private
*/
Tone.GrainPlayer.prototype._onstop = function(time){
//stop the players
this._activeSources.forEach(function(source){
source.stop(time, 0);
});
};
/**
* Invoked on each clock tick. scheduled a new
* grain at this time.
* @param {Time} time
* @private
*/
Tone.GrainPlayer.prototype._tick = function(time){
var fadeIn = this._offset < this._overlap ? 0 : this._overlap;
var source = new Tone.BufferSource({
"buffer" : this.buffer,
"fadeIn" : fadeIn,
"fadeOut" : this._overlap,
"loop" : this.loop,
"loopStart" : this._loopStart,
"loopEnd" : this._loopEnd,
"playbackRate" : Tone.intervalToFrequencyRatio(this.detune / 100)
}).connect(this.output);
source.start(time, this._offset);
this._offset += this.grainSize;
source.stop(time + this.grainSize);
//add it to the active sources
this._activeSources.push(source);
//remove it when it's done
source.onended = function(){
var index = this._activeSources.indexOf(source);
if (index !== -1){
this._activeSources.splice(index, 1);
}
}.bind(this);
};
/**
* Jump to a specific time and play it.
* @param {Time} offset The offset to jump to.
* @param {Time=} time When to make the jump.
* @return {Tone.GrainPlayer} this
*/
Tone.GrainPlayer.prototype.seek = function(offset, time){
this._offset = this.toSeconds(offset);
this._tick(this.toSeconds(time));
return this;
};
/**
* The playback rate of the sample
* @memberOf Tone.GrainPlayer#
* @type {Positive}
* @name playbackRate
*/
Object.defineProperty(Tone.GrainPlayer.prototype, "playbackRate", {
get : function(){
return this._playbackRate;
},
set : function(rate){
this._playbackRate = rate;
this.grainSize = this._grainSize;
}
});
/**
* The loop start time.
* @memberOf Tone.GrainPlayer#
* @type {Time}
* @name loopStart
*/
Object.defineProperty(Tone.GrainPlayer.prototype, "loopStart", {
get : function(){
return this._loopStart;
},
set : function(time){
this._loopStart = this.toSeconds(time);
}
});
/**
* The loop end time.
* @memberOf Tone.GrainPlayer#
* @type {Time}
* @name loopEnd
*/
Object.defineProperty(Tone.GrainPlayer.prototype, "loopEnd", {
get : function(){
return this._loopEnd;
},
set : function(time){
this._loopEnd = this.toSeconds(time);
}
});
/**
* The direction the buffer should play in
* @memberOf Tone.GrainPlayer#
* @type {boolean}
* @name reverse
*/
Object.defineProperty(Tone.GrainPlayer.prototype, "reverse", {
get : function(){
return this.buffer.reverse;
},
set : function(rev){
this.buffer.reverse = rev;
}
});
/**
* The size of each chunk of audio that the
* buffer is chopped into and played back at.
* @memberOf Tone.GrainPlayer#
* @type {Time}
* @name grainSize
*/
Object.defineProperty(Tone.GrainPlayer.prototype, "grainSize", {
get : function(){
return this._grainSize;
},
set : function(size){
this._grainSize = this.toSeconds(size);
this._clock.frequency.value = this._playbackRate / this._grainSize;
}
});
/**
* This is the duration of the cross-fade between
* sucessive grains.
* @memberOf Tone.GrainPlayer#
* @type {Time}
* @name overlap
*/
Object.defineProperty(Tone.GrainPlayer.prototype, "overlap", {
get : function(){
return this._overlap;
},
set : function(time){
this._overlap = this.toSeconds(time);
}
});
/**
* Clean up
* @return {Tone.GrainPlayer} this
*/
Tone.GrainPlayer.prototype.dispose = function(){
Tone.Source.prototype.dispose.call(this);
this.buffer.dispose();
this.buffer = null;
this._clock.dispose();
this._clock = null;
this._activeSources.forEach(function(source){
source.dispose();
});
this._activeSources = null;
return this;
};
return Tone.GrainPlayer;
});