mirror of
https://github.com/Tonejs/Tone.js
synced 2024-11-16 00:27:58 +00:00
get the playback position of the playing buffer
integrates TickSource to track position even with playbackRate changes Fixes #292 and Fixes #307
This commit is contained in:
parent
e5a6788de1
commit
4dd5c0a464
2 changed files with 243 additions and 21 deletions
|
@ -1,4 +1,5 @@
|
|||
define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source/BufferSource"], function(Tone){
|
||||
define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source/TickSource",
|
||||
"Tone/source/BufferSource"], function(Tone){
|
||||
|
||||
"use strict";
|
||||
|
||||
|
@ -9,7 +10,7 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
* @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.
|
||||
* @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();
|
||||
|
@ -30,7 +31,7 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
/**
|
||||
* If the file should play as soon
|
||||
* as the buffer is loaded.
|
||||
* @type {boolean}
|
||||
* @type {Boolean}
|
||||
* @example
|
||||
* //will play as soon as it's loaded
|
||||
* var player = new Tone.Player({
|
||||
|
@ -56,7 +57,7 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
|
||||
/**
|
||||
* if the buffer should loop once it's over
|
||||
* @type {boolean}
|
||||
* @type {Boolean}
|
||||
* @private
|
||||
*/
|
||||
this._loop = options.loop;
|
||||
|
@ -78,16 +79,23 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
/**
|
||||
* the playback rate
|
||||
* @private
|
||||
* @type {number}
|
||||
* @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}
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.retrigger = options.retrigger;
|
||||
|
||||
|
@ -135,7 +143,7 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
* @param {string} url The url of the buffer to load.
|
||||
* Filetype support depends on the
|
||||
* browser.
|
||||
* @param {function=} callback The function to invoke once
|
||||
* @param {Function=} callback The function to invoke once
|
||||
* the sample is loaded.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
@ -187,10 +195,13 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
|
||||
//compute the values in seconds
|
||||
offset = this.toSeconds(offset);
|
||||
duration = Tone.defaultArg(duration, Math.max(this._buffer.duration - offset, 0));
|
||||
duration = this.toSeconds(duration);
|
||||
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,
|
||||
|
@ -205,17 +216,17 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
//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 + duration);
|
||||
this._state.setStateAtTime(Tone.State.Stopped, startTime + computedDuration / this._playbackRate);
|
||||
}
|
||||
|
||||
var event = this._state.get(startTime);
|
||||
event.source = source;
|
||||
|
||||
//start it
|
||||
if (this._loop){
|
||||
if (this._loop && Tone.isUndef(duration)){
|
||||
source.start(startTime, offset);
|
||||
} else {
|
||||
source.start(startTime, offset, duration);
|
||||
source.start(startTime, offset, computedDuration);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
@ -228,6 +239,7 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
*/
|
||||
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);
|
||||
|
@ -352,7 +364,7 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
/**
|
||||
* If the buffer should loop once it's over.
|
||||
* @memberOf Tone.Player#
|
||||
* @type {boolean}
|
||||
* @type {Boolean}
|
||||
* @name loop
|
||||
*/
|
||||
Object.defineProperty(Tone.Player.prototype, "loop", {
|
||||
|
@ -360,20 +372,53 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
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(this.now());
|
||||
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}
|
||||
* @type {Number}
|
||||
* @name playbackRate
|
||||
*/
|
||||
Object.defineProperty(Tone.Player.prototype, "playbackRate", {
|
||||
|
@ -382,19 +427,46 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
},
|
||||
set : function(rate){
|
||||
this._playbackRate = rate;
|
||||
//set all future sources
|
||||
this._state.forEachFrom(this.now(), function(event){
|
||||
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.value = rate;
|
||||
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}
|
||||
* @type {Boolean}
|
||||
* @name reverse
|
||||
*/
|
||||
Object.defineProperty(Tone.Player.prototype, "reverse", {
|
||||
|
@ -434,6 +506,8 @@ define(["Tone/core/Tone", "Tone/core/Buffer", "Tone/source/Source", "Tone/source
|
|||
Tone.Source.prototype.dispose.call(this);
|
||||
this._buffer.dispose();
|
||||
this._buffer = null;
|
||||
this._elapsedTime.dispose();
|
||||
this._elapsedTime = null;
|
||||
return this;
|
||||
};
|
||||
|
||||
|
|
|
@ -102,6 +102,123 @@ function(BasicTests, Player, Offline, SourceTests, Buffer, Meter, Test, Tone, Co
|
|||
|
||||
});
|
||||
|
||||
context("Position/State", function(){
|
||||
|
||||
it("gets the current position of the playback", function(){
|
||||
return Offline(function(){
|
||||
var player = new Player(buffer).toMaster();
|
||||
player.start(0);
|
||||
return function(time){
|
||||
expect(player.position).to.be.closeTo(time, 0.01);
|
||||
expect(player.state).to.equal("started");
|
||||
};
|
||||
}, buffer.duration);
|
||||
});
|
||||
|
||||
it("gets the current position of the playback when rate is < 1", function(){
|
||||
return Offline(function(){
|
||||
var player = new Player(buffer).toMaster();
|
||||
player.start(0);
|
||||
player.playbackRate = 0.5;
|
||||
return function(time){
|
||||
expect(player.position).to.be.closeTo(time*0.5, 0.01);
|
||||
expect(player.state).to.equal("started");
|
||||
};
|
||||
}, buffer.duration);
|
||||
});
|
||||
|
||||
it("gets the current position of the playback when rate is > 1", function(){
|
||||
return Offline(function(){
|
||||
var player = new Player(buffer).toMaster();
|
||||
player.playbackRate = 2;
|
||||
player.start(0);
|
||||
return function(time){
|
||||
if (time < buffer.duration / 2){
|
||||
expect(player.position).to.be.closeTo(time*2, 0.01);
|
||||
expect(player.state).to.equal("started");
|
||||
} else {
|
||||
expect(player.state).to.equal("stopped");
|
||||
}
|
||||
};
|
||||
}, buffer.duration);
|
||||
});
|
||||
|
||||
it("position is 0 after the buffer has completed", function(){
|
||||
return Offline(function(){
|
||||
var player = new Player(buffer).toMaster();
|
||||
player.start(0);
|
||||
return function(time){
|
||||
if (time < buffer.duration){
|
||||
expect(player.position).to.be.closeTo(time, 0.01);
|
||||
expect(player.state).to.equal("started");
|
||||
} else if (time > buffer.duration){
|
||||
expect(player.position).to.equal(0);
|
||||
expect(player.state).to.equal("stopped");
|
||||
}
|
||||
};
|
||||
}, buffer.duration + 0.1);
|
||||
});
|
||||
|
||||
it("returns correct position while looping", function(){
|
||||
return Offline(function(){
|
||||
var player = new Player(buffer).toMaster();
|
||||
player.start(0);
|
||||
player.loop = true;
|
||||
return function(time){
|
||||
expect(player.state).to.equal("started");
|
||||
Test.whenBetween(time, 0, buffer.duration, function(){
|
||||
expect(player.position).to.be.closeTo(time, 0.01);
|
||||
});
|
||||
Test.whenBetween(time, buffer.duration, buffer.duration*2, function(){
|
||||
expect(player.position).to.be.closeTo(time - buffer.duration, 0.01);
|
||||
});
|
||||
|
||||
};
|
||||
}, buffer.duration * 2);
|
||||
});
|
||||
|
||||
it("can stop looping after second loop", function(){
|
||||
return Offline(function(){
|
||||
var player = new Player(buffer).toMaster();
|
||||
player.start(0);
|
||||
player.loop = true;
|
||||
return function(time){
|
||||
Test.whenBetween(time, 0, buffer.duration, function(){
|
||||
expect(player.state).to.equal("started");
|
||||
expect(player.position).to.be.closeTo(time, 0.01);
|
||||
});
|
||||
Test.whenBetween(time, buffer.duration, buffer.duration*2, function(){
|
||||
//set to stop looping
|
||||
player.loop = false;
|
||||
expect(player.position).to.be.closeTo(time - buffer.duration, 0.01);
|
||||
expect(player.state).to.equal("started");
|
||||
});
|
||||
Test.whenBetween(time, buffer.duration*2, Infinity, function(){
|
||||
expect(player.position).to.equal(0);
|
||||
expect(player.state).to.equal("stopped");
|
||||
});
|
||||
|
||||
};
|
||||
}, buffer.duration * 3);
|
||||
});
|
||||
|
||||
it("can change playbackRate during playback", function(){
|
||||
return Meter(function(){
|
||||
var player = new Player(buffer).toMaster();
|
||||
player.start(0);
|
||||
return Test.atTime(buffer.duration * 0.5, function(){
|
||||
player.playbackRate = 2;
|
||||
});
|
||||
}, buffer.duration).then(function(rms){
|
||||
expect(rms.getValueAtTime(buffer.duration * 0)).to.be.above(0);
|
||||
expect(rms.getValueAtTime(buffer.duration * 0.5)).to.be.above(0);
|
||||
expect(rms.getValueAtTime(buffer.duration * 0.75)).to.be.above(0);
|
||||
expect(rms.getValueAtTime(buffer.duration * 0.8)).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
context("Reverse", function(){
|
||||
|
||||
it("can be played in reverse", function(){
|
||||
|
@ -159,6 +276,19 @@ function(BasicTests, Player, Offline, SourceTests, Buffer, Meter, Test, Tone, Co
|
|||
});
|
||||
});
|
||||
|
||||
it("loops the audio when loop is set after start", function(){
|
||||
return Meter(function(){
|
||||
var player = new Player(buffer);
|
||||
player.toMaster();
|
||||
player.start(0);
|
||||
player.loop = true;
|
||||
}, buffer.duration * 1.5).then(function(rms){
|
||||
rms.forEach(function(level){
|
||||
expect(level).to.be.above(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("offset is the loopStart when set to loop", function(){
|
||||
var testSample = buffer.toArray()[Math.floor(0.1 * buffer.context.sampleRate)];
|
||||
return Offline(function(){
|
||||
|
@ -172,6 +302,24 @@ function(BasicTests, Player, Offline, SourceTests, Buffer, Meter, Test, Tone, Co
|
|||
});
|
||||
});
|
||||
|
||||
it("loops the audio for the specific duration", function(){
|
||||
var playDur = buffer.duration * 1.5;
|
||||
return Meter(function(){
|
||||
var player = new Player(buffer);
|
||||
player.loop = true;
|
||||
player.toMaster();
|
||||
player.start(0, 0, playDur);
|
||||
}, buffer.duration * 2).then(function(buff){
|
||||
buff.forEach(function(val, time){
|
||||
if (time < (playDur - 0.01)){
|
||||
expect(val).to.be.greaterThan(0);
|
||||
} else if (time > playDur){
|
||||
expect(val).to.equal(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("correctly compensates if the offset is greater than the loopEnd", function(){
|
||||
return Offline(function(){
|
||||
//make a ramp between 0-1
|
||||
|
@ -364,14 +512,14 @@ function(BasicTests, Player, Offline, SourceTests, Buffer, Meter, Test, Tone, Co
|
|||
});
|
||||
});
|
||||
|
||||
it("stops playing if multiple start/stops with 'stop' at a sooner time", function(){
|
||||
it("stops playing if at the last scheduled 'stop' time", function(){
|
||||
return Offline(function(){
|
||||
var player = new Player(buffer);
|
||||
player.toMaster();
|
||||
player.start(0, 0, 0.05).start(0.1, 0, 0.05).start(0.2, 0, 0.05);
|
||||
player.stop(0.1);
|
||||
}, 0.3).then(function(buffer){
|
||||
expect(buffer.getLastSoundTime()).to.be.closeTo(0.05, 0.02);
|
||||
expect(buffer.getLastSoundTime()).to.be.closeTo(0.1, 0.02);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue