@weasel-js/audio 1.7.0 → 1.7.2

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/dist/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
+ /** Options for `createAnalyserTap` and `AudioEngine.analyser`. */
1
2
  interface AnalyserTapOptions {
2
3
  /** Power of two, 32..32768. Default 2048. */
3
4
  fftSize?: number;
4
5
  }
6
+ /** An `AnalyserNode` attached to a point in the graph, with readers for its
7
+ * spectrum, waveform and level. Each call reads the current window. */
5
8
  interface AnalyserTap {
6
9
  /** The underlying node, exposed for disposal assertions and advanced wiring. */
7
10
  node: AnalyserNode;
@@ -19,8 +22,67 @@ interface AnalyserTap {
19
22
  /** Detach from the tapped node. Idempotent. */
20
23
  dispose(): void;
21
24
  }
25
+ /** Attach a new `AnalyserNode` to `source`. The tap only listens, adding no
26
+ * path to the destination; `dispose()` detaches it. */
22
27
  declare function createAnalyserTap(ctx: AudioContext, source: AudioNode, opts?: AnalyserTapOptions): AnalyserTap;
23
28
 
29
+ /** A one-shot timer pair, as `AudioEngineOptions.setTimer` takes it. */
30
+ interface Timers {
31
+ setTimer(cb: () => void, ms: number): unknown;
32
+ clearTimer(handle: unknown): void;
33
+ }
34
+
35
+ /**
36
+ * Anything with an input and an output node can sit in an insert slot: one of
37
+ * the built-ins (`createFilterEffect` and the rest), or a pair of nodes of your
38
+ * own — `{ input: shaper, output: shaper }` for a single node. Several nodes
39
+ * between the two are fine too. An effect goes in one slot, once.
40
+ */
41
+ interface InsertEffect {
42
+ readonly input: AudioNode;
43
+ readonly output: AudioNode;
44
+ /** Called once the effect has left the graph after `remove()` or `clear()`. */
45
+ dispose?(): void;
46
+ }
47
+ /** One effect's place in a bus's insert chain. Every edit is click-free: a
48
+ * bypass crossfades the slot's wet and dry, and a structural edit crossfades
49
+ * the old route into the new one. */
50
+ interface InsertSlot {
51
+ readonly effect: InsertEffect;
52
+ /** Route around the effect (`true`) or back through it. */
53
+ bypass(on: boolean): void;
54
+ bypassed(): boolean;
55
+ /** Clamped to the chain. */
56
+ moveTo(index: number): void;
57
+ /** Take the effect out, then call its `dispose`. Idempotent. */
58
+ remove(): void;
59
+ /** Position in the chain, or -1 once removed. */
60
+ index(): number;
61
+ }
62
+ interface InsertAddOptions {
63
+ /** Default: the end of the chain. Clamped. */
64
+ index?: number;
65
+ /** Add it routed around, for a later `bypass(false)`. Default false. */
66
+ bypassed?: boolean;
67
+ }
68
+ /**
69
+ * A bus's ordered insert effects, between where voices connect and the bus's
70
+ * fader. `slots()` is the chain as edited; the audio graph follows one
71
+ * crossfade at a time, and `settled()` resolves once it has caught up.
72
+ */
73
+ interface InsertChain {
74
+ add(effect: InsertEffect, opts?: InsertAddOptions): InsertSlot;
75
+ slots(): InsertSlot[];
76
+ clear(): void;
77
+ settled(): Promise<void>;
78
+ }
79
+ interface InsertChainOptions extends Partial<Timers> {
80
+ /** Crossfade length for every edit, in ms. Default 15. */
81
+ fadeMs?: number;
82
+ }
83
+
84
+ /** Controls for one named bus. `rampMs` slews a gain change over that many
85
+ * ms instead of stepping it. */
24
86
  interface BusHandle {
25
87
  setGain(value: number, rampMs?: number): void;
26
88
  mute(on: boolean): void;
@@ -34,13 +96,23 @@ interface BusHandle {
34
96
  /** Whether the bus is being heard: unmuted, and soloed if any bus is soloed.
35
97
  * Not derivable from `muted()` and `soloed()` alone. */
36
98
  audible(): boolean;
99
+ /** Effects between where the bus's voices connect and its fader. */
100
+ readonly inserts: InsertChain;
37
101
  }
102
+ /** The mix graph built by `createBusGraph`. `input(name)` is where a voice
103
+ * connects; it runs through the bus's inserts to `node(name)`, the fader,
104
+ * which feeds `master`, which feeds the destination. Unknown bus names throw. */
38
105
  interface BusGraph {
39
106
  master: GainNode;
107
+ input(name: string): GainNode;
40
108
  node(name: string): GainNode;
41
109
  bus(name: string): BusHandle;
42
110
  names(): string[];
111
+ /** Cancel every insert transition in flight. The nodes stay wired. */
112
+ dispose(): void;
43
113
  }
114
+ /** Options for `createBusGraph`: the insert chains' crossfade and timer. */
115
+ type BusGraphOptions = InsertChainOptions;
44
116
  /**
45
117
  * The mix graph: one `GainNode` per named bus, all routed to a master that
46
118
  * routes to the destination.
@@ -51,12 +123,13 @@ interface BusGraph {
51
123
  * `writeParam`, so `rampMs` survives instead of being overwritten by the
52
124
  * recomputation that follows it.
53
125
  */
54
- declare function createBusGraph(ctx: AudioContext, names: string[]): BusGraph;
126
+ declare function createBusGraph(ctx: AudioContext, names: string[], opts?: BusGraphOptions): BusGraph;
55
127
 
56
128
  /** Opaque reference to a decoded sound. Mirrors `TextureHandle` in core. */
57
129
  interface SoundHandle {
58
130
  readonly id: string;
59
131
  }
132
+ /** Decoded buffers behind opaque `SoundHandle`s. Nothing is ever evicted. */
60
133
  interface SoundCache {
61
134
  load(url: string): Promise<SoundHandle>;
62
135
  loadAll(urls: Record<string, string>): Promise<Record<string, SoundHandle>>;
@@ -66,12 +139,18 @@ interface SoundCache {
66
139
  register(buffer: AudioBuffer): SoundHandle;
67
140
  buffer(handle: SoundHandle): AudioBuffer | null;
68
141
  }
142
+ /** Create a cache that decodes through `ctx`. `load` fetches with `fetchFn`,
143
+ * returns the existing handle for a url already loaded, and shares one fetch
144
+ * between concurrent loads of it; a failed load is not cached. */
69
145
  declare function createSoundCache(ctx: AudioContext, fetchFn?: typeof fetch): SoundCache;
70
146
 
147
+ /** A 2D world position, in the consumer's own units. */
71
148
  interface Vec2 {
72
149
  x: number;
73
150
  y: number;
74
151
  }
152
+ /** Distance and pan model for `spatialize`, in the same units as the
153
+ * positions. */
75
154
  interface SpatialOptions {
76
155
  /** Distance within which gain stays at 1. Default 1. The inverse model needs
77
156
  * this above 0: at 0 it cliffs gain from 1 to 0 at any nonzero distance. */
@@ -96,19 +175,64 @@ declare function spatialize(source: Vec2, listener: Vec2, opts?: SpatialOptions)
96
175
  pan: number;
97
176
  };
98
177
 
178
+ /** An ADSR envelope. Times are ms; `sustain` is a level, 0..1, of the peak. */
179
+ interface Envelope {
180
+ /** Default 5 — long enough that the onset does not click. */
181
+ attack?: number;
182
+ /** Default 0. */
183
+ decay?: number;
184
+ /** Default 1. */
185
+ sustain?: number;
186
+ /** Default 30. */
187
+ release?: number;
188
+ }
189
+ type ResolvedEnvelope = Required<Envelope>;
190
+ interface EnvelopePoint {
191
+ /** ms from the note's start. */
192
+ at: number;
193
+ value: number;
194
+ }
195
+ declare function resolveEnvelope(env?: Envelope): ResolvedEnvelope;
196
+ /** The level `t` ms after the start. With `gate`, the release begins that many
197
+ * ms in, from whatever level the envelope had reached. */
198
+ declare function envelopeLevel(e: ResolvedEnvelope, t: number, gate?: number): number;
199
+ /**
200
+ * Breakpoints joined by linear ramps. Without `gate` they end at the sustain,
201
+ * held until something releases the note; with it they end at zero, `gate +
202
+ * release` ms in.
203
+ */
204
+ declare function envelopePoints(e: ResolvedEnvelope, gate?: number): EnvelopePoint[];
205
+
206
+ /** A pitch: hertz as a number, a scientific pitch name (`'A4'`, `'C#3'`,
207
+ * `'Bb2'`), or a MIDI note number wrapped as `{ midi }` so it cannot be read
208
+ * as hertz. */
209
+ type Pitch = number | string | {
210
+ midi: number;
211
+ };
212
+ /** Equal temperament, A4 = MIDI 69 = 440 Hz. Fractional notes are cents. */
213
+ declare function midiToFrequency(midi: number): number;
214
+ /** MIDI number of a scientific pitch name: C4 is 60. Accepts any run of `#`
215
+ * or `b` after the letter. */
216
+ declare function noteToMidi(name: string): number;
217
+ declare function toFrequency(pitch: Pitch): number;
218
+
219
+ /** Which voice a full pool evicts: the earliest started, or the lowest gain. */
99
220
  type StealPolicy = 'oldest' | 'quietest';
221
+ /** Options for `createVoicePool`. */
100
222
  interface VoicePoolOptions {
101
223
  /** Maximum concurrent voices, at least 1. Beyond this, `acquire` steals. */
102
224
  limit: number;
103
225
  /** Default 'oldest'. */
104
226
  steal?: StealPolicy;
105
227
  }
228
+ /** What the pool knows about a voice, for choosing one to steal. */
106
229
  interface VoiceRecord {
107
230
  /** Engine time the voice started, in ms. */
108
231
  startedAt: number;
109
232
  /** Current gain, consulted by the 'quietest' steal policy. */
110
233
  gain: number;
111
234
  }
235
+ /** The result of `VoicePool.acquire`. */
112
236
  interface Acquisition {
113
237
  slot: number;
114
238
  /** Identifies this voice, not its slot: a stolen slot is reissued at once,
@@ -118,6 +242,7 @@ interface Acquisition {
118
242
  * responsible for actually stopping that voice's nodes. */
119
243
  stolen: number | null;
120
244
  }
245
+ /** Slot accounting for concurrent voices. See `createVoicePool`. */
121
246
  interface VoicePool {
122
247
  acquire(record: VoiceRecord): Acquisition;
123
248
  release(slot: number, token: number): void;
@@ -135,6 +260,7 @@ interface VoicePool {
135
260
  */
136
261
  declare function createVoicePool(opts: VoicePoolOptions): VoicePool;
137
262
 
263
+ /** Options for `AudioEngine.play`. */
138
264
  interface PlayOptions {
139
265
  /** Default: the first configured bus. */
140
266
  bus?: string;
@@ -155,10 +281,16 @@ interface PlayOptions {
155
281
  cancelKey?: string;
156
282
  onDone?: () => void;
157
283
  }
284
+ /** Controls for one voice from `play()`. Safe to keep past the voice's end:
285
+ * every setter is then a no-op and `isPlaying()` is false. */
158
286
  interface VoiceHandle {
159
287
  id: number;
160
288
  /** `fadeMs` ramps the voice out and stops it at the end of the ramp. */
161
289
  stop(fadeMs?: number): void;
290
+ /** Note-off: a synth voice runs its envelope's release from whatever level it
291
+ * has reached. A buffer voice has no envelope, so this is `stop()`. Either
292
+ * one released before it starts is canceled. */
293
+ release(): void;
162
294
  setGain(value: number, rampMs?: number): void;
163
295
  /** Playback rate. Applies to a voice booked for a future `when` too. */
164
296
  setRate(value: number): void;
@@ -169,6 +301,110 @@ interface VoiceHandle {
169
301
  setPosition(p: Vec2): void;
170
302
  isPlaying(): boolean;
171
303
  }
304
+ /** One sine partial of an inharmonic voice. */
305
+ interface SynthPartial {
306
+ /** Frequency as a multiple of the note's pitch. Any positive number: this is
307
+ * what reaches off the harmonic series. */
308
+ ratio: number;
309
+ /** Relative level, default 1. The voice's partials are scaled so their levels
310
+ * sum to 1, so the peak cannot pass the note's gain. */
311
+ gain?: number;
312
+ /** Exponential decay time constant in ms: the partial falls to 1/e of its
313
+ * level every `decay` ms, under the note's envelope. Omit to hold it. */
314
+ decay?: number;
315
+ }
316
+ /** A voice built from sine oscillators at arbitrary ratios of the pitch, each
317
+ * with its own level and decay: a bell, a bar, struck metal. */
318
+ interface Inharmonic {
319
+ partials: readonly SynthPartial[];
320
+ }
321
+ /** An oscillator's waveform: one of Web Audio's built-in shapes; the
322
+ * amplitudes of harmonic partials — `[1, 0.5, 0.25]` is the fundamental at
323
+ * full level, the second harmonic at half and the third at a quarter, normalized
324
+ * so only their ratios matter; or `{ partials }`, sines at any ratio. */
325
+ type Waveform = 'sine' | 'square' | 'sawtooth' | 'triangle' | readonly number[] | Inharmonic;
326
+ /** A `BiquadFilterNode` inside a synth voice, ahead of its amplitude envelope,
327
+ * with an envelope of its own on the cutoff. */
328
+ interface VoiceFilter {
329
+ /** Default 'lowpass'. */
330
+ type?: BiquadFilterType;
331
+ /** Cutoff or center in Hz, where the frequency envelope starts and ends. */
332
+ frequency: number;
333
+ /** Default 1. */
334
+ Q?: number;
335
+ /** dB, for the shelf and peaking types. */
336
+ gain?: number;
337
+ /** Shapes the cutoff: it sits at `frequency + amount × level`, `level` being
338
+ * this envelope's 0..1. It shares the note's gate and is released with it.
339
+ * A thump is a lowpass with a positive `amount` and a short decay to
340
+ * `sustain: 0`. Default: the cutoff holds at `frequency`. */
341
+ envelope?: Envelope;
342
+ /** Hz added at the envelope's peak; negative sweeps the cutoff down. Default 0. */
343
+ amount?: number;
344
+ }
345
+ /** The three noise colors: white is flat, pink falls 3 dB an octave, brown 6. */
346
+ type NoiseColor = 'white' | 'pink' | 'brown';
347
+ /** A pitch sweep from the note's own pitch. */
348
+ interface Glide {
349
+ to: Pitch;
350
+ /** Default: the note's `duration`, or 100 ms for a held note. */
351
+ ms?: number;
352
+ /** Default 'exponential', which moves evenly in pitch; 'linear' moves evenly
353
+ * in hertz. */
354
+ curve?: 'linear' | 'exponential';
355
+ }
356
+ /** The timbre of a note, apart from its pitch — spread one into several
357
+ * `playNote` calls to reuse it. */
358
+ interface SynthPatch {
359
+ /** Default 'sine'. */
360
+ wave?: Waveform;
361
+ envelope?: Envelope;
362
+ glide?: Glide;
363
+ filter?: VoiceFilter;
364
+ }
365
+ /** Options for `AudioEngine.playNote`. Routing, level, position, timing and
366
+ * `cancelKey` mean what they mean for `play()`. */
367
+ interface NoteOptions extends SynthPatch, Omit<PlayOptions, 'rate' | 'loop'> {
368
+ pitch: Pitch;
369
+ /** Gate length in ms: how long the note sounds before its release begins.
370
+ * Omit to hold it until `release()`. */
371
+ duration?: number;
372
+ }
373
+ /** The timbre of a noise voice — spread one into several `playNoise` calls to
374
+ * reuse it. */
375
+ interface NoisePatch {
376
+ noise: NoiseColor;
377
+ envelope?: Envelope;
378
+ filter?: VoiceFilter;
379
+ }
380
+ /** Options for `AudioEngine.playNoise`. Everything but the source means what it
381
+ * means for `playNote`; `rate` and `detune` retune the looping noise buffer,
382
+ * which shifts a pink or brown noise's color, and leave the filter where it is. */
383
+ interface NoiseOptions extends NoisePatch, Omit<PlayOptions, 'loop'> {
384
+ /** Gate length in ms. Omit to hold the noise until `release()`. */
385
+ duration?: number;
386
+ }
387
+ /**
388
+ * Options for `AudioEngine.stream`. Routing, level, position, `rate`, `loop`,
389
+ * `cancelKey` and `onDone` mean what they mean for `play()`, with these
390
+ * differences, all from playing through a media element rather than a buffer:
391
+ *
392
+ * - No `when`: the element starts once it has buffered enough, which is not
393
+ * sample-accurate and cannot be booked against the audio clock.
394
+ * - No `detune`, and `setDetune` does nothing. `rate` is the element's
395
+ * `playbackRate`, which keeps pitch unless the element's `preservesPitch`
396
+ * is false.
397
+ * - The stream counts toward its bus's voice limit and can be stolen like any
398
+ * voice — give music a bus of its own.
399
+ */
400
+ interface StreamOptions extends Omit<PlayOptions, 'when' | 'detune'> {
401
+ /** Where to start, in ms into the media. Default: wherever the element is. */
402
+ offset?: number;
403
+ /** Set on an element made from a URL. A cross-origin URL needs CORS and
404
+ * `'anonymous'`, or the graph receives silence. */
405
+ crossOrigin?: '' | 'anonymous' | 'use-credentials';
406
+ }
407
+ /** Options for `createAudioEngine`. */
172
408
  interface AudioEngineOptions {
173
409
  /** Injectable for tests and for consumers that own the context. */
174
410
  context?: AudioContext;
@@ -188,8 +424,13 @@ interface AudioEngineOptions {
188
424
  * replaces the default, and the missing half falls back to `setTimeout`'s. */
189
425
  setTimer?: (cb: () => void, ms: number) => unknown;
190
426
  clearTimer?: (handle: unknown) => void;
427
+ /** Crossfade for every insert-chain edit and bypass, in ms. Default 15. */
428
+ insertFadeMs?: number;
191
429
  }
192
430
 
431
+ /** A sound engine: named mix buses, a per-bus voice pool, lookahead
432
+ * scheduling and 2D spatialization over one `AudioContext`. Times are engine
433
+ * ms (`now()`). */
193
434
  interface AudioEngine {
194
435
  /** The engine's `AudioContext` — the one passed as `options.context`, or the
195
436
  * one the engine created. Use it for `createBuffer`, for analysis, or for a
@@ -211,6 +452,25 @@ interface AudioEngine {
211
452
  * recording. `load` and `decode` both assume encoded bytes. */
212
453
  register(buffer: AudioBuffer): SoundHandle;
213
454
  play(sound: SoundHandle, opts?: PlayOptions): VoiceHandle;
455
+ /** Play a synthesized note: an oscillator with harmonic partials, or summed
456
+ * sines at any ratios, under an ADSR envelope and an optional filter. It is a voice like any other — same handle, same bus pool
457
+ * and stealing, same `cancelKey`. */
458
+ playNote(note: NoteOptions): VoiceHandle;
459
+ /** Play noise — white, pink or brown, from a looping buffer made once per
460
+ * context — under an envelope and an optional filter with its own cutoff
461
+ * envelope. A voice like `playNote`'s in every other respect. */
462
+ playNoise(noise: NoiseOptions): VoiceHandle;
463
+ /** Stream long audio — music, ambience — from a media element or a URL
464
+ * through a `MediaElementAudioSourceNode`, instead of decoding it whole. It
465
+ * is a voice on a bus like any other, with the exceptions `StreamOptions`
466
+ * lists. An element has one playhead, so streaming one that is already
467
+ * streaming stops the earlier voice. */
468
+ stream(media: HTMLMediaElement | string, opts?: StreamOptions): VoiceHandle;
469
+ /** Book a callback against the audio clock through the engine's lookahead
470
+ * scheduler; it receives its own `when` to hand on to `play` or `playNote`.
471
+ * `stopKey(key)` cancels it. One that came due during a suspension fires
472
+ * late on resume, with its original `when`, so compare against `now()`. */
473
+ schedule(when: number, fire: (when: number) => void, key?: string): void;
214
474
  stopKey(key: string): void;
215
475
  stopAll(): void;
216
476
  bus(name: string): BusHandle;
@@ -223,8 +483,92 @@ interface AudioEngine {
223
483
  setListener(p: Vec2, opts?: SpatialOptions): void;
224
484
  dispose(): void;
225
485
  }
486
+ /**
487
+ * Create an engine. Browsers start the context suspended: `play()` before it
488
+ * is unlocked drops the voice with a warning. The engine resumes it on the
489
+ * first pointer, key or touch gesture, or call `unlock()` from one. Call
490
+ * `dispose()` when done.
491
+ */
226
492
  declare function createAudioEngine(opts?: AudioEngineOptions): AudioEngine;
227
493
 
494
+ interface FilterEffectOptions {
495
+ /** Default 'lowpass'. */
496
+ type?: BiquadFilterType;
497
+ /** Hz. Default 350, the node's own. */
498
+ frequency?: number;
499
+ Q?: number;
500
+ /** dB, for the shelf and peaking types. */
501
+ gain?: number;
502
+ }
503
+ interface FilterEffect extends InsertEffect {
504
+ readonly filter: BiquadFilterNode;
505
+ }
506
+ /** A `BiquadFilterNode`. Write its params through `filter` to sweep it. */
507
+ declare function createFilterEffect(ctx: BaseAudioContext, opts?: FilterEffectOptions): FilterEffect;
508
+ /** A wet/dry mix around an effect, for the effects whose output replaces
509
+ * rather than colors the signal. */
510
+ interface WetDryMix {
511
+ /** 0 is all dry, 1 all wet. */
512
+ mix(): number;
513
+ setMix(mix: number, rampMs?: number): void;
514
+ }
515
+ interface ReverbEffectOptions {
516
+ /** The room: an impulse response, loaded or synthesized like any buffer. */
517
+ impulse: AudioBuffer;
518
+ /** Default 0.3. */
519
+ mix?: number;
520
+ /** Scale the impulse to a consistent level. Default true, the node's own. */
521
+ normalize?: boolean;
522
+ }
523
+ interface ReverbEffect extends InsertEffect, WetDryMix {
524
+ readonly convolver: ConvolverNode;
525
+ }
526
+ /** Convolution reverb from an impulse buffer, with a wet/dry mix. */
527
+ declare function createReverbEffect(ctx: BaseAudioContext, opts: ReverbEffectOptions): ReverbEffect;
528
+ interface DelayEffectOptions {
529
+ /** ms. Default 250. */
530
+ time?: number;
531
+ /** How much of each repeat feeds the next, -1..1 exclusive. Default 0.35. */
532
+ feedback?: number;
533
+ /** Default 0.35. */
534
+ mix?: number;
535
+ /** The longest `time` the line can be set to later, ms. Default the larger
536
+ * of 1000 and `time`. */
537
+ maxTime?: number;
538
+ }
539
+ interface DelayEffect extends InsertEffect, WetDryMix {
540
+ readonly delay: DelayNode;
541
+ setTime(ms: number, rampMs?: number): void;
542
+ feedback(): number;
543
+ setFeedback(value: number, rampMs?: number): void;
544
+ /** Break the feedback loop. Called for you when the slot is removed. */
545
+ dispose(): void;
546
+ }
547
+ /** A feedback delay with a wet/dry mix. Ramping `setTime` bends the pitch of
548
+ * the repeats while it moves, as a tape delay does. */
549
+ declare function createDelayEffect(ctx: BaseAudioContext, opts?: DelayEffectOptions): DelayEffect;
550
+ interface CompressorEffectOptions {
551
+ /** dB. */
552
+ threshold?: number;
553
+ /** dB. */
554
+ knee?: number;
555
+ ratio?: number;
556
+ /** ms. */
557
+ attack?: number;
558
+ /** ms. */
559
+ release?: number;
560
+ /** Linear gain after the compressor, to win back the level it takes. Default 1. */
561
+ makeup?: number;
562
+ }
563
+ interface CompressorEffect extends InsertEffect {
564
+ readonly compressor: DynamicsCompressorNode;
565
+ readonly makeup: GainNode;
566
+ }
567
+ /** A `DynamicsCompressorNode` into a makeup gain. Unset options keep the
568
+ * node's defaults. */
569
+ declare function createCompressorEffect(ctx: BaseAudioContext, opts?: CompressorEffectOptions): CompressorEffect;
570
+
571
+ /** Clock and timer for `createScheduler`. */
228
572
  interface SchedulerOptions {
229
573
  /** Engine time in ms. Backed by `AudioContext.currentTime * 1000` in production. */
230
574
  now: () => number;
@@ -237,6 +581,7 @@ interface SchedulerOptions {
237
581
  /** Time between passes, in ms. Default 25. */
238
582
  interval?: number;
239
583
  }
584
+ /** A queue of callbacks keyed to engine time. See `createScheduler`. */
240
585
  interface Scheduler {
241
586
  start(): void;
242
587
  /** Stop the timer. The queue survives; `clear()` empties it. */
@@ -270,6 +615,7 @@ interface Scheduler {
270
615
  */
271
616
  declare function createScheduler(opts: SchedulerOptions): Scheduler;
272
617
 
618
+ /** The timer pair `createTickTimer` returns, shaped for `SchedulerOptions`. */
273
619
  interface TickTimer {
274
620
  /** One-shot, as `createScheduler` expects. */
275
621
  setTimer(cb: () => void, ms: number): unknown;
@@ -286,4 +632,97 @@ interface TickTimer {
286
632
  */
287
633
  declare function createTickTimer(): TickTimer;
288
634
 
289
- export { type Acquisition, type AnalyserTap, type AnalyserTapOptions, type AudioEngine, type AudioEngineOptions, type BusGraph, type BusHandle, type PlayOptions, type Scheduler, type SchedulerOptions, type SoundCache, type SoundHandle, type SpatialOptions, type StealPolicy, type TickTimer, type Vec2, type VoiceHandle, type VoicePool, type VoicePoolOptions, type VoiceRecord, createAnalyserTap, createAudioEngine, createBusGraph, createScheduler, createSoundCache, createTickTimer, createVoicePool, spatialize };
635
+ /** A synth note on a step. `length` is its gate in steps, default 1. */
636
+ interface PatternNote extends Omit<NoteOptions, 'when' | 'duration'> {
637
+ step: number;
638
+ length?: number;
639
+ }
640
+ /** Noise on a step: a hi-hat, a snare's rattle. `length` is its gate in steps,
641
+ * default 1. */
642
+ interface PatternNoise extends Omit<NoiseOptions, 'when' | 'duration'> {
643
+ step: number;
644
+ length?: number;
645
+ }
646
+ /** A buffer played on a step: a drum hit, a sample. */
647
+ interface PatternHit extends Omit<PlayOptions, 'when'> {
648
+ step: number;
649
+ sound: SoundHandle;
650
+ }
651
+ type PatternEvent = PatternNote | PatternNoise | PatternHit;
652
+ /** Options for `createPatternPlayer`. */
653
+ interface PatternPlayerOptions {
654
+ /** Beats per minute. */
655
+ tempo: number;
656
+ /** Steps per beat. Default 4, so a step is a sixteenth note in 4/4. */
657
+ stepsPerBeat?: number;
658
+ events?: readonly PatternEvent[];
659
+ /** Steps in one pass. Default: through the last event, rounded up to a whole
660
+ * beat. */
661
+ length?: number;
662
+ /** Default true. */
663
+ loop?: boolean;
664
+ /** Called as each step is booked — up to a lookahead ahead of it sounding —
665
+ * with its index in the pattern and its engine time in ms. */
666
+ onStep?: (step: number, when: number) => void;
667
+ }
668
+ /** A running sequence of steps. See `createPatternPlayer`. */
669
+ interface PatternPlayer {
670
+ /** Start from step 0 at engine time `when`, default now. No-op while playing. */
671
+ start(when?: number): void;
672
+ /** Book nothing further, and release every voice the player started. */
673
+ stop(): void;
674
+ playing(): boolean;
675
+ tempo(): number;
676
+ /** Takes effect from the next step not yet booked. */
677
+ setTempo(bpm: number): void;
678
+ /** Takes effect from the next step not yet booked. */
679
+ setEvents(events: readonly PatternEvent[], length?: number): void;
680
+ }
681
+ /**
682
+ * A step sequencer over `engine`. Each step is one `engine.schedule` booking;
683
+ * when it fires, it books that step's events at the step's exact time and then
684
+ * books the step after it. So nothing is booked further ahead than the
685
+ * scheduler's lookahead, which is what lets a tempo change or new events land
686
+ * on the very next step.
687
+ *
688
+ * A step that fires more than a step late — a stalled timer, a suspended
689
+ * context — skips to the next step still ahead of the clock, keeping phase,
690
+ * rather than playing the missed ones at once.
691
+ */
692
+ declare function createPatternPlayer(engine: AudioEngine, opts: PatternPlayerOptions): PatternPlayer;
693
+
694
+ /** Samples of `color` noise, seeded so every context gets the same loop. The
695
+ * end is pulled onto the start, so the loop point does not click. */
696
+ declare function noiseSamples(color: NoiseColor, length: number): Float32Array;
697
+ /**
698
+ * Ready-made inharmonic waves. The ratios are the textbook modes of each
699
+ * object; the levels and decays are a starting point to spread and adjust.
700
+ */
701
+ declare const partialPresets: {
702
+ /** A church bell's hum, prime, tierce, quint and nominal, and above. */
703
+ bell: {
704
+ partials: {
705
+ ratio: number;
706
+ gain: number;
707
+ decay: number;
708
+ }[];
709
+ };
710
+ /** A tuned bar: its overtones sit near 4 and 10 times the fundamental and die fast. */
711
+ marimba: {
712
+ partials: {
713
+ ratio: number;
714
+ gain: number;
715
+ decay: number;
716
+ }[];
717
+ };
718
+ /** Struck sheet metal: modes nowhere near the harmonic series. */
719
+ metal: {
720
+ partials: {
721
+ ratio: number;
722
+ gain: number;
723
+ decay: number;
724
+ }[];
725
+ };
726
+ };
727
+
728
+ export { type Acquisition, type AnalyserTap, type AnalyserTapOptions, type AudioEngine, type AudioEngineOptions, type BusGraph, type BusGraphOptions, type BusHandle, type CompressorEffect, type CompressorEffectOptions, type DelayEffect, type DelayEffectOptions, type Envelope, type EnvelopePoint, type FilterEffect, type FilterEffectOptions, type Glide, type Inharmonic, type InsertAddOptions, type InsertChain, type InsertChainOptions, type InsertEffect, type InsertSlot, type NoiseColor, type NoiseOptions, type NoisePatch, type NoteOptions, type PatternEvent, type PatternHit, type PatternNoise, type PatternNote, type PatternPlayer, type PatternPlayerOptions, type Pitch, type PlayOptions, type ResolvedEnvelope, type ReverbEffect, type ReverbEffectOptions, type Scheduler, type SchedulerOptions, type SoundCache, type SoundHandle, type SpatialOptions, type StealPolicy, type StreamOptions, type SynthPartial, type SynthPatch, type TickTimer, type Vec2, type VoiceFilter, type VoiceHandle, type VoicePool, type VoicePoolOptions, type VoiceRecord, type Waveform, type WetDryMix, createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createPatternPlayer, createReverbEffect, createScheduler, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, resolveEnvelope, spatialize, toFrequency };