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:
Yotam Mann 2018-03-01 14:15:37 -05:00
parent e5a6788de1
commit 4dd5c0a464
2 changed files with 243 additions and 21 deletions

View file

@ -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;
};

View file

@ -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);
});
});