tone 14.8.33 → 14.8.37

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.
Files changed (55) hide show
  1. package/Tone/component/analysis/Meter.test.ts +25 -0
  2. package/Tone/component/analysis/Meter.ts +7 -5
  3. package/Tone/core/clock/TickSource.test.ts +52 -0
  4. package/Tone/core/clock/TickSource.ts +70 -9
  5. package/Tone/core/clock/Ticker.ts +11 -5
  6. package/Tone/core/context/Context.test.ts +2 -0
  7. package/Tone/core/context/Context.ts +29 -38
  8. package/Tone/core/context/ToneAudioBuffer.ts +52 -27
  9. package/Tone/event/Pattern.test.ts +31 -5
  10. package/Tone/event/Pattern.ts +19 -6
  11. package/Tone/event/PatternGenerator.test.ts +18 -26
  12. package/Tone/event/PatternGenerator.ts +52 -59
  13. package/Tone/instrument/MetalSynth.ts +48 -21
  14. package/Tone/signal/ToneConstantSource.test.ts +1 -1
  15. package/Tone/source/OneShotSource.ts +1 -1
  16. package/Tone/source/Source.ts +84 -27
  17. package/Tone/source/buffer/Player.test.ts +123 -49
  18. package/Tone/source/buffer/Player.ts +46 -22
  19. package/Tone/source/oscillator/ToneOscillatorNode.test.ts +1 -1
  20. package/Tone/version.ts +1 -1
  21. package/build/Tone.js +1 -1
  22. package/build/Tone.js.map +1 -1
  23. package/build/esm/component/analysis/Meter.d.ts +1 -1
  24. package/build/esm/component/analysis/Meter.js +5 -7
  25. package/build/esm/component/analysis/Meter.js.map +1 -1
  26. package/build/esm/core/clock/TickSource.d.ts +8 -0
  27. package/build/esm/core/clock/TickSource.js +49 -7
  28. package/build/esm/core/clock/TickSource.js.map +1 -1
  29. package/build/esm/core/clock/Ticker.d.ts +5 -1
  30. package/build/esm/core/clock/Ticker.js +6 -4
  31. package/build/esm/core/clock/Ticker.js.map +1 -1
  32. package/build/esm/core/context/Context.d.ts +12 -12
  33. package/build/esm/core/context/Context.js +26 -29
  34. package/build/esm/core/context/Context.js.map +1 -1
  35. package/build/esm/core/context/ToneAudioBuffer.js +25 -15
  36. package/build/esm/core/context/ToneAudioBuffer.js.map +1 -1
  37. package/build/esm/event/Pattern.d.ts +8 -0
  38. package/build/esm/event/Pattern.js +11 -4
  39. package/build/esm/event/Pattern.js.map +1 -1
  40. package/build/esm/event/PatternGenerator.d.ts +4 -4
  41. package/build/esm/event/PatternGenerator.js +52 -58
  42. package/build/esm/event/PatternGenerator.js.map +1 -1
  43. package/build/esm/instrument/MetalSynth.d.ts +0 -1
  44. package/build/esm/instrument/MetalSynth.js +15 -13
  45. package/build/esm/instrument/MetalSynth.js.map +1 -1
  46. package/build/esm/source/OneShotSource.js +1 -1
  47. package/build/esm/source/OneShotSource.js.map +1 -1
  48. package/build/esm/source/Source.js +27 -14
  49. package/build/esm/source/Source.js.map +1 -1
  50. package/build/esm/source/buffer/Player.d.ts +2 -1
  51. package/build/esm/source/buffer/Player.js +21 -12
  52. package/build/esm/source/buffer/Player.js.map +1 -1
  53. package/build/esm/version.js +1 -1
  54. package/docs/tone.json +1 -1
  55. package/package.json +2 -2
@@ -5,6 +5,8 @@ import { ONLINE_TESTING } from "test/helper/Supports";
5
5
  import { Signal } from "Tone/signal/Signal";
6
6
  import { Oscillator } from "Tone/source/oscillator/Oscillator";
7
7
  import { Meter } from "./Meter";
8
+ import { Panner } from "Tone/component/channel/Panner";
9
+ import { Merge } from "Tone/component/channel/Merge";
8
10
 
9
11
  describe("Meter", () => {
10
12
 
@@ -86,6 +88,29 @@ describe("Meter", () => {
86
88
  done();
87
89
  }, 400);
88
90
  });
91
+
92
+ it("can get the rms levels for multiple channels", (done) => {
93
+ const meter = new Meter({
94
+ channelCount: 2,
95
+ smoothing: 0.5,
96
+ });
97
+ const merge = new Merge().connect(meter);
98
+ const osc0 = new Oscillator().connect(merge, 0, 0).start();
99
+ const osc1 = new Oscillator().connect(merge, 0, 1).start();
100
+ osc0.volume.value = -6;
101
+ osc1.volume.value = -18;
102
+ setTimeout(() => {
103
+ const values = meter.getValue();
104
+ expect(values).to.have.lengthOf(2);
105
+ expect(values[0]).to.be.closeTo(-9, 1);
106
+ expect(values[1]).to.be.closeTo(-21, 1);
107
+ meter.dispose();
108
+ merge.dispose();
109
+ osc0.dispose();
110
+ osc1.dispose();
111
+ done();
112
+ }, 400);
113
+ });
89
114
  }
90
115
  });
91
116
  });
@@ -42,9 +42,9 @@ export class Meter extends MeterBase<MeterOptions> {
42
42
  smoothing: number;
43
43
 
44
44
  /**
45
- * The previous frame's value
45
+ * The previous frame's value for each channel.
46
46
  */
47
- private _rms = 0;
47
+ private _rms: number[];
48
48
 
49
49
  /**
50
50
  * @param smoothing The amount of smoothing applied between frames.
@@ -64,6 +64,8 @@ export class Meter extends MeterBase<MeterOptions> {
64
64
 
65
65
  this.smoothing = options.smoothing,
66
66
  this.normalRange = options.normalRange;
67
+ this._rms = new Array(options.channelCount);
68
+ this._rms.fill(0);
67
69
  }
68
70
 
69
71
  static getDefaults(): MeterOptions {
@@ -93,13 +95,13 @@ export class Meter extends MeterBase<MeterOptions> {
93
95
  getValue(): number | number[] {
94
96
  const aValues = this._analyser.getValue();
95
97
  const channelValues = this.channels === 1 ? [aValues as Float32Array] : aValues as Float32Array[];
96
- const vals = channelValues.map(values => {
98
+ const vals = channelValues.map((values, index) => {
97
99
  const totalSquared = values.reduce((total, current) => total + current * current, 0);
98
100
  const rms = Math.sqrt(totalSquared / values.length);
99
101
  // the rms can only fall at the rate of the smoothing
100
102
  // but can jump up instantly
101
- this._rms = Math.max(rms, this._rms * this.smoothing);
102
- return this.normalRange ? this._rms : gainToDb(this._rms);
103
+ this._rms[index] = Math.max(rms, this._rms[index] * this.smoothing);
104
+ return this.normalRange ? this._rms[index] : gainToDb(this._rms[index]);
103
105
  });
104
106
  if (this.channels === 1) {
105
107
  return vals[0];
@@ -237,6 +237,35 @@ describe("TickSource", () => {
237
237
  expect(source.getTicksAtTime(6)).to.be.closeTo(6, 0.01);
238
238
  source.dispose();
239
239
  });
240
+
241
+ it("will recompute memoized values when events are modified", () => {
242
+ const source = new TickSource(1);
243
+ source.start(3).pause(4);
244
+ expect(source.getTicksAtTime(1)).to.be.closeTo(0, 0.01);
245
+ expect(source.getTicksAtTime(2)).to.be.closeTo(0, 0.01);
246
+ expect(source.getTicksAtTime(3)).to.be.closeTo(0, 0.01);
247
+ expect(source.getTicksAtTime(4)).to.be.closeTo(1, 0.01);
248
+ expect(source.getTicksAtTime(5)).to.be.closeTo(1, 0.01);
249
+ source.start(1).pause(2);
250
+ expect(source.getTicksAtTime(1)).to.be.closeTo(0, 0.01);
251
+ expect(source.getTicksAtTime(2)).to.be.closeTo(1, 0.01);
252
+ expect(source.getTicksAtTime(3)).to.be.closeTo(1, 0.01);
253
+ expect(source.getTicksAtTime(4)).to.be.closeTo(2, 0.01);
254
+ expect(source.getTicksAtTime(5)).to.be.closeTo(2, 0.01);
255
+ source.setTicksAtTime(3, 1);
256
+ expect(source.getTicksAtTime(1)).to.be.closeTo(3, 0.01);
257
+ expect(source.getTicksAtTime(2)).to.be.closeTo(4, 0.01);
258
+ expect(source.getTicksAtTime(3)).to.be.closeTo(4, 0.01);
259
+ expect(source.getTicksAtTime(4)).to.be.closeTo(5, 0.01);
260
+ expect(source.getTicksAtTime(5)).to.be.closeTo(5, 0.01);
261
+ source.cancel(4);
262
+ expect(source.getTicksAtTime(1)).to.be.closeTo(3, 0.01);
263
+ expect(source.getTicksAtTime(2)).to.be.closeTo(4, 0.01);
264
+ expect(source.getTicksAtTime(3)).to.be.closeTo(4, 0.01);
265
+ expect(source.getTicksAtTime(4)).to.be.closeTo(5, 0.01);
266
+ expect(source.getTicksAtTime(5)).to.be.closeTo(6, 0.01);
267
+ source.dispose();
268
+ });
240
269
  });
241
270
 
242
271
  context("forEachTickBetween", () => {
@@ -591,6 +620,29 @@ describe("TickSource", () => {
591
620
  expect(source.getSecondsAtTime(1.5)).to.be.closeTo(0, 0.01);
592
621
  source.dispose();
593
622
  });
623
+
624
+ it("will recompute memoized values when events are modified", () => {
625
+ const source = new TickSource(1);
626
+ source.start(3).pause(4);
627
+ expect(source.getSecondsAtTime(1)).to.be.closeTo(0, 0.01);
628
+ expect(source.getSecondsAtTime(2)).to.be.closeTo(0, 0.01);
629
+ expect(source.getSecondsAtTime(3)).to.be.closeTo(0, 0.01);
630
+ expect(source.getSecondsAtTime(4)).to.be.closeTo(1, 0.01);
631
+ expect(source.getSecondsAtTime(5)).to.be.closeTo(1, 0.01);
632
+ source.start(1).pause(2);
633
+ expect(source.getSecondsAtTime(1)).to.be.closeTo(0, 0.01);
634
+ expect(source.getSecondsAtTime(2)).to.be.closeTo(1, 0.01);
635
+ expect(source.getSecondsAtTime(3)).to.be.closeTo(1, 0.01);
636
+ expect(source.getSecondsAtTime(4)).to.be.closeTo(2, 0.01);
637
+ expect(source.getSecondsAtTime(5)).to.be.closeTo(2, 0.01);
638
+ source.cancel(4);
639
+ expect(source.getSecondsAtTime(1)).to.be.closeTo(0, 0.01);
640
+ expect(source.getSecondsAtTime(2)).to.be.closeTo(1, 0.01);
641
+ expect(source.getSecondsAtTime(3)).to.be.closeTo(1, 0.01);
642
+ expect(source.getSecondsAtTime(4)).to.be.closeTo(2, 0.01);
643
+ expect(source.getSecondsAtTime(5)).to.be.closeTo(3, 0.01);
644
+ source.dispose();
645
+ });
594
646
  });
595
647
 
596
648
  context("Frequency", () => {
@@ -3,7 +3,7 @@ import { Seconds, Ticks, Time } from "../type/Units";
3
3
  import { optionsFromArguments } from "../util/Defaults";
4
4
  import { readOnly } from "../util/Interface";
5
5
  import { PlaybackState, StateTimeline, StateTimelineEvent } from "../util/StateTimeline";
6
- import { Timeline } from "../util/Timeline";
6
+ import { Timeline, TimelineEvent } from "../util/Timeline";
7
7
  import { isDefined } from "../util/TypeCheck";
8
8
  import { TickSignal } from "./TickSignal";
9
9
  import { EQ } from "../util/Math";
@@ -13,12 +13,24 @@ interface TickSourceOptions extends ToneWithContextOptions {
13
13
  units: "bpm" | "hertz";
14
14
  }
15
15
 
16
- interface TickSourceOffsetEvent {
16
+ interface TickSourceOffsetEvent extends TimelineEvent {
17
17
  ticks: number;
18
18
  time: number;
19
19
  seconds: number;
20
20
  }
21
21
 
22
+ interface TickSourceTicksAtTimeEvent extends TimelineEvent {
23
+ state: PlaybackState;
24
+ time: number;
25
+ ticks: number;
26
+ }
27
+
28
+ interface TickSourceSecondsAtTimeEvent extends TimelineEvent {
29
+ state: PlaybackState;
30
+ time: number;
31
+ seconds: number;
32
+ }
33
+
22
34
  /**
23
35
  * Uses [TickSignal](TickSignal) to track elapsed ticks with complex automation curves.
24
36
  */
@@ -41,6 +53,16 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
41
53
  */
42
54
  private _tickOffset: Timeline<TickSourceOffsetEvent> = new Timeline();
43
55
 
56
+ /**
57
+ * Memoized values of getTicksAtTime at events with state other than "started"
58
+ */
59
+ private _ticksAtTime: Timeline<TickSourceTicksAtTimeEvent> = new Timeline<TickSourceTicksAtTimeEvent>();
60
+
61
+ /**
62
+ * Memoized values of getSecondsAtTime at events with state other than "started"
63
+ */
64
+ private _secondsAtTime: Timeline<TickSourceSecondsAtTimeEvent> = new Timeline<TickSourceSecondsAtTimeEvent>();
65
+
44
66
  /**
45
67
  * @param frequency The initial frequency that the signal ticks at
46
68
  */
@@ -90,6 +112,8 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
90
112
  if (isDefined(offset)) {
91
113
  this.setTicksAtTime(offset, computedTime);
92
114
  }
115
+ this._ticksAtTime.cancel(computedTime);
116
+ this._secondsAtTime.cancel(computedTime);
93
117
  }
94
118
  return this;
95
119
  }
@@ -111,6 +135,8 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
111
135
  this._state.cancel(computedTime);
112
136
  this._state.setStateAtTime("stopped", computedTime);
113
137
  this.setTicksAtTime(0, computedTime);
138
+ this._ticksAtTime.cancel(computedTime);
139
+ this._secondsAtTime.cancel(computedTime);
114
140
  return this;
115
141
  }
116
142
 
@@ -122,6 +148,8 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
122
148
  const computedTime = this.toSeconds(time);
123
149
  if (this._state.getValueAtTime(computedTime) === "started") {
124
150
  this._state.setStateAtTime("paused", computedTime);
151
+ this._ticksAtTime.cancel(computedTime);
152
+ this._secondsAtTime.cancel(computedTime);
125
153
  }
126
154
  return this;
127
155
  }
@@ -134,6 +162,8 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
134
162
  time = this.toSeconds(time);
135
163
  this._state.cancel(time);
136
164
  this._tickOffset.cancel(time);
165
+ this._ticksAtTime.cancel(time);
166
+ this._secondsAtTime.cancel(time);
137
167
  return this;
138
168
  }
139
169
 
@@ -145,16 +175,21 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
145
175
  getTicksAtTime(time?: Time): Ticks {
146
176
  const computedTime = this.toSeconds(time);
147
177
  const stopEvent = this._state.getLastState("stopped", computedTime) as StateTimelineEvent;
178
+
179
+ // get previously memoized ticks if available
180
+ const memoizedEvent = this._ticksAtTime.get(computedTime);
181
+
148
182
  // this event allows forEachBetween to iterate until the current time
149
183
  const tmpEvent: StateTimelineEvent = { state: "paused", time: computedTime };
150
184
  this._state.add(tmpEvent);
151
185
 
152
186
  // keep track of the previous offset event
153
- let lastState = stopEvent;
154
- let elapsedTicks = 0;
187
+ let lastState = memoizedEvent ? memoizedEvent : stopEvent;
188
+ let elapsedTicks = memoizedEvent ? memoizedEvent.ticks : 0;
189
+ let eventToMemoize : TickSourceTicksAtTimeEvent | null = null;
155
190
 
156
191
  // iterate through all the events since the last stop
157
- this._state.forEachBetween(stopEvent.time, computedTime + this.sampleTime, e => {
192
+ this._state.forEachBetween(lastState.time, computedTime + this.sampleTime, e => {
158
193
  let periodStartTime = lastState.time;
159
194
  // if there is an offset event in this period use that
160
195
  const offsetEvent = this._tickOffset.get(e.time);
@@ -164,6 +199,10 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
164
199
  }
165
200
  if (lastState.state === "started" && e.state !== "started") {
166
201
  elapsedTicks += this.frequency.getTicksAtTime(e.time) - this.frequency.getTicksAtTime(periodStartTime);
202
+ // do not memoize the temporary event
203
+ if (e.time != tmpEvent.time) {
204
+ eventToMemoize = { state: e.state, time: e.time, ticks: elapsedTicks };
205
+ }
167
206
  }
168
207
  lastState = e;
169
208
  });
@@ -171,6 +210,11 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
171
210
  // remove the temporary event
172
211
  this._state.remove(tmpEvent);
173
212
 
213
+ // memoize the ticks at the most recent event with state other than "started"
214
+ if (eventToMemoize) {
215
+ this._ticksAtTime.add(eventToMemoize);
216
+ }
217
+
174
218
  // return the ticks
175
219
  return elapsedTicks;
176
220
  }
@@ -211,12 +255,16 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
211
255
  const tmpEvent: StateTimelineEvent = { state: "paused", time };
212
256
  this._state.add(tmpEvent);
213
257
 
258
+ // get previously memoized seconds if available
259
+ const memoizedEvent = this._secondsAtTime.get(time);
260
+
214
261
  // keep track of the previous offset event
215
- let lastState = stopEvent;
216
- let elapsedSeconds = 0;
262
+ let lastState = memoizedEvent ? memoizedEvent : stopEvent;
263
+ let elapsedSeconds = memoizedEvent ? memoizedEvent.seconds : 0;
264
+ let eventToMemoize : TickSourceSecondsAtTimeEvent | null = null;
217
265
 
218
266
  // iterate through all the events since the last stop
219
- this._state.forEachBetween(stopEvent.time, time + this.sampleTime, e => {
267
+ this._state.forEachBetween(lastState.time, time + this.sampleTime, e => {
220
268
  let periodStartTime = lastState.time;
221
269
  // if there is an offset event in this period use that
222
270
  const offsetEvent = this._tickOffset.get(e.time);
@@ -226,6 +274,10 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
226
274
  }
227
275
  if (lastState.state === "started" && e.state !== "started") {
228
276
  elapsedSeconds += e.time - periodStartTime;
277
+ // do not memoize the temporary event
278
+ if (e.time != tmpEvent.time) {
279
+ eventToMemoize = { state: e.state, time: e.time, seconds: elapsedSeconds };
280
+ }
229
281
  }
230
282
  lastState = e;
231
283
  });
@@ -233,7 +285,12 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
233
285
  // remove the temporary event
234
286
  this._state.remove(tmpEvent);
235
287
 
236
- // return the ticks
288
+ // memoize the seconds at the most recent event with state other than "started"
289
+ if (eventToMemoize) {
290
+ this._secondsAtTime.add(eventToMemoize);
291
+ }
292
+
293
+ // return the seconds
237
294
  return elapsedSeconds;
238
295
  }
239
296
 
@@ -250,6 +307,8 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
250
307
  ticks,
251
308
  time,
252
309
  });
310
+ this._ticksAtTime.cancel(time);
311
+ this._secondsAtTime.cancel(time);
253
312
  return this;
254
313
  }
255
314
 
@@ -332,6 +391,8 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
332
391
  super.dispose();
333
392
  this._state.dispose();
334
393
  this._tickOffset.dispose();
394
+ this._ticksAtTime.dispose();
395
+ this._secondsAtTime.dispose();
335
396
  this.frequency.dispose();
336
397
  return this;
337
398
  }
@@ -16,7 +16,12 @@ export class Ticker {
16
16
  /**
17
17
  * The update interval of the worker
18
18
  */
19
- private _updateInterval: Seconds;
19
+ private _updateInterval!: Seconds;
20
+
21
+ /**
22
+ * The lowest allowable interval, preferably calculated from context sampleRate
23
+ */
24
+ private _minimumUpdateInterval: Seconds;
20
25
 
21
26
  /**
22
27
  * The callback to invoke at regular intervals
@@ -33,11 +38,12 @@ export class Ticker {
33
38
  */
34
39
  private _worker!: Worker;
35
40
 
36
- constructor(callback: () => void, type: TickerClockSource, updateInterval: Seconds) {
41
+ constructor(callback: () => void, type: TickerClockSource, updateInterval: Seconds, contextSampleRate?: number) {
37
42
 
38
43
  this._callback = callback;
39
44
  this._type = type;
40
- this._updateInterval = updateInterval;
45
+ this._minimumUpdateInterval = Math.max( 128/(contextSampleRate || 44100), .001 );
46
+ this.updateInterval = updateInterval;
41
47
 
42
48
  // create the clock source for the first time
43
49
  this._createClock();
@@ -121,9 +127,9 @@ export class Ticker {
121
127
  return this._updateInterval;
122
128
  }
123
129
  set updateInterval(interval: Seconds) {
124
- this._updateInterval = Math.max(interval, 128 / 44100);
130
+ this._updateInterval = Math.max(interval, this._minimumUpdateInterval);
125
131
  if (this._type === "worker") {
126
- this._worker.postMessage(Math.max(interval * 1000, 1));
132
+ this._worker?.postMessage(this._updateInterval * 1000);
127
133
  }
128
134
  }
129
135
 
@@ -75,8 +75,10 @@ describe("Context", () => {
75
75
  clockSource: "timeout",
76
76
  latencyHint: "playback",
77
77
  lookAhead: 0.2,
78
+ updateInterval: 0.1
78
79
  });
79
80
  expect(ctx.lookAhead).to.equal(0.2);
81
+ expect(ctx.updateInterval).to.equal(0.1);
80
82
  expect(ctx.latencyHint).to.equal("playback");
81
83
  expect(ctx.clockSource).to.equal("timeout");
82
84
  ctx.dispose();
@@ -3,7 +3,7 @@ import { Seconds } from "../type/Units";
3
3
  import { isAudioContext } from "../util/AdvancedTypeCheck";
4
4
  import { optionsFromArguments } from "../util/Defaults";
5
5
  import { Timeline } from "../util/Timeline";
6
- import { isDefined, isString } from "../util/TypeCheck";
6
+ import { isDefined } from "../util/TypeCheck";
7
7
  import {
8
8
  AnyAudioContext,
9
9
  createAudioContext,
@@ -39,13 +39,6 @@ export interface ContextTimeoutEvent {
39
39
  export class Context extends BaseContext {
40
40
  readonly name: string = "Context";
41
41
 
42
- /**
43
- * The amount of time into the future events are scheduled. Giving Web Audio
44
- * a short amount of time into the future to schedule events can reduce clicks and
45
- * improve performance. This value can be set to 0 to get the lowest latency.
46
- */
47
- lookAhead: Seconds;
48
-
49
42
  /**
50
43
  * private reference to the BaseAudioContext
51
44
  */
@@ -116,16 +109,20 @@ export class Context extends BaseContext {
116
109
 
117
110
  if (options.context) {
118
111
  this._context = options.context;
112
+ // custom context provided, latencyHint unknown (unless explicitly provided in options)
113
+ this._latencyHint = arguments[0]?.latencyHint || "";
119
114
  } else {
120
115
  this._context = createAudioContext({
121
116
  latencyHint: options.latencyHint,
122
117
  });
118
+ this._latencyHint = options.latencyHint;
123
119
  }
124
120
 
125
121
  this._ticker = new Ticker(
126
122
  this.emit.bind(this, "tick"),
127
123
  options.clockSource,
128
- options.updateInterval
124
+ options.updateInterval,
125
+ this._context.sampleRate
129
126
  );
130
127
  this.on("tick", this._timeoutLoop.bind(this));
131
128
 
@@ -133,9 +130,9 @@ export class Context extends BaseContext {
133
130
  this._context.onstatechange = () => {
134
131
  this.emit("statechange", this.state);
135
132
  };
136
-
137
- this._setLatencyHint(options.latencyHint);
138
- this.lookAhead = options.lookAhead;
133
+
134
+ // if no custom updateInterval provided, updateInterval will be derived by lookAhead setter
135
+ this[arguments[0]?.hasOwnProperty("updateInterval") ? "_lookAhead" : "lookAhead"] = options.lookAhead;
139
136
  }
140
137
 
141
138
  static getDefaults(): ContextOptions {
@@ -391,8 +388,9 @@ export class Context extends BaseContext {
391
388
  /**
392
389
  * How often the interval callback is invoked.
393
390
  * This number corresponds to how responsive the scheduling
394
- * can be. context.updateInterval + context.lookAhead gives you the
395
- * total latency between scheduling an event and hearing it.
391
+ * can be. Setting to 0 will result in the lowest practial interval
392
+ * based on context properties. context.updateInterval + context.lookAhead
393
+ * gives you the total latency between scheduling an event and hearing it.
396
394
  */
397
395
  get updateInterval(): Seconds {
398
396
  return this._ticker.updateInterval;
@@ -412,6 +410,22 @@ export class Context extends BaseContext {
412
410
  this._ticker.type = type;
413
411
  }
414
412
 
413
+ /**
414
+ * The amount of time into the future events are scheduled. Giving Web Audio
415
+ * a short amount of time into the future to schedule events can reduce clicks and
416
+ * improve performance. This value can be set to 0 to get the lowest latency.
417
+ * Adjusting this value also affects the [[updateInterval]].
418
+ */
419
+ get lookAhead(): Seconds {
420
+ return this._lookAhead;
421
+ }
422
+ set lookAhead(time: Seconds) {
423
+ this._lookAhead = time;
424
+ // if lookAhead is 0, default to .01 updateInterval
425
+ this.updateInterval = time ? (time / 2) : .01;
426
+ }
427
+ private _lookAhead!: Seconds;
428
+
415
429
  /**
416
430
  * The type of playback, which affects tradeoffs between audio
417
431
  * output latency and responsiveness.
@@ -431,29 +445,6 @@ export class Context extends BaseContext {
431
445
  return this._latencyHint;
432
446
  }
433
447
 
434
- /**
435
- * Update the lookAhead and updateInterval based on the latencyHint
436
- */
437
- private _setLatencyHint(hint: ContextLatencyHint | Seconds): void {
438
- let lookAheadValue = 0;
439
- this._latencyHint = hint;
440
- if (isString(hint)) {
441
- switch (hint) {
442
- case "interactive":
443
- lookAheadValue = 0.1;
444
- break;
445
- case "playback":
446
- lookAheadValue = 0.5;
447
- break;
448
- case "balanced":
449
- lookAheadValue = 0.25;
450
- break;
451
- }
452
- }
453
- this.lookAhead = lookAheadValue;
454
- this.updateInterval = lookAheadValue / 2;
455
- }
456
-
457
448
  /**
458
449
  * The unwrapped AudioContext or OfflineAudioContext
459
450
  */
@@ -469,7 +460,7 @@ export class Context extends BaseContext {
469
460
  * }, 100);
470
461
  */
471
462
  now(): Seconds {
472
- return this._context.currentTime + this.lookAhead;
463
+ return this._context.currentTime + this._lookAhead;
473
464
  }
474
465
 
475
466
  /**