smplr 0.17.1 → 0.18.0

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/README.md CHANGED
@@ -328,6 +328,178 @@ const piano = new SplendidGrandPiano(context, { storage });
328
328
 
329
329
  ⚠️ `CacheStorage` is based on [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache) and only works in secure environments that runs with `https`. Read your framework documentation for setup instructions. For example, in nextjs you can use https://www.npmjs.com/package/next-dev-https. For vite there's https://github.com/liuweiGL/vite-plugin-mkcert. Find the appropriate solution for your environment.
330
330
 
331
+ ## Sequencer
332
+
333
+ `Sequencer` schedules notes from one or more tracks against any smplr instrument with sample-accurate timing.
334
+
335
+ ```js
336
+ import { Sequencer, SplendidGrandPiano, DrumMachine } from "smplr";
337
+
338
+ const context = new AudioContext();
339
+ const piano = new SplendidGrandPiano(context);
340
+ const drums = new DrumMachine(context, { instrument: "TR-808" });
341
+
342
+ const seq = new Sequencer(context, { bpm: 120, loop: true });
343
+
344
+ seq.addTrack(piano, [
345
+ { note: "C4", at: "1:1", duration: "4n" },
346
+ { note: "E4", at: "1:2", duration: "4n" },
347
+ { note: "G4", at: "1:3", duration: "4n" },
348
+ { note: "C5", at: "1:4", duration: "2n" },
349
+ ]);
350
+
351
+ seq.addTrack(drums, [
352
+ { note: "kick", at: "1:1" },
353
+ { note: "snare", at: "1:2" },
354
+ { note: "kick", at: "1:3" },
355
+ { note: "snare", at: "1:4" },
356
+ ]);
357
+
358
+ seq.loopEnd = "2:1"; // 1 bar
359
+ seq.start();
360
+ ```
361
+
362
+ #### Time notation
363
+
364
+ Note positions and durations accept several formats:
365
+
366
+ | Format | Meaning |
367
+ |-------------|--------------------------------------|
368
+ | `"4n"` | quarter note |
369
+ | `"8n"` | eighth note |
370
+ | `"4n."` | dotted quarter (1.5×) |
371
+ | `"1m"` | one measure |
372
+ | `"2:1"` | bar 2, beat 1 (1-indexed) |
373
+ | `"2:3:48"` | bar 2, beat 3, +48 ticks |
374
+ | `96` | raw ticks (number passthrough) |
375
+
376
+ #### Constructor options
377
+
378
+ ```js
379
+ const seq = new Sequencer(context, {
380
+ bpm: 120, // default 120
381
+ ppq: 480, // pulses per quarter note, default 480
382
+ timeSignature: 4, // beats per bar, default 4
383
+ loop: false, // default false
384
+ loopStart: 0, // loop start position (ticks or string)
385
+ loopEnd: "2:1", // loop end position; defaults to end of longest track
386
+ lookaheadMs: 200, // scheduling lookahead, default 200
387
+ intervalMs: 50, // flush interval, default 50
388
+ humanize: { timing: 0.01, velocity: 8 }, // optional randomisation
389
+ });
390
+ ```
391
+
392
+ #### Playback
393
+
394
+ ```js
395
+ seq.start(); // start from beginning (or resume from pause if no offset given)
396
+ seq.pause(); // freeze position
397
+ seq.stop(); // stop and reset to 0
398
+
399
+ seq.state; // "stopped" | "playing" | "paused"
400
+ ```
401
+
402
+ #### Tempo and position
403
+
404
+ ```js
405
+ seq.bpm = 140; // change BPM live, no glitch
406
+ seq.timeSignature = 3; // change time signature
407
+
408
+ seq.position; // current position as "bar:beat:tick" string
409
+ seq.position = "3:1"; // seek while playing or stopped
410
+ ```
411
+
412
+ #### Loop
413
+
414
+ ```js
415
+ seq.loop = true;
416
+ seq.loopStart = "1:1"; // ticks or string notation
417
+ seq.loopEnd = "3:1"; // ticks or string notation
418
+
419
+ seq.progress; // 0..1 within the loop range
420
+ ```
421
+
422
+ #### Pattern API
423
+
424
+ `scheduleRepeat` fires a callback at a regular musical interval, passing the exact AudioContext time:
425
+
426
+ ```js
427
+ const cancel = seq.scheduleRepeat((time) => {
428
+ piano.start({ note: "C4", time, duration: 0.1 });
429
+ }, "8n"); // every eighth note
430
+
431
+ cancel(); // stop repeating
432
+ ```
433
+
434
+ An optional third argument sets the start position:
435
+
436
+ ```js
437
+ seq.scheduleRepeat(callback, "4n", "2:1"); // start at bar 2
438
+ ```
439
+
440
+ #### Events
441
+
442
+ ```js
443
+ seq.on("beat", (beat, time) => {
444
+ const delay = (time - context.currentTime) * 1000;
445
+ setTimeout(() => metronome.flash(), delay);
446
+ });
447
+
448
+ seq.on("bar", (bar, time) => { ui.updateBar(bar); });
449
+ seq.on("loop", () => { console.log("looped"); });
450
+ seq.on("end", () => { console.log("done"); });
451
+ seq.on("start", () => { });
452
+ seq.on("stop", () => { });
453
+ seq.on("pause", () => { });
454
+
455
+ seq.off("beat", handler); // remove a listener
456
+ ```
457
+
458
+ #### Note events
459
+
460
+ `noteOn` and `noteOff` events fire when the instrument's `onStart` / `onEnded` callbacks are called, so they are driven by the actual audio playback — not by the scheduling lookahead.
461
+
462
+ ```js
463
+ seq.on("noteOn", (event) => {
464
+ console.log(event.noteId, event.trackIndex, event.noteIndex);
465
+ highlight(event.noteId);
466
+ });
467
+ seq.on("noteOff", (event) => {
468
+ unhighlight(event.noteId);
469
+ });
470
+ ```
471
+
472
+ The `event` object (`NoteEvent`) contains:
473
+
474
+ | Field | Type | Description |
475
+ |--------------|--------------------|--------------------------------------------------|
476
+ | `noteId` | `string \| number` | The note's `id` if provided, otherwise its array index |
477
+ | `trackIndex` | `number` | Index of the track in the order it was added |
478
+ | `noteIndex` | `number` | Index of the note within its track's notes array |
479
+ | `note` | `SequencerNote` | The original note object |
480
+
481
+ You can set a custom `id` on any `SequencerNote` to use as `noteId`:
482
+
483
+ ```js
484
+ seq.addTrack(piano, [
485
+ { id: "intro-c", note: "C4", at: "1:1", duration: "4n" },
486
+ { id: "intro-e", note: "E4", at: "1:2", duration: "4n" },
487
+ ]);
488
+ ```
489
+
490
+ #### Humanize
491
+
492
+ Add subtle randomisation to timing (seconds) and velocity for a more natural feel:
493
+
494
+ ```js
495
+ const seq = new Sequencer(context, {
496
+ bpm: 90,
497
+ humanize: { timing: 0.012, velocity: 8 },
498
+ });
499
+ ```
500
+
501
+ ---
502
+
331
503
  ## Instruments
332
504
 
333
505
  ### Sampler
package/dist/index.d.mts CHANGED
@@ -122,7 +122,7 @@ type SmplrJson = {
122
122
  /**
123
123
  * A note event passed to Smplr.start(). Can be a full object, a note name, or a MIDI number.
124
124
  */
125
- type NoteEvent = {
125
+ type NoteEvent$1 = {
126
126
  note: string | number;
127
127
  velocity?: number;
128
128
  time?: number;
@@ -132,8 +132,8 @@ type NoteEvent = {
132
132
  loop?: boolean;
133
133
  ampRelease?: number;
134
134
  stopId?: string | number;
135
- onStart?: (event: NoteEvent) => void;
136
- onEnded?: (event: NoteEvent) => void;
135
+ onStart?: (event: NoteEvent$1) => void;
136
+ onEnded?: (event: NoteEvent$1) => void;
137
137
  } | string | number;
138
138
  /**
139
139
  * Target for Smplr.stop(). Can be a full object, a stopId, or a MIDI number.
@@ -199,7 +199,7 @@ declare class DrumMachine {
199
199
  getSampleNames(): string[];
200
200
  getGroupNames(): string[];
201
201
  getSampleNamesForGroup(groupName: string): string[];
202
- start(sample: NoteEvent): StopFn;
202
+ start(sample: NoteEvent$1): StopFn;
203
203
  stop(sample?: StopTarget): void;
204
204
  disconnect(): void;
205
205
  /** @deprecated */
@@ -218,6 +218,178 @@ declare class DrumMachine {
218
218
  */
219
219
  declare function drumMachineToSmplrJson(instrument: DrumMachineInstrument): SmplrJson;
220
220
 
221
+ /**
222
+ * TransportClock
223
+ *
224
+ * Converts between musical ticks and AudioContext time with support for:
225
+ * - BPM changes mid-playback (via checkpoints)
226
+ * - Pause / resume
227
+ * - Seek to arbitrary tick position
228
+ * - Loop restart (seekAt a future audio time)
229
+ *
230
+ * All time values are in AudioContext seconds (context.currentTime).
231
+ * Ticks are the internal unit: ppq ticks per quarter note.
232
+ *
233
+ * Principle: a Checkpoint records that at a specific audio time, a specific
234
+ * tick position was reached at a specific BPM. Checkpoints are always ordered
235
+ * by audioTime. The conversion functions find the last checkpoint whose
236
+ * audioTime (or tick) is <= the query value and interpolate from there.
237
+ */
238
+ type TransportState = "stopped" | "playing" | "paused";
239
+
240
+ type SequencerNote = {
241
+ /** Optional identifier for this note. Used as `noteId` in noteOn/noteOff events. Defaults to the note's array index. */
242
+ id?: string | number;
243
+ note: string | number;
244
+ /** Musical position: ticks, "4n", "1m", "2:1", "1:1.5", etc. */
245
+ at: string | number;
246
+ /** Note duration: ticks, "4n", "8n", etc. Omit for a one-shot trigger. */
247
+ duration?: string | number;
248
+ velocity?: number;
249
+ };
250
+ /**
251
+ * Any instrument the Sequencer can drive.
252
+ * Compatible with SplendidGrandPiano, DrumMachine, Smplr, and any object
253
+ * that has a `start()` method accepting note + optional scheduling params.
254
+ */
255
+ type SequencerInstrument = {
256
+ start(event: {
257
+ note: string | number;
258
+ time?: number;
259
+ duration?: number;
260
+ velocity?: number;
261
+ noteId?: string | number;
262
+ onStart?: (event: {
263
+ noteId?: string | number;
264
+ }) => void;
265
+ onEnded?: (event: {
266
+ noteId?: string | number;
267
+ }) => void;
268
+ }): unknown;
269
+ };
270
+ /** Emitted with "noteOn" and "noteOff" events. */
271
+ type NoteEvent = {
272
+ noteId: string | number;
273
+ trackIndex: number;
274
+ noteIndex: number;
275
+ note: SequencerNote;
276
+ };
277
+ type SequencerOptions = {
278
+ bpm?: number;
279
+ ppq?: number;
280
+ timeSignature?: number;
281
+ loop?: boolean;
282
+ loopStart?: string | number;
283
+ loopEnd?: string | number;
284
+ /** How far ahead (ms) to pre-schedule notes. Default 200. */
285
+ lookaheadMs?: number;
286
+ /** How often (ms) the flush loop runs. Default 50. */
287
+ intervalMs?: number;
288
+ /** Randomise timing (seconds) and velocity per note for a human feel. */
289
+ humanize?: {
290
+ timing?: number;
291
+ velocity?: number;
292
+ };
293
+ };
294
+ declare class Sequencer {
295
+ private readonly _context;
296
+ private readonly _clock;
297
+ private readonly _ppq;
298
+ private _timeSignature;
299
+ private _tracks;
300
+ private _repeatEvents;
301
+ private _listeners;
302
+ private _loop;
303
+ private _loopStartTick;
304
+ /** null = default to _totalTicks */
305
+ private _loopEndOverride;
306
+ private _lookaheadSec;
307
+ private _intervalMs;
308
+ private _humanize;
309
+ private _intervalId;
310
+ /** AudioContext time high-water mark: notes up to here have been scheduled. */
311
+ private _scheduledThrough;
312
+ /** Computed from track notes; the tick where the last note ends. */
313
+ private _totalTicks;
314
+ /** Guards against scheduling the auto-stop setTimeout more than once. */
315
+ private _endScheduled;
316
+ constructor(context: BaseAudioContext, options?: SequencerOptions);
317
+ addTrack(instrument: SequencerInstrument, notes: SequencerNote[]): this;
318
+ removeTrack(instrument: SequencerInstrument): this;
319
+ clearTracks(): this;
320
+ get state(): TransportState;
321
+ /**
322
+ * Start playback from `offsetTick`, or resume from pause if no offset given.
323
+ */
324
+ start(offsetTick?: number): this;
325
+ pause(): this;
326
+ stop(): this;
327
+ get bpm(): number;
328
+ set bpm(value: number);
329
+ get timeSignature(): number;
330
+ set timeSignature(value: number);
331
+ /** Current transport position as "bar:beat:tick" (1-indexed). */
332
+ get position(): string;
333
+ /**
334
+ * Seek to a position. Accepts ticks or any time string ("2:1", "4n", …).
335
+ * Works while playing (seamless) or stopped/paused.
336
+ */
337
+ set position(value: string | number);
338
+ get loop(): boolean;
339
+ set loop(value: boolean);
340
+ /** Loop start in ticks. */
341
+ get loopStart(): number;
342
+ set loopStart(value: string | number);
343
+ /** Loop end in ticks. Defaults to the end of the longest track. */
344
+ get loopEnd(): number;
345
+ set loopEnd(value: string | number);
346
+ /**
347
+ * Normalised loop position [0, 1]. Always 0 when loop=false.
348
+ */
349
+ get progress(): number;
350
+ /**
351
+ * Schedule a callback to fire on every `interval` while the sequencer plays.
352
+ * Returns a cancel function.
353
+ *
354
+ * @param callback Called with the exact AudioContext time of each firing.
355
+ * @param interval Musical interval: "4n", "8n", "1m", ticks, etc.
356
+ * @param startAt First firing position (default 0 = beginning).
357
+ */
358
+ scheduleRepeat(callback: (time: number) => void, interval: string | number, startAt?: string | number): () => void;
359
+ /**
360
+ * Listen to a sequencer event.
361
+ *
362
+ * | Event | Args |
363
+ * |-----------|------------------------------|
364
+ * | "start" | |
365
+ * | "stop" | |
366
+ * | "pause" | |
367
+ * | "end" | |
368
+ * | "loop" | |
369
+ * | "beat" | (beat: number, time: number) |
370
+ * | "bar" | (bar: number, time: number) |
371
+ * | "noteOn" | (event: NoteEvent) |
372
+ * | "noteOff" | (event: NoteEvent) |
373
+ */
374
+ on(event: string, callback: (...args: any[]) => void): this;
375
+ off(event: string, callback: (...args: any[]) => void): this;
376
+ private _startLoop;
377
+ private _stopLoop;
378
+ private _flush;
379
+ private _scheduleWindow;
380
+ private _emitBeatsInWindow;
381
+ private _emit;
382
+ /** Recompute _totalTicks from all track notes (at + duration). */
383
+ private _recomputeTotalTicks;
384
+ /** Format a raw tick count as "bar:beat:tick" (all 1-indexed). */
385
+ private _tickToPosition;
386
+ /**
387
+ * Reset all repeat events so their next firing is the first occurrence
388
+ * at or after `fromTick`.
389
+ */
390
+ private _resetRepeatEvents;
391
+ }
392
+
221
393
  declare function getElectricPianoNames(): string[];
222
394
  type ElectricPianoOptions = Partial<{
223
395
  instrument: string;
@@ -241,7 +413,7 @@ declare class ElectricPiano {
241
413
  });
242
414
  get output(): OutputChannel;
243
415
  get loadProgress(): LoadProgress;
244
- start(sample: NoteEvent | string | number): StopFn;
416
+ start(sample: NoteEvent$1 | string | number): StopFn;
245
417
  stop(target?: StopTarget): void;
246
418
  disconnect(): void;
247
419
  }
@@ -270,7 +442,7 @@ declare class Versilian {
270
442
  constructor(context: BaseAudioContext, options?: VersilianOptions);
271
443
  get output(): OutputChannel;
272
444
  get loadProgress(): LoadProgress;
273
- start(sample: NoteEvent | string | number): StopFn;
445
+ start(sample: NoteEvent$1 | string | number): StopFn;
274
446
  stop(target?: StopTarget): void;
275
447
  disconnect(): void;
276
448
  }
@@ -301,7 +473,7 @@ declare class Mellotron {
301
473
  constructor(context: BaseAudioContext, options?: MellotronOptions);
302
474
  get output(): OutputChannel;
303
475
  get loadProgress(): LoadProgress;
304
- start(sample: NoteEvent | string | number): StopFn;
476
+ start(sample: NoteEvent$1 | string | number): StopFn;
305
477
  stop(target?: StopTarget): void;
306
478
  disconnect(): void;
307
479
  }
@@ -359,7 +531,7 @@ declare class Sampler {
359
531
  constructor(context: AudioContext, options?: Partial<SamplerConfig>);
360
532
  loaded(): Promise<this>;
361
533
  get output(): OutputChannel;
362
- start(sample: NoteEvent | string | number): StopFn;
534
+ start(sample: NoteEvent$1 | string | number): StopFn;
363
535
  stop(sample?: StopTarget | string | number): void;
364
536
  disconnect(): void;
365
537
  }
@@ -399,7 +571,7 @@ declare class Smolken {
399
571
  constructor(context: BaseAudioContext, options?: SmolkenOptions);
400
572
  get output(): OutputChannel;
401
573
  get loadProgress(): LoadProgress;
402
- start(sample: NoteEvent | string | number): StopFn;
574
+ start(sample: NoteEvent$1 | string | number): StopFn;
403
575
  stop(target?: StopTarget): void;
404
576
  disconnect(): void;
405
577
  }
@@ -433,7 +605,7 @@ declare class Soundfont {
433
605
  get output(): OutputChannel;
434
606
  loaded(): Promise<this>;
435
607
  disconnect(): void;
436
- start(sample: NoteEvent | string | number): StopFn;
608
+ start(sample: NoteEvent$1 | string | number): StopFn;
437
609
  stop(sample?: StopTarget | string | number): void;
438
610
  }
439
611
  /**
@@ -492,7 +664,7 @@ declare class Soundfont2Sampler {
492
664
  get instrumentNames(): string[];
493
665
  get output(): OutputChannel;
494
666
  loadInstrument(instrumentName: string): Promise<void> | undefined;
495
- start(sample: NoteEvent | string | number): StopFn;
667
+ start(sample: NoteEvent$1 | string | number): StopFn;
496
668
  stop(sample?: StopTarget | string | number): void;
497
669
  disconnect(): void;
498
670
  }
@@ -532,7 +704,7 @@ declare class SplendidGrandPiano {
532
704
  get loadProgress(): LoadProgress;
533
705
  /** @deprecated Use `load` instead. */
534
706
  loaded(): Promise<this>;
535
- start(event: NoteEvent): StopFn;
707
+ start(event: NoteEvent$1): StopFn;
536
708
  stop(target?: StopTarget): void;
537
709
  disconnect(): void;
538
710
  }
@@ -561,4 +733,4 @@ declare const LAYERS: ({
561
733
  cutoff?: undefined;
562
734
  })[];
563
735
 
564
- export { CacheStorage, DrumMachine, type DrumMachineOptions, ElectricPiano, type ElectricPianoOptions, HttpStorage, LAYERS, Mallet, Mellotron, type MellotronConfig, type MellotronOptions, NAME_TO_PATH, Reverb, Sampler, type SamplerConfig, Smolken, type SmolkenConfig, type SmolkenOptions, Soundfont, type Soundfont2Options, Soundfont2Sampler, type SoundfontOptions, SplendidGrandPiano, type SplendidGrandPianoConfig, type Storage, type StorageResponse, Versilian, type VersilianConfig, type VersilianOptions, drumMachineToSmplrJson, getDrumMachineNames, getElectricPianoNames, getMalletNames, getMellotronNames, getSmolkenNames, getSoundfontKits, getSoundfontNames, getVersilianInstruments, mellotronToSmplrJson, pianoToSmplrJson, samplerToSmplrJson, sf2InstrumentToSmplrJson, soundfontToSmplrJson, spreadKeyRanges };
736
+ export { CacheStorage, DrumMachine, type DrumMachineOptions, ElectricPiano, type ElectricPianoOptions, HttpStorage, LAYERS, Mallet, Mellotron, type MellotronConfig, type MellotronOptions, NAME_TO_PATH, type NoteEvent, Reverb, Sampler, type SamplerConfig, Sequencer, type SequencerInstrument, type SequencerNote, type SequencerOptions, Smolken, type SmolkenConfig, type SmolkenOptions, Soundfont, type Soundfont2Options, Soundfont2Sampler, type SoundfontOptions, SplendidGrandPiano, type SplendidGrandPianoConfig, type Storage, type StorageResponse, Versilian, type VersilianConfig, type VersilianOptions, drumMachineToSmplrJson, getDrumMachineNames, getElectricPianoNames, getMalletNames, getMellotronNames, getSmolkenNames, getSoundfontKits, getSoundfontNames, getVersilianInstruments, mellotronToSmplrJson, pianoToSmplrJson, samplerToSmplrJson, sf2InstrumentToSmplrJson, soundfontToSmplrJson, spreadKeyRanges };