tone 15.5.43 → 15.5.45

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.
@@ -1,5 +1,6 @@
1
1
  import { getContext } from "../Global.js";
2
2
  import { Seconds, Ticks } from "../type/Units.js";
3
+ import { isDefined } from "../util/TypeCheck.js";
3
4
  import { TimeClass } from "./Time.js";
4
5
  import { TimeBaseUnit, TimeExpression, TimeValue } from "./TimeBase.js";
5
6
 
@@ -22,6 +23,21 @@ export class TransportTimeClass<
22
23
  return this.context.transport.seconds as Type;
23
24
  }
24
25
 
26
+ /**
27
+ * Return the time in ticks.
28
+ */
29
+ toTicks(): Ticks {
30
+ if (isDefined(this._val)) {
31
+ return super.toTicks();
32
+ }
33
+ // read the transport's actual current tick instead of re-deriving it
34
+ // from elapsed seconds at the current bpm, which ignores tempo changes.
35
+ // round up to the next tick since the current one may already be
36
+ // in the middle of being processed by the clock.
37
+ const transport = this.context.transport;
38
+ return Math.ceil(transport.getTicksAtTime(transport.now()));
39
+ }
40
+
25
41
  protected _getExpressions(): TimeExpression<Type> {
26
42
  const expressions = super._getExpressions();
27
43
  // Override the quantize ("@") handler so that it returns Transport time
@@ -928,5 +928,59 @@ describe("Part", () => {
928
928
  }).to.throw(Error);
929
929
  part.dispose();
930
930
  });
931
+
932
+ it("`toTicks()` with no argument matches the transport's actual current tick after a bpm change (#693)", async () => {
933
+ let actualTicks = 0;
934
+ let noArgTicks = 0;
935
+ await Offline(({ transport }) => {
936
+ transport.bpm.value = 120;
937
+ transport.start(0);
938
+ return atTime(0.5, () => {
939
+ transport.bpm.value = 40;
940
+ actualTicks = transport.getTicksAtTime(transport.now());
941
+ noArgTicks = transport.toTicks();
942
+ });
943
+ }, 0.6);
944
+ expect(noArgTicks).to.be.closeTo(actualTicks, 1);
945
+ });
946
+
947
+ it("does not delay a part's note when it is (re)started with no time arg while the transport is running", async () => {
948
+ let callTime: number | undefined;
949
+ await Offline(({ transport }) => {
950
+ const part = new Part(
951
+ (time) => {
952
+ callTime = time;
953
+ },
954
+ [0]
955
+ );
956
+ transport.bpm.value = 120;
957
+ transport.start(0);
958
+ return atTime(0.5, () => {
959
+ part.start();
960
+ });
961
+ }, 1);
962
+ expect(callTime).to.be.closeTo(0.5, 0.02);
963
+ });
964
+
965
+ it("does not leave an orphaned repeat after stopping a phase-shifted note before its first occurrence", async () => {
966
+ // a note's own repeat is phase-shifted from the part's start tick;
967
+ // stopping before a phase-shifted note's first occurrence used to miss
968
+ // that boundary, leaving an orphaned repeat that could later collide
969
+ // with a subsequent restart (#864)
970
+ const calls: number[] = [];
971
+ await Offline(({ transport }) => {
972
+ const part = new Part((time) => calls.push(time), [0, 0.5]);
973
+ part.loop = true;
974
+ part.loopEnd = 1;
975
+ transport.bpm.value = 120;
976
+ transport.start(0);
977
+ part.start(0);
978
+ // stop before the note at 0.5s into the loop ever gets to fire
979
+ part.stop(0.1);
980
+ // restart partway through the next cycle
981
+ part.start(200 / 384);
982
+ }, 2);
983
+ expect(calls.length).to.equal(4);
984
+ });
931
985
  });
932
986
  });
@@ -1,7 +1,7 @@
1
1
  import { expect } from "chai";
2
2
 
3
3
  import { BasicTests } from "../../test/helper/Basic.js";
4
- import { Offline, whenBetween } from "../../test/helper/Offline.js";
4
+ import { atTime, Offline, whenBetween } from "../../test/helper/Offline.js";
5
5
  import { Time } from "../core/type/Time.js";
6
6
  import { noOp } from "../core/util/Interface.js";
7
7
  import { ToneEvent } from "./ToneEvent.js";
@@ -362,6 +362,30 @@ describe("ToneEvent", () => {
362
362
  }, 2);
363
363
  });
364
364
 
365
+ it("keeps a still-playing segment running until its own scheduled stop, even when a later restart is already queued (#864)", async () => {
366
+ const callTimes: number[] = [];
367
+ await Offline(({ transport }) => {
368
+ const note = new ToneEvent({
369
+ loop: true,
370
+ loopEnd: 0.1,
371
+ callback(time): void {
372
+ callTimes.push(time);
373
+ },
374
+ });
375
+ note.start(0);
376
+ transport.start(0);
377
+ return atTime(0.3, () => {
378
+ // queue a stop/restart ahead of the still-playing segment
379
+ note.stop(transport.seconds + 0.5);
380
+ note.start(transport.seconds + 0.8);
381
+ });
382
+ }, 1.5);
383
+ // the live segment should keep firing up to its own stop (~0.8),
384
+ // not be cut short by the later start() call scheduled at the same time
385
+ const notesBetween = callTimes.filter((t) => t > 0.35 && t < 0.75);
386
+ expect(notesBetween.length).to.be.greaterThan(2);
387
+ });
388
+
365
389
  it("loops the correct amount of times when the event is started in the transport's past", async () => {
366
390
  let callCount = 0;
367
391
  await Offline(({ transport }) => {
@@ -16,6 +16,7 @@ import {
16
16
  } from "../core/type/Units.js";
17
17
  import { defaultArg, optionsFromArguments } from "../core/util/Defaults.js";
18
18
  import { noOp } from "../core/util/Interface.js";
19
+ import { clamp } from "../core/util/Math.js";
19
20
  import {
20
21
  BasicPlaybackState,
21
22
  StateTimeline,
@@ -185,11 +186,15 @@ export class ToneEvent<ValueType = any> extends ToneWithContext<
185
186
  if (isNumber(this._loop)) {
186
187
  duration = this._loop * this._getLoopDuration();
187
188
  }
188
- const nextEvent = this._state.getAfter(startTick);
189
+ // look up the next boundary in the same (unshifted) frame as
190
+ // event.time; startOffset can push startTick past it entirely,
191
+ // in which case this segment shouldn't schedule anything
192
+ const nextEvent = this._state.getAfter(event.time);
189
193
  if (nextEvent !== null) {
190
- duration = Math.min(
191
- duration,
192
- nextEvent.time - startTick
194
+ duration = clamp(
195
+ nextEvent.time - startTick,
196
+ 0,
197
+ duration
193
198
  );
194
199
  }
195
200
  if (duration !== Infinity) {
@@ -395,5 +395,31 @@ describe("Source", () => {
395
395
  expect(output.getValueAtTime(0.25)).to.be.closeTo(0.05, 0.01);
396
396
  expect(output.getValueAtTime(0.3)).to.be.closeTo(0.1, 0.01);
397
397
  });
398
+
399
+ it("does not replay a clip synced at time 0 after stopping and restarting at a later offset (#1417)", async () => {
400
+ let onStopCount = 0;
401
+ await Offline(({ transport }) => {
402
+ const source = new Player(rampBuffer).toDestination();
403
+ source.onstop = () => {
404
+ onStopCount++;
405
+ };
406
+ // rampBuffer duration is 1s. Sync at time 0 with duration 0.2s.
407
+ // This clip only spans [0, 0.2]. At transport time 0.4, it is over.
408
+ source.sync().start(0, 0, 0.2);
409
+ transport.start(0, 0);
410
+
411
+ return [
412
+ atTime(0.25, () => {
413
+ transport.stop();
414
+ }),
415
+ atTime(0.3, () => {
416
+ // restart past the clip duration at offset 0.4
417
+ transport.start(0.3, 0.4);
418
+ }),
419
+ ];
420
+ }, 0.5);
421
+
422
+ expect(onStopCount).to.equal(1);
423
+ });
398
424
  });
399
425
  });
@@ -348,6 +348,9 @@ export abstract class Source<
348
348
  duration =
349
349
  this.toSeconds(stateEvent.duration) -
350
350
  startOffset;
351
+ if (duration <= 0) {
352
+ return;
353
+ }
351
354
  }
352
355
  this._start(
353
356
  time,
package/Tone/version.ts CHANGED
@@ -1 +1 @@
1
- export const version: string = "15.5.43";
1
+ export const version: string = "15.5.45";