tone 15.5.59 → 15.5.61

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.
@@ -428,6 +428,38 @@ describe("Clock", () => {
428
428
  ["start", restartTime],
429
429
  ]);
430
430
  });
431
+
432
+ it("invokes pending ticks and state events in time order", async () => {
433
+ const events: Array<[string, number]> = [];
434
+ let pauseTime = 0;
435
+ await Offline(() => {
436
+ const clock = new Clock((time, ticks) => {
437
+ events.push(["tick " + ticks, time]);
438
+ }, 200);
439
+ clock.on("pause", (time) => events.push(["pause", time]));
440
+ clock.on("start", (time) => events.push(["start", time]));
441
+ clock.start(0);
442
+ // ticks 0-4 (every 0.005s) and the pause are all pending in
443
+ // the same clock update; the ticks before the pause must be
444
+ // invoked before the pause event, not after it
445
+ return atTime(
446
+ 0.022,
447
+ holdClock(clock, (time) => {
448
+ pauseTime = time;
449
+ clock.pause(time);
450
+ })
451
+ );
452
+ }, 0.03);
453
+ expect(events).to.deep.equal([
454
+ ["start", 0],
455
+ ["tick 0", 0],
456
+ ["tick 1", 0.005],
457
+ ["tick 2", 0.01],
458
+ ["tick 3", 0.015],
459
+ ["tick 4", 0.02],
460
+ ["pause", pauseTime],
461
+ ]);
462
+ });
431
463
  });
432
464
 
433
465
  context("Events", () => {
@@ -7,7 +7,12 @@ import { assertContextRunning } from "../util/Debug.js";
7
7
  import { optionsFromArguments } from "../util/Defaults.js";
8
8
  import { Emitter } from "../util/Emitter.js";
9
9
  import { noOp, readOnly } from "../util/Interface.js";
10
- import { PlaybackState, StateTimeline } from "../util/StateTimeline.js";
10
+ import { GT } from "../util/Math.js";
11
+ import {
12
+ PlaybackState,
13
+ StateTimeline,
14
+ StateTimelineEvent,
15
+ } from "../util/StateTimeline.js";
11
16
  import { TickSignal } from "./TickSignal.js";
12
17
  import { TickSource } from "./TickSource.js";
13
18
 
@@ -290,39 +295,62 @@ export class Clock<TypeName extends "bpm" | "hertz" = "hertz">
290
295
 
291
296
  /**
292
297
  * Invoke the state-transition events (start/stop/pause) and tick
293
- * callbacks scheduled between startTime and endTime, and advance
294
- * _lastUpdate to endTime.
298
+ * callbacks scheduled between startTime and endTime in time order,
299
+ * and advance _lastUpdate to endTime.
295
300
  * @param startTime The beginning of the range to process.
296
301
  * @param endTime The end of the range to process.
297
302
  */
298
303
  private _processRange(startTime: number, endTime: number): void {
299
304
  this._lastUpdate = endTime;
300
305
  if (startTime !== endTime) {
301
- // the state change events
306
+ // the state change events, interleaved with the tick callbacks
307
+ // so that e.g. a tick just before a pause is delivered before
308
+ // the pause event, not after it
309
+ const stateEvents: StateTimelineEvent[] = [];
302
310
  this._state.forEachBetween(startTime, endTime, (e) => {
303
- switch (e.state) {
304
- case "started":
305
- const offset = this._tickSource.getTicksAtTime(e.time);
306
- this.emit("start", e.time, offset);
307
- break;
308
- case "stopped":
309
- if (e.time !== 0) {
310
- this.emit("stop", e.time);
311
- }
312
- break;
313
- case "paused":
314
- this.emit("pause", e.time);
315
- break;
316
- }
311
+ stateEvents.push(e);
317
312
  });
318
- // the tick callbacks
313
+ let nextStateEvent = 0;
314
+ const emitStateEventsUpTo = (time: Seconds) => {
315
+ // a callback may have disposed the clock
316
+ while (
317
+ !this.disposed &&
318
+ nextStateEvent < stateEvents.length &&
319
+ !GT(stateEvents[nextStateEvent].time, time)
320
+ ) {
321
+ this._emitStateEvent(stateEvents[nextStateEvent]);
322
+ nextStateEvent++;
323
+ }
324
+ };
319
325
  this._tickSource.forEachTickBetween(
320
326
  startTime,
321
327
  endTime,
322
328
  (time, ticks) => {
329
+ emitStateEventsUpTo(time);
323
330
  this.callback(time, ticks);
324
331
  }
325
332
  );
333
+ emitStateEventsUpTo(endTime);
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Emit the event corresponding to a state change
339
+ */
340
+ private _emitStateEvent(event: StateTimelineEvent): void {
341
+ switch (event.state) {
342
+ case "started":
343
+ const offset = this._tickSource.getTicksAtTime(event.time);
344
+ this.emit("start", event.time, offset);
345
+ break;
346
+ case "stopped":
347
+ if (event.time !== 0) {
348
+ this.emit("stop", event.time);
349
+ }
350
+ break;
351
+ case "paused":
352
+ this.emit("pause", event.time);
353
+ break;
326
354
  }
327
355
  }
328
356
 
@@ -3,6 +3,7 @@ import { expect } from "chai";
3
3
  import { atTime, holdClock, Offline } from "../../test/helper/Offline.js";
4
4
  import { ToneAudioBuffer } from "../core/context/ToneAudioBuffer.js";
5
5
  import { getContext } from "../core/Global.js";
6
+ import { noOp } from "../core/util/Interface.js";
6
7
  import { Player } from "./buffer/Player.js";
7
8
  import { Oscillator } from "./oscillator/Oscillator.js";
8
9
 
@@ -351,6 +352,55 @@ describe("Source", () => {
351
352
  expect(output.getValueAtTime(0.4)).to.be.closeTo(0.7, 0.01);
352
353
  });
353
354
 
355
+ it("stops a source whose start was pending in the same clock update as a pause (#1289)", async () => {
356
+ const output = await Offline(({ transport }) => {
357
+ const source = new Player(rampBuffer).toDestination();
358
+ source.sync().start(0.5);
359
+ transport.start(0);
360
+ // the clip's start tick at 0.5 and the pause at ~0.52 are
361
+ // processed in the same clock update
362
+ return atTime(
363
+ 0.52,
364
+ holdClock(transport, (time) => {
365
+ transport.pause(time);
366
+ })
367
+ );
368
+ }, 1);
369
+ expect(output.getValueAtTime(0.7)).to.equal(0);
370
+ expect(output.getValueAtTime(0.9)).to.equal(0);
371
+ });
372
+
373
+ it("stops a source whose start was pending in the same clock update as a stop (#1289)", async () => {
374
+ const output = await Offline(({ transport }) => {
375
+ const source = new Player(rampBuffer).toDestination();
376
+ source.sync().start(0.5);
377
+ transport.start(0);
378
+ return atTime(
379
+ 0.52,
380
+ holdClock(transport, (time) => {
381
+ transport.stop(time);
382
+ })
383
+ );
384
+ }, 1);
385
+ expect(output.getValueAtTime(0.7)).to.equal(0);
386
+ expect(output.getValueAtTime(0.9)).to.equal(0);
387
+ });
388
+
389
+ it("stops a source whose start was pending in the same clock update as a stop scheduled ahead of time", async () => {
390
+ const output = await Offline(({ transport }) => {
391
+ const source = new Player(rampBuffer).toDestination();
392
+ source.sync().start(0.5);
393
+ transport.start(0);
394
+ // stop scheduled in advance; the start tick at 0.5 and the
395
+ // stop at 0.52 are both pending when the clock catches up
396
+ transport.stop(0.52);
397
+ return atTime(0.55, holdClock(transport, noOp));
398
+ }, 1);
399
+ expect(output.getValueAtTime(0.51)).to.be.closeTo(0.01, 0.005);
400
+ expect(output.getValueAtTime(0.7)).to.equal(0);
401
+ expect(output.getValueAtTime(0.9)).to.equal(0);
402
+ });
403
+
354
404
  it("gives the correct offset on time on start/stop events when started with an offset", async () => {
355
405
  const output = await Offline(({ transport }) => {
356
406
  const source = new Player(rampBuffer).toDestination();
@@ -9,6 +9,7 @@ import { Offline } from "../../../test/helper/Offline.js";
9
9
  import { OscillatorTests } from "../../../test/helper/OscillatorTests.js";
10
10
  import { OutputAudio } from "../../../test/helper/OutputAudio.js";
11
11
  import { SourceTests } from "../../../test/helper/SourceTests.js";
12
+ import { OfflineContext } from "../../core/context/OfflineContext.js";
12
13
  import { Signal } from "../../signal/Signal.js";
13
14
  import { Oscillator } from "./Oscillator.js";
14
15
  import { ToneOscillatorType } from "./OscillatorInterface.js";
@@ -111,14 +112,103 @@ describe("Oscillator", () => {
111
112
  }
112
113
  });
113
114
  });
115
+ });
116
+
117
+ context("Periodic Wave Cache", () => {
118
+ beforeEach(() => {
119
+ // @ts-ignore
120
+ Oscillator._periodicWaveCache = [];
121
+ });
114
122
 
115
123
  it("can go past the cache max size of 100", () => {
116
124
  const osc = new Oscillator();
117
125
  for (let i = 0; i < 110; i++) {
118
126
  osc.phase = i;
119
127
  }
128
+ // @ts-ignore
129
+ expect(Oscillator._periodicWaveCache.length).to.equal(100);
120
130
  osc.dispose();
121
131
  });
132
+
133
+ it("caches periodic waves by sample rate", () => {
134
+ const createWave = sinon.spy(
135
+ OfflineContext.prototype,
136
+ "createPeriodicWave"
137
+ );
138
+ const context44k = new OfflineContext(1, 0.01, 44100);
139
+ const context48k = new OfflineContext(1, 0.01, 48000);
140
+
141
+ const osc1 = new Oscillator({
142
+ context: context44k,
143
+ phase: 90,
144
+ });
145
+ const osc2 = new Oscillator({
146
+ context: context44k,
147
+ phase: 90,
148
+ });
149
+ expect(createWave).to.have.been.calledOnce;
150
+
151
+ const osc3 = new Oscillator({
152
+ context: context48k,
153
+ phase: 90,
154
+ });
155
+ expect(createWave).to.have.been.calledTwice;
156
+
157
+ const osc4 = new Oscillator({
158
+ context: context48k,
159
+ phase: 90,
160
+ });
161
+ expect(createWave).to.have.been.calledTwice;
162
+
163
+ osc1.dispose();
164
+ osc2.dispose();
165
+ osc3.dispose();
166
+ osc4.dispose();
167
+ context44k.dispose();
168
+ context48k.dispose();
169
+ });
170
+
171
+ it("caches custom waveforms by sample rate", () => {
172
+ const createWave = sinon.spy(
173
+ OfflineContext.prototype,
174
+ "createPeriodicWave"
175
+ );
176
+ const context44k = new OfflineContext(1, 0.01, 44100);
177
+ const context48k = new OfflineContext(1, 0.01, 48000);
178
+
179
+ const osc1 = new Oscillator({
180
+ context: context44k,
181
+ partials: [1, 2, 3],
182
+ type: "custom",
183
+ });
184
+ const osc2 = new Oscillator({
185
+ context: context44k,
186
+ partials: [1, 2, 3],
187
+ type: "custom",
188
+ });
189
+ expect(createWave).to.have.been.calledOnce;
190
+
191
+ const osc3 = new Oscillator({
192
+ context: context48k,
193
+ partials: [1, 2, 3],
194
+ type: "custom",
195
+ });
196
+ expect(createWave).to.have.been.calledTwice;
197
+
198
+ const osc4 = new Oscillator({
199
+ context: context48k,
200
+ partials: [1, 2, 3],
201
+ type: "custom",
202
+ });
203
+ expect(createWave).to.have.been.calledTwice;
204
+
205
+ osc1.dispose();
206
+ osc2.dispose();
207
+ osc3.dispose();
208
+ osc4.dispose();
209
+ context44k.dispose();
210
+ context48k.dispose();
211
+ });
122
212
  });
123
213
 
124
214
  context("Type", () => {
@@ -230,6 +230,7 @@ export class Oscillator
230
230
  phase: number;
231
231
  type: string;
232
232
  partialCount: number;
233
+ sampleRate: number;
233
234
  real: Float32Array;
234
235
  imag: Float32Array;
235
236
  wave: PeriodicWave;
@@ -252,6 +253,7 @@ export class Oscillator
252
253
  const oscProps = Oscillator._periodicWaveCache.find(
253
254
  (description) => {
254
255
  return (
256
+ description.sampleRate === this.context.sampleRate &&
255
257
  description.phase === this._phase &&
256
258
  deepEquals(description.partials, this._partials)
257
259
  );
@@ -262,6 +264,7 @@ export class Oscillator
262
264
  const oscProps = Oscillator._periodicWaveCache.find(
263
265
  (description) => {
264
266
  return (
267
+ description.sampleRate === this.context.sampleRate &&
265
268
  description.type === this._type &&
266
269
  description.phase === this._phase
267
270
  );
@@ -315,6 +318,7 @@ export class Oscillator
315
318
  partialCount: this._partialCount,
316
319
  partials: this._partials,
317
320
  phase: this._phase,
321
+ sampleRate: this.context.sampleRate,
318
322
  real,
319
323
  type: this._type,
320
324
  wave: this._wave,
package/Tone/version.ts CHANGED
@@ -1 +1 @@
1
- export const version: string = "15.5.59";
1
+ export const version: string = "15.5.61";