Don't reschedule source when offset is very small

This offset is due to floating point error

Fixes #999
Fixes #944
This commit is contained in:
Yotam Mann 2021-12-19 11:21:27 -05:00
parent aeaaa1e871
commit 444d6179c4
3 changed files with 182 additions and 76 deletions

View file

@ -2,11 +2,19 @@ import { Volume } from "../component/channel/Volume";
import "../core/context/Destination"; import "../core/context/Destination";
import "../core/clock/Transport"; import "../core/clock/Transport";
import { Param } from "../core/context/Param"; import { Param } from "../core/context/Param";
import { OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../core/context/ToneAudioNode"; import {
OutputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../core/context/ToneAudioNode";
import { Decibels, Seconds, Time } from "../core/type/Units"; import { Decibels, Seconds, Time } from "../core/type/Units";
import { defaultArg } from "../core/util/Defaults"; import { defaultArg } from "../core/util/Defaults";
import { noOp, readOnly } from "../core/util/Interface"; import { noOp, readOnly } from "../core/util/Interface";
import { BasicPlaybackState, StateTimeline, StateTimelineEvent } from "../core/util/StateTimeline"; import {
BasicPlaybackState,
StateTimeline,
StateTimelineEvent,
} from "../core/util/StateTimeline";
import { isDefined, isUndef } from "../core/util/TypeCheck"; import { isDefined, isUndef } from "../core/util/TypeCheck";
import { assert, assertContextRunning } from "../core/util/Debug"; import { assert, assertContextRunning } from "../core/util/Debug";
import { GT } from "../core/util/Math"; import { GT } from "../core/util/Math";
@ -20,9 +28,9 @@ export interface SourceOptions extends ToneAudioNodeOptions {
} }
/** /**
* Base class for sources. * Base class for sources.
* start/stop of this.context.transport. * start/stop of this.context.transport.
* *
* ``` * ```
* // Multiple state change events can be chained together, * // Multiple state change events can be chained together,
* // but must be set in the correct order and with ascending times * // but must be set in the correct order and with ascending times
@ -36,8 +44,9 @@ export interface SourceOptions extends ToneAudioNodeOptions {
* state.start("+0.3").stop("+0.2"); * state.start("+0.3").stop("+0.2");
* ``` * ```
*/ */
export abstract class Source<Options extends SourceOptions> extends ToneAudioNode<Options> { export abstract class Source<
Options extends SourceOptions
> extends ToneAudioNode<Options> {
/** /**
* The output volume node * The output volume node
*/ */
@ -129,7 +138,9 @@ export abstract class Source<Options extends SourceOptions> extends ToneAudioNod
get state(): BasicPlaybackState { get state(): BasicPlaybackState {
if (this._synced) { if (this._synced) {
if (this.context.transport.state === "started") { if (this.context.transport.state === "started") {
return this._state.getValueAtTime(this.context.transport.seconds) as BasicPlaybackState; return this._state.getValueAtTime(
this.context.transport.seconds
) as BasicPlaybackState;
} else { } else {
return "stopped"; return "stopped";
} }
@ -155,7 +166,11 @@ export abstract class Source<Options extends SourceOptions> extends ToneAudioNod
// overwrite these functions // overwrite these functions
protected abstract _start(time: Time, offset?: Time, duration?: Time): void; protected abstract _start(time: Time, offset?: Time, duration?: Time): void;
protected abstract _stop(time: Time): void; protected abstract _stop(time: Time): void;
protected abstract _restart(time: Seconds, offset?: Time, duration?: Time): void; protected abstract _restart(
time: Seconds,
offset?: Time,
duration?: Time
): void;
/** /**
* Ensure that the scheduled time is not before the current time. * Ensure that the scheduled time is not before the current time.
@ -178,12 +193,24 @@ export abstract class Source<Options extends SourceOptions> extends ToneAudioNod
* source.start("+0.5"); // starts the source 0.5 seconds from now * source.start("+0.5"); // starts the source 0.5 seconds from now
*/ */
start(time?: Time, offset?: Time, duration?: Time): this { start(time?: Time, offset?: Time, duration?: Time): this {
let computedTime = isUndef(time) && this._synced ? this.context.transport.seconds : this.toSeconds(time); let computedTime =
isUndef(time) && this._synced
? this.context.transport.seconds
: this.toSeconds(time);
computedTime = this._clampToCurrentTime(computedTime); computedTime = this._clampToCurrentTime(computedTime);
// if it's started, stop it and restart it // if it's started, stop it and restart it
if (!this._synced && this._state.getValueAtTime(computedTime) === "started") { if (
!this._synced &&
this._state.getValueAtTime(computedTime) === "started"
) {
// time should be strictly greater than the previous start time // time should be strictly greater than the previous start time
assert(GT(computedTime, (this._state.get(computedTime) as StateTimelineEvent).time), "Start time must be strictly greater than previous start time"); assert(
GT(
computedTime,
(this._state.get(computedTime) as StateTimelineEvent).time
),
"Start time must be strictly greater than previous start time"
);
this._state.cancel(computedTime); this._state.cancel(computedTime);
this._state.setStateAtTime("started", computedTime); this._state.setStateAtTime("started", computedTime);
this.log("restart", computedTime); this.log("restart", computedTime);
@ -196,18 +223,26 @@ export abstract class Source<Options extends SourceOptions> extends ToneAudioNod
const event = this._state.get(computedTime); const event = this._state.get(computedTime);
if (event) { if (event) {
event.offset = this.toSeconds(defaultArg(offset, 0)); event.offset = this.toSeconds(defaultArg(offset, 0));
event.duration = duration ? this.toSeconds(duration) : undefined; event.duration = duration
? this.toSeconds(duration)
: undefined;
} }
const sched = this.context.transport.schedule(t => { const sched = this.context.transport.schedule((t) => {
this._start(t, offset, duration); this._start(t, offset, duration);
}, computedTime); }, computedTime);
this._scheduled.push(sched); this._scheduled.push(sched);
// if the transport is already started // if the transport is already started
// and the time is greater than where the transport is // and the time is greater than where the transport is
if (this.context.transport.state === "started" && if (
this.context.transport.getSecondsAtTime(this.immediate()) > computedTime) { this.context.transport.state === "started" &&
this._syncedStart(this.now(), this.context.transport.seconds); this.context.transport.getSecondsAtTime(this.immediate()) >
computedTime
) {
this._syncedStart(
this.now(),
this.context.transport.seconds
);
} }
} else { } else {
assertContextRunning(this.context); assertContextRunning(this.context);
@ -227,14 +262,23 @@ export abstract class Source<Options extends SourceOptions> extends ToneAudioNod
* source.stop("+0.5"); // stops the source 0.5 seconds from now * source.stop("+0.5"); // stops the source 0.5 seconds from now
*/ */
stop(time?: Time): this { stop(time?: Time): this {
let computedTime = isUndef(time) && this._synced ? this.context.transport.seconds : this.toSeconds(time); let computedTime =
isUndef(time) && this._synced
? this.context.transport.seconds
: this.toSeconds(time);
computedTime = this._clampToCurrentTime(computedTime); computedTime = this._clampToCurrentTime(computedTime);
if (this._state.getValueAtTime(computedTime) === "started" || isDefined(this._state.getNextState("started", computedTime))) { if (
this._state.getValueAtTime(computedTime) === "started" ||
isDefined(this._state.getNextState("started", computedTime))
) {
this.log("stop", computedTime); this.log("stop", computedTime);
if (!this._synced) { if (!this._synced) {
this._stop(computedTime); this._stop(computedTime);
} else { } else {
const sched = this.context.transport.schedule(this._stop.bind(this), computedTime); const sched = this.context.transport.schedule(
this._stop.bind(this),
computedTime
);
this._scheduled.push(sched); this._scheduled.push(sched);
} }
this._state.cancel(computedTime); this._state.cancel(computedTime);
@ -274,23 +318,36 @@ export abstract class Source<Options extends SourceOptions> extends ToneAudioNod
if (!this._synced) { if (!this._synced) {
this._synced = true; this._synced = true;
this._syncedStart = (time, offset) => { this._syncedStart = (time, offset) => {
if (offset > 0) { if (GT(offset, 0)) {
// get the playback state at that time // get the playback state at that time
const stateEvent = this._state.get(offset); const stateEvent = this._state.get(offset);
// listen for start events which may occur in the middle of the sync'ed time // listen for start events which may occur in the middle of the sync'ed time
if (stateEvent && stateEvent.state === "started" && stateEvent.time !== offset) { if (
stateEvent &&
stateEvent.state === "started" &&
stateEvent.time !== offset
) {
// get the offset // get the offset
const startOffset = offset - this.toSeconds(stateEvent.time); const startOffset =
offset - this.toSeconds(stateEvent.time);
let duration: number | undefined; let duration: number | undefined;
if (stateEvent.duration) { if (stateEvent.duration) {
duration = this.toSeconds(stateEvent.duration) - startOffset; duration =
this.toSeconds(stateEvent.duration) -
startOffset;
} }
this._start(time, this.toSeconds(stateEvent.offset) + startOffset, duration); this._start(
time,
this.toSeconds(stateEvent.offset) + startOffset,
duration
);
} }
} }
}; };
this._syncedStop = time => { this._syncedStop = (time) => {
const seconds = this.context.transport.getSecondsAtTime(Math.max(time - this.sampleTime, 0)); const seconds = this.context.transport.getSecondsAtTime(
Math.max(time - this.sampleTime, 0)
);
if (this._state.getValueAtTime(seconds) === "started") { if (this._state.getValueAtTime(seconds) === "started") {
this._stop(time); this._stop(time);
} }
@ -317,7 +374,7 @@ export abstract class Source<Options extends SourceOptions> extends ToneAudioNod
} }
this._synced = false; this._synced = false;
// clear all of the scheduled ids // clear all of the scheduled ids
this._scheduled.forEach(id => this.context.transport.clear(id)); this._scheduled.forEach((id) => this.context.transport.clear(id));
this._scheduled = []; this._scheduled = [];
this._state.cancel(0); this._state.cancel(0);
// stop it also // stop it also

View file

@ -8,7 +8,6 @@ import { getContext } from "Tone/core/Global";
import { Player } from "./Player"; import { Player } from "./Player";
describe("Player", () => { describe("Player", () => {
const buffer = new ToneAudioBuffer(); const buffer = new ToneAudioBuffer();
beforeEach(() => { beforeEach(() => {
@ -20,15 +19,18 @@ describe("Player", () => {
SourceTests(Player, buffer); SourceTests(Player, buffer);
it("matches a file", () => { it("matches a file", () => {
return CompareToFile(() => { return CompareToFile(
const player = new Player(buffer).toDestination(); () => {
player.start(0.1).stop(0.2); const player = new Player(buffer).toDestination();
player.playbackRate = 2; player.start(0.1).stop(0.2);
}, "player.wav", 0.005); player.playbackRate = 2;
},
"player.wav",
0.005
);
}); });
context("Constructor", () => { context("Constructor", () => {
it("can be constructed with a Tone.Buffer", () => { it("can be constructed with a Tone.Buffer", () => {
const player = new Player(buffer); const player = new Player(buffer);
expect(player.buffer.get()).to.equal(buffer.get()); expect(player.buffer.get()).to.equal(buffer.get());
@ -60,7 +62,6 @@ describe("Player", () => {
}); });
context("onstop", () => { context("onstop", () => {
it("invokes the onstop method when the player is explicitly stopped", () => { it("invokes the onstop method when the player is explicitly stopped", () => {
let wasInvoked = false; let wasInvoked = false;
return Offline(() => { return Offline(() => {
@ -105,7 +106,6 @@ describe("Player", () => {
}); });
context("Loading", () => { context("Loading", () => {
it("loads a url which was passed in", (done) => { it("loads a url which was passed in", (done) => {
const player = new Player("./audio/sine.wav", () => { const player = new Player("./audio/sine.wav", () => {
expect(player.loaded).to.be.true; expect(player.loaded).to.be.true;
@ -131,12 +131,12 @@ describe("Player", () => {
it("invokes onerror if no url", (done) => { it("invokes onerror if no url", (done) => {
const source = new Player({ const source = new Player({
url: "./nosuchfile.wav", url: "./nosuchfile.wav",
onerror(e) { onerror(e) {
expect(e).to.be.instanceOf(Error); expect(e).to.be.instanceOf(Error);
source.dispose(); source.dispose();
done(); done();
} },
}); });
}); });
@ -152,11 +152,9 @@ describe("Player", () => {
url: "./audio/sine.wav", url: "./audio/sine.wav",
}); });
}); });
}); });
context("Reverse", () => { context("Reverse", () => {
it("can get/set reverse", () => { it("can get/set reverse", () => {
const player = new Player(); const player = new Player();
player.reverse = true; player.reverse = true;
@ -166,7 +164,9 @@ describe("Player", () => {
it("can be played in reverse", () => { it("can be played in reverse", () => {
const shorterBuffer = buffer.slice(0, buffer.duration / 2); const shorterBuffer = buffer.slice(0, buffer.duration / 2);
const audioBuffer = (shorterBuffer.get() as AudioBuffer).getChannelData(0); const audioBuffer = (
shorterBuffer.get() as AudioBuffer
).getChannelData(0);
const lastSample = audioBuffer[audioBuffer.length - 1]; const lastSample = audioBuffer[audioBuffer.length - 1];
expect(lastSample).to.not.equal(0); expect(lastSample).to.not.equal(0);
return Offline(() => { return Offline(() => {
@ -180,11 +180,9 @@ describe("Player", () => {
expect(firstSample).to.equal(lastSample); expect(firstSample).to.equal(lastSample);
}); });
}); });
}); });
context("Looping", () => { context("Looping", () => {
beforeEach(() => { beforeEach(() => {
return buffer.load("./audio/short_sine.wav"); return buffer.load("./audio/short_sine.wav");
}); });
@ -243,7 +241,7 @@ describe("Player", () => {
player.toDestination(); player.toDestination();
player.start(0); player.start(0);
player.loop = true; player.loop = true;
}, buffer.duration * 1.5).then(buff => { }, buffer.duration * 1.5).then((buff) => {
expect(buff.getRmsAtTime(0)).to.be.above(0); expect(buff.getRmsAtTime(0)).to.be.above(0);
expect(buff.getRmsAtTime(buffer.duration * 0.5)).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)).to.be.above(0);
@ -252,7 +250,8 @@ describe("Player", () => {
}); });
it("offset is the loopStart when set to loop", () => { it("offset is the loopStart when set to loop", () => {
const testSample = buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)]; const testSample =
buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)];
return Offline(() => { return Offline(() => {
const player = new Player(buffer); const player = new Player(buffer);
player.loopStart = 0.1; player.loopStart = 0.1;
@ -271,10 +270,10 @@ describe("Player", () => {
player.loop = true; player.loop = true;
player.toDestination(); player.toDestination();
player.start(0, 0, playDur); player.start(0, 0, playDur);
}, buffer.duration * 2).then(buff => { }, buffer.duration * 2).then((buff) => {
for (let time = 0; time < buffer.duration * 2; time += 0.1) { for (let time = 0; time < buffer.duration * 2; time += 0.1) {
const val = buff.getRmsAtTime(time); const val = buff.getRmsAtTime(time);
if (time < (playDur - 0.01)) { if (time < playDur - 0.01) {
expect(val).to.be.greaterThan(0); expect(val).to.be.greaterThan(0);
} else if (time > playDur) { } else if (time > playDur) {
expect(val).to.equal(0); expect(val).to.equal(0);
@ -286,9 +285,11 @@ describe("Player", () => {
it("correctly compensates if the offset is greater than the loopEnd", () => { it("correctly compensates if the offset is greater than the loopEnd", () => {
return Offline(() => { return Offline(() => {
// make a ramp between 0-1 // make a ramp between 0-1
const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) { for (let i = 0; i < ramp.length; i++) {
ramp[i] = (i / (ramp.length)) * 0.3; ramp[i] = (i / ramp.length) * 0.3;
} }
const buff = ToneAudioBuffer.fromArray(ramp); const buff = ToneAudioBuffer.fromArray(ramp);
const player = new Player(buff).toDestination(); const player = new Player(buff).toDestination();
@ -306,7 +307,6 @@ describe("Player", () => {
}); });
}); });
}); });
}); });
context("PlaybackRate", () => { context("PlaybackRate", () => {
@ -344,7 +344,6 @@ describe("Player", () => {
}); });
context("Get/Set", () => { context("Get/Set", () => {
it("can be set with an options object", () => { it("can be set with an options object", () => {
const player = new Player(); const player = new Player();
expect(player.loop).to.be.false; expect(player.loop).to.be.false;
@ -399,13 +398,12 @@ describe("Player", () => {
expect(player.playbackRate).to.equal(0.5); expect(player.playbackRate).to.equal(0.5);
player.dispose(); player.dispose();
}); });
}); });
context("Start Scheduling", () => { context("Start Scheduling", () => {
it("can be start with an offset", () => { it("can be start with an offset", () => {
const testSample = buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)]; const testSample =
buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)];
return Offline(() => { return Offline(() => {
const player = new Player(buffer.get()); const player = new Player(buffer.get());
player.toDestination(); player.toDestination();
@ -418,9 +416,11 @@ describe("Player", () => {
it("is stopped and restarted when start is called twice", () => { it("is stopped and restarted when start is called twice", () => {
return Offline(() => { return Offline(() => {
// make a ramp between 0-1 // make a ramp between 0-1
const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) { for (let i = 0; i < ramp.length; i++) {
ramp[i] = (i / (ramp.length - 1)); ramp[i] = i / (ramp.length - 1);
} }
const buff = new ToneAudioBuffer().fromArray(ramp); const buff = new ToneAudioBuffer().fromArray(ramp);
const player = new Player(buff).toDestination(); const player = new Player(buff).toDestination();
@ -442,9 +442,11 @@ describe("Player", () => {
it("can seek to a position at the given time", () => { it("can seek to a position at the given time", () => {
return Offline(() => { return Offline(() => {
const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) { for (let i = 0; i < ramp.length; i++) {
ramp[i] = (i / (ramp.length)) * 0.3; ramp[i] = (i / ramp.length) * 0.3;
} }
const buff = new ToneAudioBuffer().fromArray(ramp); const buff = new ToneAudioBuffer().fromArray(ramp);
const player = new Player(buff).toDestination(); const player = new Player(buff).toDestination();
@ -466,7 +468,7 @@ describe("Player", () => {
const player = new Player(buffer); const player = new Player(buffer);
player.toDestination(); player.toDestination();
player.start(0).stop(0.1); player.start(0).stop(0.1);
return time => { return (time) => {
whenBetween(time, 0.1, Infinity, () => { whenBetween(time, 0.1, Infinity, () => {
expect(player.state).to.equal("stopped"); expect(player.state).to.equal("stopped");
}); });
@ -475,9 +477,13 @@ describe("Player", () => {
}); });
}; };
}, 0.3).then((buff) => { }, 0.3).then((buff) => {
buff.forEachBetween((sample) => { buff.forEachBetween(
expect(sample).to.equal(0); (sample) => {
}, 0.11, 0.15); expect(sample).to.equal(0);
},
0.11,
0.15
);
}); });
}); });
@ -495,7 +501,10 @@ describe("Player", () => {
return Offline(() => { return Offline(() => {
const player = new Player(buffer); const player = new Player(buffer);
player.toDestination(); player.toDestination();
player.start(0, 0, 0.05).start(0.1, 0, 0.05).start(0.2, 0, 0.05); player
.start(0, 0, 0.05)
.start(0.1, 0, 0.05)
.start(0.2, 0, 0.05);
player.stop(0.1); player.stop(0.1);
}, 0.3).then((buff) => { }, 0.3).then((buff) => {
expect(buff.getTimeOfLastSound()).to.be.closeTo(0.1, 0.02); expect(buff.getTimeOfLastSound()).to.be.closeTo(0.1, 0.02);
@ -518,7 +527,7 @@ describe("Player", () => {
const player = new Player(buffer); const player = new Player(buffer);
player.toDestination(); player.toDestination();
player.start(0, 0, 0.1); player.start(0, 0, 0.1);
return time => { return (time) => {
whenBetween(time, 0.1, Infinity, () => { whenBetween(time, 0.1, Infinity, () => {
expect(player.state).to.equal("stopped"); expect(player.state).to.equal("stopped");
}); });
@ -549,17 +558,42 @@ describe("Player", () => {
it("plays synced to the Transport", () => { it("plays synced to the Transport", () => {
return Offline(({ transport }) => { return Offline(({ transport }) => {
const player = new Player(buffer).sync().start(0).toDestination(); const player = new Player(buffer)
.sync()
.start(0)
.toDestination();
transport.start(0); transport.start(0);
}, 0.05).then((buff) => { }, 0.05).then((buff) => {
expect(buff.isSilent()).to.be.false; 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", () => { it("offsets correctly when started by the Transport", () => {
const testSample = buffer.toArray(0)[Math.floor(0.13125 * getContext().sampleRate)]; const testSample =
buffer.toArray(0)[
Math.floor(0.13125 * getContext().sampleRate)
];
return Offline(({ transport }) => { return Offline(({ transport }) => {
const player = new Player(buffer).sync().start(0, 0.1).toDestination(); const player = new Player(buffer)
.sync()
.start(0, 0.1)
.toDestination();
transport.start(0, 0.03125); transport.start(0, 0.03125);
}, 0.05).then((buff) => { }, 0.05).then((buff) => {
expect(buff.toArray()[0][0]).to.equal(testSample); expect(buff.toArray()[0][0]).to.equal(testSample);
@ -569,9 +603,11 @@ describe("Player", () => {
it("starts at the correct position when Transport is offset and playbackRate is not 1", () => { it("starts at the correct position when Transport is offset and playbackRate is not 1", () => {
return Offline(({ transport }) => { return Offline(({ transport }) => {
// make a ramp between 0-1 // make a ramp between 0-1
const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) { for (let i = 0; i < ramp.length; i++) {
ramp[i] = (i / (ramp.length)); ramp[i] = i / ramp.length;
} }
const buff = ToneAudioBuffer.fromArray(ramp); const buff = ToneAudioBuffer.fromArray(ramp);
const player = new Player(buff).toDestination(); const player = new Player(buff).toDestination();
@ -586,9 +622,11 @@ describe("Player", () => {
it("starts with an offset when synced and started after Transport is running", () => { it("starts with an offset when synced and started after Transport is running", () => {
return Offline(({ transport }) => { return Offline(({ transport }) => {
const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) { for (let i = 0; i < ramp.length; i++) {
ramp[i] = (i / (ramp.length)) * 0.3; ramp[i] = (i / ramp.length) * 0.3;
} }
const buff = new ToneAudioBuffer().fromArray(ramp); const buff = new ToneAudioBuffer().fromArray(ramp);
const player = new Player(buff).toDestination(); const player = new Player(buff).toDestination();
@ -606,9 +644,11 @@ describe("Player", () => {
it("can pass in an offset when synced and started after Transport is running", () => { it("can pass in an offset when synced and started after Transport is running", () => {
return Offline(({ transport }) => { return Offline(({ transport }) => {
const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); const ramp = new Float32Array(
Math.floor(getContext().sampleRate * 0.3)
);
for (let i = 0; i < ramp.length; i++) { for (let i = 0; i < ramp.length; i++) {
ramp[i] = (i / (ramp.length)) * 0.3; ramp[i] = (i / ramp.length) * 0.3;
} }
const buff = new ToneAudioBuffer().fromArray(ramp); const buff = new ToneAudioBuffer().fromArray(ramp);
const player = new Player(buff).toDestination(); const player = new Player(buff).toDestination();
@ -630,12 +670,18 @@ describe("Player", () => {
it("fades in and out correctly", () => { it("fades in and out correctly", () => {
let duration = 0.5; let duration = 0.5;
return Offline(() => { return Offline(() => {
const onesArray = new Float32Array(getContext().sampleRate * duration); const onesArray = new Float32Array(
getContext().sampleRate * duration
);
onesArray.forEach((sample, index) => { onesArray.forEach((sample, index) => {
onesArray[index] = 1; onesArray[index] = 1;
}); });
const onesBuffer = ToneAudioBuffer.fromArray(onesArray); const onesBuffer = ToneAudioBuffer.fromArray(onesArray);
const player = new Player({ url: onesBuffer, fadeOut: 0.1, fadeIn: 0.1 }).toDestination(); const player = new Player({
url: onesBuffer,
fadeOut: 0.1,
fadeIn: 0.1,
}).toDestination();
player.start(0); player.start(0);
}, 0.6).then((buff) => { }, 0.6).then((buff) => {
expect(buff.getRmsAtTime(0)).to.be.closeTo(0, 0.1); expect(buff.getRmsAtTime(0)).to.be.closeTo(0, 0.1);
@ -643,7 +689,10 @@ describe("Player", () => {
expect(buff.getRmsAtTime(0.1)).to.be.closeTo(1, 0.1); expect(buff.getRmsAtTime(0.1)).to.be.closeTo(1, 0.1);
duration -= 0.1; duration -= 0.1;
expect(buff.getRmsAtTime(duration)).to.be.closeTo(1, 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.05)).to.be.closeTo(
0.5,
0.1
);
expect(buff.getRmsAtTime(duration + 0.1)).to.be.closeTo(0, 0.1); expect(buff.getRmsAtTime(duration + 0.1)).to.be.closeTo(0, 0.1);
}); });
}); });

Binary file not shown.