Tone.js/Tone/source/buffer/Player.test.ts
Yotam Mann 3196b2d68e
Merge pull request #968 from marcelblum/buffer-player-timekeeping
fix attempt #2 for AudioBufferSourceNode stop time miscalculation, with updated tests
2022-01-15 09:53:08 -05:00

725 lines
20 KiB
TypeScript

import { expect } from "chai";
import { BasicTests } from "test/helper/Basic";
import { CompareToFile } from "test/helper/CompareToFile";
import { atTime, Offline, whenBetween } from "test/helper/Offline";
import { SourceTests } from "test/helper/SourceTests";
import { ToneAudioBuffer } from "Tone/core/context/ToneAudioBuffer";
import { getContext } from "Tone/core/Global";
import { Player } from "./Player";
describe("Player", () => {
const buffer = new ToneAudioBuffer();
beforeEach(() => {
return buffer.load("./audio/sine.wav");
});
// run the common tests
BasicTests(Player, buffer);
SourceTests(Player, buffer);
it("matches a file", () => {
return CompareToFile(
() => {
const player = new Player(buffer).toDestination();
player.start(0.1).stop(0.2);
player.playbackRate = 2;
},
"player.wav",
0.005
);
});
context("Constructor", () => {
it("can be constructed with a Tone.Buffer", () => {
const player = new Player(buffer);
expect(player.buffer.get()).to.equal(buffer.get());
player.dispose();
});
it("can be constructed with an AudioBuffer", () => {
const player = new Player(buffer.get());
expect(player.buffer.get()).to.equal(buffer.get());
player.dispose();
});
it("can be constructed with an unloaded Tone.Buffer", (done) => {
const playerBuffer = new ToneAudioBuffer("./audio/sine.wav");
const player = new Player(playerBuffer, () => {
expect(player.buffer.get()).to.equal(playerBuffer.get());
player.dispose();
done();
});
});
it("can be constructed with no arguments", () => {
const player = new Player();
// set the buffer
player.buffer = buffer;
expect(player.buffer.get()).to.equal(buffer.get());
player.dispose();
});
});
context("onstop", () => {
it("invokes the onstop method when the player is explicitly stopped", () => {
let wasInvoked = false;
return Offline(() => {
const player = new Player({
onstop: () => {
wasInvoked = true;
},
url: buffer,
});
player.start(0).stop(0.1);
}, 0.2).then(() => {
expect(wasInvoked).to.equal(true);
});
});
it("invokes the onstop method when the file is naturally over", () => {
let wasInvoked = false;
return Offline(() => {
const player = new Player(buffer);
player.start(0);
player.onstop = () => {
wasInvoked = true;
expect(player.state).to.equal("stopped");
};
}, buffer.duration * 1.1).then(() => {
expect(wasInvoked).to.equal(true);
});
});
it("invokes the onstop method on restart", () => {
let wasInvoked = 0;
return Offline(() => {
const player = new Player(buffer);
player.start(0).restart(0.1).stop(0.2);
player.onstop = () => {
wasInvoked++;
};
}, 0.3).then(() => {
expect(wasInvoked).to.equal(2);
});
});
});
context("Loading", () => {
it("loads a url which was passed in", (done) => {
const player = new Player("./audio/sine.wav", () => {
expect(player.loaded).to.be.true;
player.dispose();
done();
});
});
it("loads a url using the load method", () => {
const player = new Player();
return player.load("./audio/sine.wav").then(() => {
expect(player.buffer).to.be.instanceof(ToneAudioBuffer);
});
});
it("can be created with an options object", () => {
const player = new Player({
loop: true,
url: "./audio/sine.wav",
});
player.dispose();
});
it("invokes onerror if no url", (done) => {
const source = new Player({
url: "./nosuchfile.wav",
onerror(e) {
expect(e).to.be.instanceOf(Error);
source.dispose();
done();
},
});
});
it("can autostart after loading", (done) => {
const player = new Player({
autostart: true,
onload(): void {
setTimeout(() => {
expect(player.state).to.be.equal("started");
done();
}, 10);
},
url: "./audio/sine.wav",
});
});
});
context("Reverse", () => {
it("can get/set reverse", () => {
const player = new Player();
player.reverse = true;
expect(player.reverse).to.equal(true);
player.dispose();
});
it("can be played in reverse", () => {
const shorterBuffer = buffer.slice(0, buffer.duration / 2);
const audioBuffer = (
shorterBuffer.get() as AudioBuffer
).getChannelData(0);
const lastSample = audioBuffer[audioBuffer.length - 1];
expect(lastSample).to.not.equal(0);
return Offline(() => {
const player = new Player({
reverse: true,
url: shorterBuffer.get(),
}).toDestination();
player.start(0);
}).then((buff) => {
const firstSample = buff.toArray()[0][0];
expect(firstSample).to.equal(lastSample);
});
});
});
context("Looping", () => {
beforeEach(() => {
return buffer.load("./audio/short_sine.wav");
});
it("can be set to loop", () => {
const player = new Player();
player.loop = true;
expect(player.loop).to.be.true;
player.dispose();
});
it("can set the loop points", () => {
const player = new Player();
player.loopStart = 0.4;
expect(player.loopStart).to.equal(0.4);
player.loopEnd = 0.5;
expect(player.loopEnd).to.equal(0.5);
player.setLoopPoints(0, 0.2);
expect(player.loopStart).to.equal(0);
expect(player.loopEnd).to.equal(0.2);
player.dispose();
});
it("loops the audio", () => {
return Offline(() => {
const player = new Player(buffer);
player.loop = true;
player.toDestination();
player.start(0);
}, buffer.duration * 1.5).then((buff) => {
expect(buff.getRmsAtTime(0)).to.be.above(0);
expect(buff.getRmsAtTime(buffer.duration * 0.5)).to.be.above(0);
expect(buff.getRmsAtTime(buffer.duration)).to.be.above(0);
expect(buff.getRmsAtTime(buffer.duration * 1.2)).to.be.above(0);
});
});
it("setting the loop multiple times has no affect", () => {
return Offline(() => {
const player = new Player(buffer);
player.loop = true;
player.loop = true;
player.toDestination();
player.start(0);
}, buffer.duration * 1.5).then((buff) => {
expect(buff.getRmsAtTime(0)).to.be.above(0);
expect(buff.getRmsAtTime(buffer.duration * 0.5)).to.be.above(0);
expect(buff.getRmsAtTime(buffer.duration)).to.be.above(0);
expect(buff.getRmsAtTime(buffer.duration * 1.2)).to.be.above(0);
});
});
it("loops the audio when loop is set after start", () => {
return Offline(() => {
const player = new Player(buffer);
player.toDestination();
player.start(0);
player.loop = true;
}, buffer.duration * 1.5).then((buff) => {
expect(buff.getRmsAtTime(0)).to.be.above(0);
expect(buff.getRmsAtTime(buffer.duration * 0.5)).to.be.above(0);
expect(buff.getRmsAtTime(buffer.duration)).to.be.above(0);
expect(buff.getRmsAtTime(buffer.duration * 1.2)).to.be.above(0);
});
});
it("offset is the loopStart when set to loop", () => {
const testSample =
buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)];
return Offline(() => {
const player = new Player(buffer);
player.loopStart = 0.1;
player.loop = true;
player.toDestination();
player.start(0);
}, 0.05).then((buff) => {
expect(buff.toArray()[0][0]).to.equal(testSample);
});
});
it("loops the audio for the specific duration", () => {
const playDur = buffer.duration * 1.5;
return Offline(() => {
const player = new Player(buffer);
player.loop = true;
player.toDestination();
player.start(0, 0, playDur);
}, buffer.duration * 2).then((buff) => {
for (let time = 0; time < buffer.duration * 2; time += 0.1) {
const val = buff.getRmsAtTime(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", () => {
return Offline(() => {
// make a ramp between 0-1
const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) {
ramp[i] = (i / ramp.length) * 0.3;
}
const buff = ToneAudioBuffer.fromArray(ramp);
const player = new Player(buff).toDestination();
player.loopStart = 0.1;
player.loopEnd = 0.2;
player.loop = true;
player.start(0, 0.35);
}, 0.05).then((buff) => {
buff.forEach((sample, time) => {
if (time < 0.04) {
expect(sample).to.be.within(0.15, 0.2);
} else if (time > 0.05 && time < 0.09) {
expect(sample).to.be.within(0.1, 0.15);
}
});
});
});
});
context("PlaybackRate", () => {
it("reports itself as completed after the stop time when playbackRate = 1", () => {
return Offline(() => {
const player = new Player(buffer);
player.start(0);
return atTime(buffer.duration + 0.1, () => {
expect(player.state).to.equal("stopped");
});
}, buffer.duration * 1.1);
});
it("no longer reports itself as stopped when playback rate is changed to < 1", () => {
return Offline(() => {
const player = new Player(buffer);
player.start(0);
player.playbackRate = 0.5;
return atTime(buffer.duration + 0.1, () => {
expect(player.state).to.equal("started");
});
}, buffer.duration * 1.1);
});
it("when end is explicitly scheduled, it does not matter if playbackRate is changed", () => {
return Offline(() => {
const player = new Player(buffer);
player.start(0).stop(0.1);
player.playbackRate = 0.5;
return atTime(0.11, () => {
expect(player.state).to.equal("stopped");
});
}, buffer.duration);
});
});
context("Get/Set", () => {
it("can be set with an options object", () => {
const player = new Player();
expect(player.loop).to.be.false;
player.set({
fadeIn: 0.1,
fadeOut: 0.2,
loop: true,
loopStart: 0.4,
});
expect(player.loop).to.be.true;
expect(player.loopStart).to.equal(0.4);
expect(player.fadeIn).to.equal(0.1);
expect(player.fadeOut).to.equal(0.2);
player.dispose();
});
it("can set attributes after player is started", () => {
const player = new Player(buffer);
expect(player.loop).to.be.false;
player.start();
player.set({
loop: true,
loopEnd: 0.3,
loopStart: 0.2,
playbackRate: 0.9,
});
expect(player.loop).to.be.true;
expect(player.loopStart).to.equal(0.2);
expect(player.loopEnd).to.equal(0.3);
expect(player.playbackRate).to.equal(0.9);
player.dispose();
});
it("can get an options object", () => {
const player = new Player({
loop: true,
loopEnd: 0.3,
loopStart: 0.2,
reverse: true,
url: "./audio/sine.wav",
});
expect(player.get().loopStart).to.equal(0.2);
expect(player.get().loopEnd).to.equal(0.3);
expect(player.get().loop).to.be.true;
expect(player.get().reverse).to.be.true;
player.dispose();
});
it("can get/set the playbackRate", () => {
const player = new Player();
player.playbackRate = 0.5;
expect(player.playbackRate).to.equal(0.5);
player.dispose();
});
});
context("Start Scheduling", () => {
it("can be start with an offset", () => {
const testSample =
buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)];
return Offline(() => {
const player = new Player(buffer.get());
player.toDestination();
player.start(0, 0.1);
}).then((buff) => {
expect(buff.toArray()[0][0]).to.equal(testSample);
});
});
it("is stopped and restarted when start is called twice", () => {
return Offline(() => {
// make a ramp between 0-1
const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) {
ramp[i] = i / (ramp.length - 1);
}
const buff = new ToneAudioBuffer().fromArray(ramp);
const player = new Player(buff).toDestination();
player.start(0);
player.start(0.1);
}, 0.31).then((buff) => {
expect(buff.max()).to.be.lessThan(1);
});
});
it("only seeks if player is started", () => {
return Offline(() => {
const player = new Player(buffer).toDestination();
player.seek(0.2, 0.01);
}, 0.05).then((buff) => {
expect(buff.isSilent()).to.be.true;
});
});
it("can seek to a position at the given time", () => {
return Offline(() => {
const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) {
ramp[i] = (i / ramp.length) * 0.3;
}
const buff = new ToneAudioBuffer().fromArray(ramp);
const player = new Player(buff).toDestination();
player.start(0);
player.seek(0.2, 0.1);
}, 0.3).then((buff) => {
buff.forEach((sample, time) => {
if (time < 0.09) {
expect(sample).to.be.within(0, 0.1);
} else if (time > 0.1 && time < 0.19) {
expect(sample).to.be.within(0.2, 0.3);
}
});
});
});
it("can be play for a specific duration", () => {
return Offline(() => {
const player = new Player(buffer);
player.toDestination();
player.start(0).stop(0.1);
return (time) => {
whenBetween(time, 0.1, Infinity, () => {
expect(player.state).to.equal("stopped");
});
whenBetween(time, 0, 0.1, () => {
expect(player.state).to.equal("started");
});
};
}, 0.3).then((buff) => {
buff.forEachBetween(
(sample) => {
expect(sample).to.equal(0);
},
0.11,
0.15
);
});
});
it("stops playing if invoked with 'stop' at a sooner time", () => {
return Offline(() => {
const player = new Player(buffer);
player.toDestination();
player.start(0).stop(0.1).stop(0.05);
}, 0.3).then((buff) => {
expect(buff.getTimeOfLastSound()).to.be.closeTo(0.05, 0.02);
});
});
it("stops playing if at the last scheduled 'stop' time", () => {
return Offline(() => {
const player = new Player(buffer);
player.toDestination();
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((buff) => {
expect(buff.getTimeOfLastSound()).to.be.closeTo(0.1, 0.02);
});
});
it("can retrigger multiple sources which all stop at the stop time", () => {
return Offline(() => {
const player = new Player(buffer);
player.toDestination();
player.loop = true;
player.start(0).start(0.1).start(0.2).stop(0.25);
}, 0.4).then((buff) => {
expect(buff.getTimeOfLastSound()).to.be.closeTo(0.25, 0.02);
});
});
it("can be play for a specific duration passed in the 'start' method", () => {
return Offline(() => {
const player = new Player(buffer);
player.toDestination();
player.start(0, 0, 0.1);
return (time) => {
whenBetween(time, 0.1, Infinity, () => {
expect(player.state).to.equal("stopped");
});
whenBetween(time, 0, 0.1, () => {
expect(player.state).to.equal("started");
});
};
}, 0.3).then((buff) => {
expect(buff.getTimeOfLastSound()).to.be.closeTo(0.1, 0.02);
});
});
it("reports itself as stopped after a single iterations of the buffer", () => {
return Offline(() => {
const player = new Player(buffer).toDestination();
player.start();
return (time) => {
whenBetween(time, buffer.duration, Infinity, () => {
expect(player.state).to.equal("stopped");
});
whenBetween(time, 0, buffer.duration, () => {
expect(player.state).to.equal("started");
});
};
}, buffer.duration * 1.1);
});
it("plays synced to the Transport", () => {
return Offline(({ transport }) => {
const player = new Player(buffer)
.sync()
.start(0)
.toDestination();
transport.start(0);
}, 0.05).then((buff) => {
expect(buff.isSilent()).to.be.false;
});
});
it("does not play twice when the offset is very small", () => {
// addresses #999 and #944
return CompareToFile(
() => {
const player = new Player(buffer).toDestination();
player.sync().start(0);
getContext().transport.bpm.value = 125;
getContext().transport.setLoopPoints(0, "1:0:0");
getContext().transport.loop = true;
getContext().transport.start(0);
},
"playerSyncLoop.wav",
0.01
);
});
it("offsets correctly when started by the Transport", () => {
const testSample =
buffer.toArray(0)[
Math.floor(0.13125 * getContext().sampleRate)
];
return Offline(({ transport }) => {
const player = new Player(buffer)
.sync()
.start(0, 0.1)
.toDestination();
transport.start(0, 0.03125);
}, 0.05).then((buff) => {
expect(buff.toArray()[0][0]).to.equal(testSample);
});
});
it("starts at the correct position when Transport is offset and playbackRate is not 1", () => {
return Offline(({ transport }) => {
// make a ramp between 0-1
const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) {
ramp[i] = i / ramp.length;
}
const buff = ToneAudioBuffer.fromArray(ramp);
const player = new Player(buff).toDestination();
player.playbackRate = 0.5;
player.sync().start(0);
// start halfway through
transport.start(0, 0.15);
}, 0.05).then((buff) => {
expect(buff.getValueAtTime(0)).to.be.closeTo(0.5, 0.05);
});
});
it("starts with an offset when synced and started after Transport is running", () => {
return Offline(({ transport }) => {
const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) {
ramp[i] = (i / ramp.length) * 0.3;
}
const buff = new ToneAudioBuffer().fromArray(ramp);
const player = new Player(buff).toDestination();
transport.start(0);
return atTime(0.1, () => {
player.sync().start(0);
});
}, 0.3).then((buff) => {
expect(buff.getValueAtTime(0)).to.equal(0);
expect(buff.getValueAtTime(0.05)).to.equal(0);
expect(buff.getValueAtTime(0.11)).to.be.closeTo(0.11, 0.01);
expect(buff.getValueAtTime(0.2)).to.be.closeTo(0.2, 0.01);
});
});
it("can pass in an offset when synced and started after Transport is running", () => {
return Offline(({ transport }) => {
const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) {
ramp[i] = (i / ramp.length) * 0.3;
}
const buff = new ToneAudioBuffer().fromArray(ramp);
const player = new Player(buff).toDestination();
player.loop = true;
transport.start(0);
return atTime(0.1, () => {
player.sync().start(0, 0.1);
});
}, 0.3).then((buff) => {
expect(buff.getValueAtTime(0)).to.equal(0);
expect(buff.getValueAtTime(0.05)).to.equal(0);
expect(buff.getValueAtTime(0.11)).to.be.closeTo(0.21, 0.01);
expect(buff.getValueAtTime(0.15)).to.be.closeTo(0.25, 0.01);
expect(buff.getValueAtTime(0.2)).to.be.closeTo(0.0, 0.01);
expect(buff.getValueAtTime(0.25)).to.be.closeTo(0.05, 0.01);
});
});
it("fades in and out correctly", () => {
let duration = 0.5;
return Offline(() => {
const onesArray = new Float32Array(
getContext().sampleRate * duration
);
onesArray.forEach((sample, index) => {
onesArray[index] = 1;
});
const onesBuffer = ToneAudioBuffer.fromArray(onesArray);
const player = new Player({
url: onesBuffer,
fadeOut: 0.1,
fadeIn: 0.1,
}).toDestination();
player.start(0);
}, 0.6).then((buff) => {
expect(buff.getRmsAtTime(0)).to.be.closeTo(0, 0.1);
expect(buff.getRmsAtTime(0.05)).to.be.closeTo(0.5, 0.1);
expect(buff.getRmsAtTime(0.1)).to.be.closeTo(1, 0.1);
duration -= 0.1;
expect(buff.getRmsAtTime(duration)).to.be.closeTo(1, 0.1);
expect(buff.getRmsAtTime(duration + 0.05)).to.be.closeTo(
0.5,
0.1
);
expect(buff.getRmsAtTime(duration + 0.1)).to.be.closeTo(0, 0.1);
});
});
it("stops only last activeSource when restarting at intervals < latencyHint", (done) => {
const originalLookAhead = getContext().lookAhead;
getContext().lookAhead = .3;
const player = new Player({
onload(): void {
player.start(undefined, undefined, 1);
setTimeout(() => player.restart(undefined, undefined, 1), 50);
setTimeout(() => player.restart(undefined, undefined, 1), 100);
setTimeout(() => player.restart(undefined, undefined, 1), 150);
setTimeout(() => {
player.restart(undefined, undefined, 1);
const checkStopTimes = new Set();
player["_activeSources"].forEach(source => {
checkStopTimes.add(source["_stopTime"]);
});
getContext().lookAhead = originalLookAhead;
// ensure each source has a different stopTime
expect(checkStopTimes.size).to.equal(player["_activeSources"].size);
done();
}, 250);
},
url: "./audio/sine.wav",
});
});
});
});