tone 15.5.16 → 15.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Tone/core/context/Offline.test.ts +20 -0
- package/Tone/core/context/OfflineContext.ts +6 -1
- package/Tone/source/Source.ts +17 -1
- package/Tone/source/buffer/Player.test.ts +118 -1
- package/Tone/source/buffer/Player.ts +11 -0
- package/Tone/version.ts +1 -1
- package/build/Tone.js +1 -1
- package/build/Tone.js.map +1 -1
- package/build/esm/core/context/OfflineContext.js +6 -1
- package/build/esm/core/context/OfflineContext.js.map +1 -1
- package/build/esm/source/Source.d.ts +7 -0
- package/build/esm/source/Source.js +10 -1
- package/build/esm/source/Source.js.map +1 -1
- package/build/esm/source/buffer/Player.d.ts +5 -0
- package/build/esm/source/buffer/Player.js +7 -0
- package/build/esm/source/buffer/Player.js.map +1 -1
- package/build/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "../clock/Transport.js";
|
|
2
|
+
|
|
1
3
|
import { expect } from "chai";
|
|
2
4
|
|
|
3
5
|
import { TestAudioBuffer } from "../../../test/helper/compare/TestAudioBuffer.js";
|
|
@@ -58,4 +60,22 @@ describe("Offline", () => {
|
|
|
58
60
|
const testBuff = new TestAudioBuffer(buffer.get() as AudioBuffer);
|
|
59
61
|
expect(testBuff.getTimeOfFirstSound()).to.be.closeTo(0.05, 0.0001);
|
|
60
62
|
});
|
|
63
|
+
|
|
64
|
+
it("nodes created in transport scheduleRepeat callbacks use the offline context", async () => {
|
|
65
|
+
// Addresses #781
|
|
66
|
+
// When transport callbacks fire during offline rendering, any nodes
|
|
67
|
+
// created inside them must use the offline context.
|
|
68
|
+
const buffer = await Offline(
|
|
69
|
+
({ transport }) => {
|
|
70
|
+
transport.scheduleRepeat((time) => {
|
|
71
|
+
new ToneOscillatorNode().toDestination().start(time);
|
|
72
|
+
}, 0.1);
|
|
73
|
+
transport.start(0);
|
|
74
|
+
},
|
|
75
|
+
0.5,
|
|
76
|
+
1
|
|
77
|
+
);
|
|
78
|
+
const testBuff = new TestAudioBuffer(buffer.get() as AudioBuffer);
|
|
79
|
+
expect(testBuff.isSilent()).to.equal(false);
|
|
80
|
+
});
|
|
61
81
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createOfflineAudioContext } from "../context/AudioContext.js";
|
|
2
2
|
import { Context } from "../context/Context.js";
|
|
3
|
+
import { getContext, setContext } from "../Global.js";
|
|
3
4
|
import { Seconds } from "../type/Units.js";
|
|
4
5
|
import { isOfflineAudioContext } from "../util/AdvancedTypeCheck.js";
|
|
5
6
|
import { ToneAudioBuffer } from "./ToneAudioBuffer.js";
|
|
@@ -83,8 +84,12 @@ export class OfflineContext extends Context {
|
|
|
83
84
|
private async _renderClock(asynchronous: boolean): Promise<void> {
|
|
84
85
|
let index = 0;
|
|
85
86
|
while (this._duration - this._currentTime >= 0) {
|
|
86
|
-
//
|
|
87
|
+
// temporarily set this offline context as the global default
|
|
88
|
+
// so that any nodes created inside tick callbacks use this context
|
|
89
|
+
const previousContext = getContext();
|
|
90
|
+
setContext(this);
|
|
87
91
|
this.emit("tick");
|
|
92
|
+
setContext(previousContext);
|
|
88
93
|
|
|
89
94
|
// increment the clock in block-sized chunks
|
|
90
95
|
this._currentTime += 128 / this.sampleRate;
|
package/Tone/source/Source.ts
CHANGED
|
@@ -173,6 +173,19 @@ export abstract class Source<
|
|
|
173
173
|
duration?: Time
|
|
174
174
|
): void;
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Compute the buffer offset to use when the source is started mid-playback.
|
|
178
|
+
* @param explicitOffset The buffer-time offset the caller passed to `start()`
|
|
179
|
+
* @param transportElapsed Seconds of Transport time that have elapsed since
|
|
180
|
+
* this source was scheduled to start.
|
|
181
|
+
*/
|
|
182
|
+
protected _getSyncedStartOffset(
|
|
183
|
+
explicitOffset: Seconds,
|
|
184
|
+
transportElapsed: Seconds
|
|
185
|
+
): Seconds {
|
|
186
|
+
return explicitOffset + transportElapsed;
|
|
187
|
+
}
|
|
188
|
+
|
|
176
189
|
/**
|
|
177
190
|
* Ensure that the scheduled time is not before the current time.
|
|
178
191
|
* Should only be used when scheduled unsynced.
|
|
@@ -338,7 +351,10 @@ export abstract class Source<
|
|
|
338
351
|
}
|
|
339
352
|
this._start(
|
|
340
353
|
time,
|
|
341
|
-
this.
|
|
354
|
+
this._getSyncedStartOffset(
|
|
355
|
+
this.toSeconds(stateEvent.offset),
|
|
356
|
+
startOffset
|
|
357
|
+
),
|
|
342
358
|
duration
|
|
343
359
|
);
|
|
344
360
|
}
|
|
@@ -620,7 +620,124 @@ describe("Player", () => {
|
|
|
620
620
|
// start halfway through
|
|
621
621
|
transport.start(0, 0.15);
|
|
622
622
|
}, 0.05);
|
|
623
|
-
|
|
623
|
+
// With playbackRate=0.5, transport position 0.15 maps to buffer position
|
|
624
|
+
// 0.15 * 0.5 = 0.075. The ramp value there is 0.075 / 0.3 = 0.25.
|
|
625
|
+
expect(buff.getValueAtTime(0)).to.be.closeTo(0.25, 0.03);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("seeks to the correct buffer position when Transport starts at a non-zero offset with playbackRate != 1", async () => {
|
|
629
|
+
const buff = await Offline(({ transport }) => {
|
|
630
|
+
// Ramp 0 -> 1 over 0.4 s
|
|
631
|
+
const ramp = new Float32Array(
|
|
632
|
+
Math.floor(getContext().sampleRate * 0.4)
|
|
633
|
+
);
|
|
634
|
+
for (let i = 0; i < ramp.length; i++) {
|
|
635
|
+
ramp[i] = i / ramp.length;
|
|
636
|
+
}
|
|
637
|
+
const playerBuff = ToneAudioBuffer.fromArray(ramp);
|
|
638
|
+
const player = new Player(playerBuff).toDestination();
|
|
639
|
+
// Buffer plays at 2x speed: 1 transport-second advances 2 buffer-seconds
|
|
640
|
+
player.playbackRate = 2;
|
|
641
|
+
player.sync().start(0);
|
|
642
|
+
// Seek transport to 0.1 s. The player should start at buffer position
|
|
643
|
+
// 0.1 * 2 = 0.2, giving a ramp value of 0.2 / 0.4 = 0.5.
|
|
644
|
+
transport.start(0, 0.1);
|
|
645
|
+
}, 0.05);
|
|
646
|
+
expect(buff.getValueAtTime(0)).to.be.closeTo(0.5, 0.03);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it("explicit buffer-time offset is not scaled by playbackRate when synced", async () => {
|
|
650
|
+
const buff = await Offline(({ transport }) => {
|
|
651
|
+
const ramp = new Float32Array(
|
|
652
|
+
Math.floor(getContext().sampleRate * 0.3)
|
|
653
|
+
);
|
|
654
|
+
for (let i = 0; i < ramp.length; i++) {
|
|
655
|
+
ramp[i] = i / ramp.length;
|
|
656
|
+
}
|
|
657
|
+
const playerBuff = ToneAudioBuffer.fromArray(ramp);
|
|
658
|
+
const player = new Player(playerBuff).toDestination();
|
|
659
|
+
player.loop = true;
|
|
660
|
+
player.playbackRate = 2;
|
|
661
|
+
player.sync().start(0, 0.1);
|
|
662
|
+
transport.start(0);
|
|
663
|
+
}, 0.05);
|
|
664
|
+
expect(buff.getValueAtTime(0)).to.be.closeTo(1 / 3, 0.03);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it("combines explicit buffer-time offset with transport-elapsed time scaled by playbackRate", async () => {
|
|
668
|
+
const buff = await Offline(({ transport }) => {
|
|
669
|
+
const ramp = new Float32Array(
|
|
670
|
+
Math.floor(getContext().sampleRate * 0.3)
|
|
671
|
+
);
|
|
672
|
+
for (let i = 0; i < ramp.length; i++) {
|
|
673
|
+
ramp[i] = i / ramp.length;
|
|
674
|
+
}
|
|
675
|
+
const playerBuff = ToneAudioBuffer.fromArray(ramp);
|
|
676
|
+
const player = new Player(playerBuff).toDestination();
|
|
677
|
+
player.loop = true;
|
|
678
|
+
player.playbackRate = 2;
|
|
679
|
+
player.sync().start(0, 0.1);
|
|
680
|
+
transport.start(0, 0.05);
|
|
681
|
+
}, 0.05);
|
|
682
|
+
expect(buff.getValueAtTime(0)).to.be.closeTo(2 / 3, 0.03);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("looping player wraps correctly when transport offset exceeds buffer duration (playbackRate=1)", async () => {
|
|
686
|
+
// buffer duration = 1s, transport seeks to 2.5s
|
|
687
|
+
// buffer offset = 2.5 * 1 = 2.5 → wrapped = 2.5 % 1 = 0.5
|
|
688
|
+
const buff = await Offline(({ transport }) => {
|
|
689
|
+
const ramp = new Float32Array(getContext().sampleRate); // 1 s
|
|
690
|
+
for (let i = 0; i < ramp.length; i++) {
|
|
691
|
+
ramp[i] = i / ramp.length;
|
|
692
|
+
}
|
|
693
|
+
const playerBuff = ToneAudioBuffer.fromArray(ramp);
|
|
694
|
+
const player = new Player(playerBuff).toDestination();
|
|
695
|
+
player.loop = true;
|
|
696
|
+
player.sync().start(0);
|
|
697
|
+
transport.start(0, 2.5);
|
|
698
|
+
}, 0.05);
|
|
699
|
+
expect(buff.getValueAtTime(0)).to.be.closeTo(0.5, 0.03);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("looping player wraps correctly when transport offset exceeds buffer duration (playbackRate=2)", async () => {
|
|
703
|
+
// buffer duration = 1s, transport seeks to 1.25s, playbackRate=2
|
|
704
|
+
// buffer offset = 1.25 * 2 = 2.5 → wrapped = 2.5 % 1 = 0.5
|
|
705
|
+
const buff = await Offline(({ transport }) => {
|
|
706
|
+
const ramp = new Float32Array(getContext().sampleRate); // 1 s
|
|
707
|
+
for (let i = 0; i < ramp.length; i++) {
|
|
708
|
+
ramp[i] = i / ramp.length;
|
|
709
|
+
}
|
|
710
|
+
const playerBuff = ToneAudioBuffer.fromArray(ramp);
|
|
711
|
+
const player = new Player(playerBuff).toDestination();
|
|
712
|
+
player.loop = true;
|
|
713
|
+
player.playbackRate = 2;
|
|
714
|
+
player.sync().start(0);
|
|
715
|
+
transport.start(0, 1.25);
|
|
716
|
+
}, 0.05);
|
|
717
|
+
expect(buff.getValueAtTime(0)).to.be.closeTo(0.5, 0.03);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("looping player with loopStart/loopEnd wraps correctly when transport offset exceeds loop duration (playbackRate=2)", async () => {
|
|
721
|
+
// buffer duration = 1s, loopStart=0.2, loopEnd=1.0, loopDuration=0.8
|
|
722
|
+
// transport seeks to 1.15s, playbackRate=2
|
|
723
|
+
// buffer offset = 1.15 * 2 = 2.3
|
|
724
|
+
// wrapped = ((2.3 - 0.2) % 0.8) + 0.2 = (2.1 % 0.8) + 0.2 = 0.5 + 0.2 = 0.7
|
|
725
|
+
// ramp value at 0.7 in a 1s ramp = 0.7
|
|
726
|
+
const buff = await Offline(({ transport }) => {
|
|
727
|
+
const ramp = new Float32Array(getContext().sampleRate); // 1 s
|
|
728
|
+
for (let i = 0; i < ramp.length; i++) {
|
|
729
|
+
ramp[i] = i / ramp.length;
|
|
730
|
+
}
|
|
731
|
+
const playerBuff = ToneAudioBuffer.fromArray(ramp);
|
|
732
|
+
const player = new Player(playerBuff).toDestination();
|
|
733
|
+
player.loop = true;
|
|
734
|
+
player.loopStart = 0.2;
|
|
735
|
+
player.loopEnd = 1.0;
|
|
736
|
+
player.playbackRate = 2;
|
|
737
|
+
player.sync().start(0);
|
|
738
|
+
transport.start(0, 1.15);
|
|
739
|
+
}, 0.05);
|
|
740
|
+
expect(buff.getValueAtTime(0)).to.be.closeTo(0.7, 0.03);
|
|
624
741
|
});
|
|
625
742
|
|
|
626
743
|
it("starts with an offset when synced and started after Transport is running", async () => {
|
|
@@ -262,6 +262,17 @@ export class Player extends Source<PlayerOptions> {
|
|
|
262
262
|
return this;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Factor in the playback rate to compute the buffer offset when the player
|
|
267
|
+
* is synced to the Transport.
|
|
268
|
+
*/
|
|
269
|
+
protected _getSyncedStartOffset(
|
|
270
|
+
explicitOffset: number,
|
|
271
|
+
transportElapsed: number
|
|
272
|
+
): number {
|
|
273
|
+
return explicitOffset + transportElapsed * this._playbackRate;
|
|
274
|
+
}
|
|
275
|
+
|
|
265
276
|
/**
|
|
266
277
|
* Internal start method
|
|
267
278
|
*/
|
package/Tone/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version: string = "15.5.
|
|
1
|
+
export const version: string = "15.5.18";
|