smplr 0.18.0 → 0.18.1

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
@@ -349,9 +349,9 @@ seq.addTrack(piano, [
349
349
  ]);
350
350
 
351
351
  seq.addTrack(drums, [
352
- { note: "kick", at: "1:1" },
352
+ { note: "kick", at: "1:1" },
353
353
  { note: "snare", at: "1:2" },
354
- { note: "kick", at: "1:3" },
354
+ { note: "kick", at: "1:3" },
355
355
  { note: "snare", at: "1:4" },
356
356
  ]);
357
357
 
@@ -363,60 +363,68 @@ seq.start();
363
363
 
364
364
  Note positions and durations accept several formats:
365
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) |
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
375
 
376
376
  #### Constructor options
377
377
 
378
378
  ```js
379
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
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: { timingMs: 10, velocity: 8 }, // optional randomisation
389
389
  });
390
390
  ```
391
391
 
392
392
  #### Playback
393
393
 
394
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
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
+ seq.togglePlayPause(); // pause if playing, start/resume otherwise
398
399
 
399
- seq.state; // "stopped" | "playing" | "paused"
400
+ seq.state; // "stopped" | "playing" | "paused"
401
+ ```
402
+
403
+ Individual sequenced notes can be stopped by their id:
404
+
405
+ ```js
406
+ seq.stopNote("intro-c"); // stop immediately
407
+ seq.stopNote("intro-c", time); // stop at a scheduled time
400
408
  ```
401
409
 
402
410
  #### Tempo and position
403
411
 
404
412
  ```js
405
- seq.bpm = 140; // change BPM live, no glitch
406
- seq.timeSignature = 3; // change time signature
413
+ seq.bpm = 140; // change BPM live, no glitch
414
+ seq.timeSignature = 3; // change time signature
407
415
 
408
- seq.position; // current position as "bar:beat:tick" string
409
- seq.position = "3:1"; // seek while playing or stopped
416
+ seq.position; // current position as "bar:beat:tick" string
417
+ seq.position = "3:1"; // seek while playing or stopped
410
418
  ```
411
419
 
412
420
  #### Loop
413
421
 
414
422
  ```js
415
423
  seq.loop = true;
416
- seq.loopStart = "1:1"; // ticks or string notation
417
- seq.loopEnd = "3:1"; // ticks or string notation
424
+ seq.loopStart = "1:1"; // ticks or string notation
425
+ seq.loopEnd = "3:1"; // ticks or string notation
418
426
 
419
- seq.progress; // 0..1 within the loop range
427
+ seq.progress; // 0..1 within the loop range
420
428
  ```
421
429
 
422
430
  #### Pattern API
@@ -440,19 +448,30 @@ seq.scheduleRepeat(callback, "4n", "2:1"); // start at bar 2
440
448
  #### Events
441
449
 
442
450
  ```js
451
+ seq.on("statechange", (state) => {
452
+ // state: "playing" | "paused" | "stopped"
453
+ setSeqState(state);
454
+ });
455
+
443
456
  seq.on("beat", (beat, time) => {
444
457
  const delay = (time - context.currentTime) * 1000;
445
458
  setTimeout(() => metronome.flash(), delay);
446
459
  });
447
460
 
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", () => { });
461
+ seq.on("bar", (bar, time) => {
462
+ ui.updateBar(bar);
463
+ });
464
+ seq.on("loop", () => {
465
+ console.log("looped");
466
+ });
467
+ seq.on("end", () => {
468
+ console.log("done");
469
+ });
470
+ seq.on("start", () => {});
471
+ seq.on("stop", () => {});
472
+ seq.on("pause", () => {});
454
473
 
455
- seq.off("beat", handler); // remove a listener
474
+ seq.off("beat", handler); // remove a listener
456
475
  ```
457
476
 
458
477
  #### Note events
@@ -471,12 +490,12 @@ seq.on("noteOff", (event) => {
471
490
 
472
491
  The `event` object (`NoteEvent`) contains:
473
492
 
474
- | Field | Type | Description |
475
- |--------------|--------------------|--------------------------------------------------|
493
+ | Field | Type | Description |
494
+ | ------------ | ------------------ | ------------------------------------------------------ |
476
495
  | `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 |
496
+ | `trackIndex` | `number` | Index of the track in the order it was added |
497
+ | `noteIndex` | `number` | Index of the note within its track's notes array |
498
+ | `note` | `SequencerNote` | The original note object |
480
499
 
481
500
  You can set a custom `id` on any `SequencerNote` to use as `noteId`:
482
501
 
@@ -489,15 +508,18 @@ seq.addTrack(piano, [
489
508
 
490
509
  #### Humanize
491
510
 
492
- Add subtle randomisation to timing (seconds) and velocity for a more natural feel:
511
+ Add subtle randomisation to timing and velocity for a more natural feel:
493
512
 
494
513
  ```js
495
514
  const seq = new Sequencer(context, {
496
515
  bpm: 90,
497
- humanize: { timing: 0.012, velocity: 8 },
516
+ humanize: { timingMs: 12, velocity: 8 },
498
517
  });
499
518
  ```
500
519
 
520
+ - `timingMs`: maximum random offset in milliseconds (±). Default 0.
521
+ - `velocity`: maximum random offset in MIDI velocity units (±). Default 0.
522
+
501
523
  ---
502
524
 
503
525
  ## Instruments
package/dist/index.d.mts CHANGED
@@ -259,12 +259,8 @@ type SequencerInstrument = {
259
259
  duration?: number;
260
260
  velocity?: number;
261
261
  noteId?: string | number;
262
- onStart?: (event: {
263
- noteId?: string | number;
264
- }) => void;
265
- onEnded?: (event: {
266
- noteId?: string | number;
267
- }) => void;
262
+ onStart?: (event: unknown) => void;
263
+ onEnded?: (event: unknown) => void;
268
264
  }): unknown;
269
265
  };
270
266
  /** Emitted with "noteOn" and "noteOff" events. */
@@ -285,9 +281,9 @@ type SequencerOptions = {
285
281
  lookaheadMs?: number;
286
282
  /** How often (ms) the flush loop runs. Default 50. */
287
283
  intervalMs?: number;
288
- /** Randomise timing (seconds) and velocity per note for a human feel. */
284
+ /** Randomise timing (ms) and velocity per note for a human feel. */
289
285
  humanize?: {
290
- timing?: number;
286
+ timingMs?: number;
291
287
  velocity?: number;
292
288
  };
293
289
  };
@@ -313,6 +309,8 @@ declare class Sequencer {
313
309
  private _totalTicks;
314
310
  /** Guards against scheduling the auto-stop setTimeout more than once. */
315
311
  private _endScheduled;
312
+ /** Active voices keyed by noteId, so individual notes can be stopped. */
313
+ private _activeVoices;
316
314
  constructor(context: BaseAudioContext, options?: SequencerOptions);
317
315
  addTrack(instrument: SequencerInstrument, notes: SequencerNote[]): this;
318
316
  removeTrack(instrument: SequencerInstrument): this;
@@ -324,6 +322,16 @@ declare class Sequencer {
324
322
  start(offsetTick?: number): this;
325
323
  pause(): this;
326
324
  stop(): this;
325
+ /**
326
+ * Stop a single note that was scheduled by the sequencer.
327
+ * @param noteId The id of the note (from SequencerNote.id or auto-assigned index).
328
+ * @param time Optional AudioContext time to schedule the stop.
329
+ */
330
+ stopNote(noteId: string | number, time?: number): this;
331
+ /**
332
+ * Toggle between playing and paused. If stopped, starts from the beginning.
333
+ */
334
+ togglePlayPause(): this;
327
335
  get bpm(): number;
328
336
  set bpm(value: number);
329
337
  get timeSignature(): number;
@@ -359,17 +367,18 @@ declare class Sequencer {
359
367
  /**
360
368
  * Listen to a sequencer event.
361
369
  *
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) |
370
+ * | Event | Args |
371
+ * |----------------|---------------------------------------------------|
372
+ * | "statechange" | (state: "playing" \| "paused" \| "stopped") |
373
+ * | "start" | |
374
+ * | "stop" | |
375
+ * | "pause" | |
376
+ * | "end" | |
377
+ * | "loop" | |
378
+ * | "beat" | (beat: number, time: number) |
379
+ * | "bar" | (bar: number, time: number) |
380
+ * | "noteOn" | (event: NoteEvent) |
381
+ * | "noteOff" | (event: NoteEvent) |
373
382
  */
374
383
  on(event: string, callback: (...args: any[]) => void): this;
375
384
  off(event: string, callback: (...args: any[]) => void): this;
@@ -379,6 +388,8 @@ declare class Sequencer {
379
388
  private _scheduleWindow;
380
389
  private _emitBeatsInWindow;
381
390
  private _emit;
391
+ /** Emit both the specific state event ("start"/"pause"/"stop") and the unified "statechange" event. */
392
+ private _emitStateChange;
382
393
  /** Recompute _totalTicks from all track notes (at + duration). */
383
394
  private _recomputeTotalTicks;
384
395
  /** Format a raw tick count as "bar:beat:tick" (all 1-indexed). */
package/dist/index.d.ts CHANGED
@@ -259,12 +259,8 @@ type SequencerInstrument = {
259
259
  duration?: number;
260
260
  velocity?: number;
261
261
  noteId?: string | number;
262
- onStart?: (event: {
263
- noteId?: string | number;
264
- }) => void;
265
- onEnded?: (event: {
266
- noteId?: string | number;
267
- }) => void;
262
+ onStart?: (event: unknown) => void;
263
+ onEnded?: (event: unknown) => void;
268
264
  }): unknown;
269
265
  };
270
266
  /** Emitted with "noteOn" and "noteOff" events. */
@@ -285,9 +281,9 @@ type SequencerOptions = {
285
281
  lookaheadMs?: number;
286
282
  /** How often (ms) the flush loop runs. Default 50. */
287
283
  intervalMs?: number;
288
- /** Randomise timing (seconds) and velocity per note for a human feel. */
284
+ /** Randomise timing (ms) and velocity per note for a human feel. */
289
285
  humanize?: {
290
- timing?: number;
286
+ timingMs?: number;
291
287
  velocity?: number;
292
288
  };
293
289
  };
@@ -313,6 +309,8 @@ declare class Sequencer {
313
309
  private _totalTicks;
314
310
  /** Guards against scheduling the auto-stop setTimeout more than once. */
315
311
  private _endScheduled;
312
+ /** Active voices keyed by noteId, so individual notes can be stopped. */
313
+ private _activeVoices;
316
314
  constructor(context: BaseAudioContext, options?: SequencerOptions);
317
315
  addTrack(instrument: SequencerInstrument, notes: SequencerNote[]): this;
318
316
  removeTrack(instrument: SequencerInstrument): this;
@@ -324,6 +322,16 @@ declare class Sequencer {
324
322
  start(offsetTick?: number): this;
325
323
  pause(): this;
326
324
  stop(): this;
325
+ /**
326
+ * Stop a single note that was scheduled by the sequencer.
327
+ * @param noteId The id of the note (from SequencerNote.id or auto-assigned index).
328
+ * @param time Optional AudioContext time to schedule the stop.
329
+ */
330
+ stopNote(noteId: string | number, time?: number): this;
331
+ /**
332
+ * Toggle between playing and paused. If stopped, starts from the beginning.
333
+ */
334
+ togglePlayPause(): this;
327
335
  get bpm(): number;
328
336
  set bpm(value: number);
329
337
  get timeSignature(): number;
@@ -359,17 +367,18 @@ declare class Sequencer {
359
367
  /**
360
368
  * Listen to a sequencer event.
361
369
  *
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) |
370
+ * | Event | Args |
371
+ * |----------------|---------------------------------------------------|
372
+ * | "statechange" | (state: "playing" \| "paused" \| "stopped") |
373
+ * | "start" | |
374
+ * | "stop" | |
375
+ * | "pause" | |
376
+ * | "end" | |
377
+ * | "loop" | |
378
+ * | "beat" | (beat: number, time: number) |
379
+ * | "bar" | (bar: number, time: number) |
380
+ * | "noteOn" | (event: NoteEvent) |
381
+ * | "noteOff" | (event: NoteEvent) |
373
382
  */
374
383
  on(event: string, callback: (...args: any[]) => void): this;
375
384
  off(event: string, callback: (...args: any[]) => void): this;
@@ -379,6 +388,8 @@ declare class Sequencer {
379
388
  private _scheduleWindow;
380
389
  private _emitBeatsInWindow;
381
390
  private _emit;
391
+ /** Emit both the specific state event ("start"/"pause"/"stop") and the unified "statechange" event. */
392
+ private _emitStateChange;
382
393
  /** Recompute _totalTicks from all track notes (at + duration). */
383
394
  private _recomputeTotalTicks;
384
395
  /** Format a raw tick count as "bar:beat:tick" (all 1-indexed). */
package/dist/index.js CHANGED
@@ -1532,6 +1532,8 @@ var Sequencer = class {
1532
1532
  this._totalTicks = 0;
1533
1533
  /** Guards against scheduling the auto-stop setTimeout more than once. */
1534
1534
  this._endScheduled = false;
1535
+ /** Active voices keyed by noteId, so individual notes can be stopped. */
1536
+ this._activeVoices = /* @__PURE__ */ new Map();
1535
1537
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
1536
1538
  this._context = context;
1537
1539
  this._ppq = (_a = options.ppq) != null ? _a : 480;
@@ -1553,7 +1555,7 @@ var Sequencer = class {
1553
1555
  this._lookaheadSec = ((_e = options.lookaheadMs) != null ? _e : 200) / 1e3;
1554
1556
  this._intervalMs = (_f = options.intervalMs) != null ? _f : 50;
1555
1557
  this._humanize = {
1556
- timing: (_h = (_g = options.humanize) == null ? void 0 : _g.timing) != null ? _h : 0,
1558
+ timing: (_h = (_g = options.humanize) == null ? void 0 : _g.timingMs) != null ? _h : 0,
1557
1559
  velocity: (_j = (_i = options.humanize) == null ? void 0 : _i.velocity) != null ? _j : 0
1558
1560
  };
1559
1561
  }
@@ -1590,7 +1592,7 @@ var Sequencer = class {
1590
1592
  this._clock.resume();
1591
1593
  this._scheduledThrough = this._context.currentTime;
1592
1594
  this._startLoop();
1593
- this._emit("start");
1595
+ this._emitStateChange("playing");
1594
1596
  return this;
1595
1597
  }
1596
1598
  const startTick = offsetTick != null ? offsetTick : 0;
@@ -1599,23 +1601,44 @@ var Sequencer = class {
1599
1601
  this._endScheduled = false;
1600
1602
  this._resetRepeatEvents(startTick);
1601
1603
  this._startLoop();
1602
- this._emit("start");
1604
+ this._emitStateChange("playing");
1603
1605
  return this;
1604
1606
  }
1605
1607
  pause() {
1606
1608
  if (this._clock.state !== "playing") return this;
1607
1609
  this._clock.pause();
1608
1610
  this._stopLoop();
1609
- this._emit("pause");
1611
+ this._emitStateChange("paused");
1610
1612
  return this;
1611
1613
  }
1612
1614
  stop() {
1613
1615
  this._clock.stop();
1614
1616
  this._stopLoop();
1615
1617
  this._endScheduled = false;
1616
- this._emit("stop");
1618
+ this._activeVoices.clear();
1619
+ this._emitStateChange("stopped");
1617
1620
  return this;
1618
1621
  }
1622
+ /**
1623
+ * Stop a single note that was scheduled by the sequencer.
1624
+ * @param noteId The id of the note (from SequencerNote.id or auto-assigned index).
1625
+ * @param time Optional AudioContext time to schedule the stop.
1626
+ */
1627
+ stopNote(noteId, time) {
1628
+ const stopFn = this._activeVoices.get(noteId);
1629
+ if (stopFn) {
1630
+ stopFn(time);
1631
+ this._activeVoices.delete(noteId);
1632
+ }
1633
+ return this;
1634
+ }
1635
+ /**
1636
+ * Toggle between playing and paused. If stopped, starts from the beginning.
1637
+ */
1638
+ togglePlayPause() {
1639
+ if (this._clock.state === "playing") return this.pause();
1640
+ return this.start();
1641
+ }
1619
1642
  // ---------------------------------------------------------------------------
1620
1643
  // Tempo
1621
1644
  // ---------------------------------------------------------------------------
@@ -1719,17 +1742,18 @@ var Sequencer = class {
1719
1742
  /**
1720
1743
  * Listen to a sequencer event.
1721
1744
  *
1722
- * | Event | Args |
1723
- * |-----------|------------------------------|
1724
- * | "start" | |
1725
- * | "stop" | |
1726
- * | "pause" | |
1727
- * | "end" | |
1728
- * | "loop" | |
1729
- * | "beat" | (beat: number, time: number) |
1730
- * | "bar" | (bar: number, time: number) |
1731
- * | "noteOn" | (event: NoteEvent) |
1732
- * | "noteOff" | (event: NoteEvent) |
1745
+ * | Event | Args |
1746
+ * |----------------|---------------------------------------------------|
1747
+ * | "statechange" | (state: "playing" \| "paused" \| "stopped") |
1748
+ * | "start" | |
1749
+ * | "stop" | |
1750
+ * | "pause" | |
1751
+ * | "end" | |
1752
+ * | "loop" | |
1753
+ * | "beat" | (beat: number, time: number) |
1754
+ * | "bar" | (bar: number, time: number) |
1755
+ * | "noteOn" | (event: NoteEvent) |
1756
+ * | "noteOff" | (event: NoteEvent) |
1733
1757
  */
1734
1758
  on(event, callback) {
1735
1759
  if (!this._listeners.has(event)) {
@@ -1788,6 +1812,7 @@ var Sequencer = class {
1788
1812
  this._stopLoop();
1789
1813
  this._clock.stop();
1790
1814
  this._emit("end");
1815
+ this._emit("statechange", "stopped");
1791
1816
  }, delay);
1792
1817
  }
1793
1818
  }
@@ -1806,19 +1831,25 @@ var Sequencer = class {
1806
1831
  const durationSec = note.duration !== void 0 ? this._clock.tickDuration(
1807
1832
  parseTicks(note.duration, this._ppq, this._timeSignature)
1808
1833
  ) : void 0;
1809
- const timingOffset = this._humanize.timing ? (Math.random() * 2 - 1) * this._humanize.timing : 0;
1834
+ const timingOffset = this._humanize.timing ? (Math.random() * 2 - 1) * this._humanize.timing / 1e3 : 0;
1810
1835
  const velocityOffset = this._humanize.velocity ? Math.round((Math.random() * 2 - 1) * this._humanize.velocity) : 0;
1811
1836
  const noteId = (_a = note.id) != null ? _a : noteIndex;
1812
1837
  const noteEvent = { noteId, trackIndex, noteIndex, note };
1813
- track.instrument.start({
1838
+ const result = track.instrument.start({
1814
1839
  note: note.note,
1815
- time: audioTime + timingOffset,
1840
+ time: Math.max(0, audioTime + timingOffset),
1816
1841
  duration: durationSec,
1817
1842
  velocity: ((_b = note.velocity) != null ? _b : 100) + velocityOffset,
1818
1843
  noteId,
1819
1844
  onStart: () => this._emit("noteOn", noteEvent),
1820
- onEnded: () => this._emit("noteOff", noteEvent)
1845
+ onEnded: () => {
1846
+ this._activeVoices.delete(noteId);
1847
+ this._emit("noteOff", noteEvent);
1848
+ }
1821
1849
  });
1850
+ if (typeof result === "function") {
1851
+ this._activeVoices.set(noteId, result);
1852
+ }
1822
1853
  }
1823
1854
  }
1824
1855
  for (const rep of this._repeatEvents) {
@@ -1858,6 +1889,12 @@ var Sequencer = class {
1858
1889
  }
1859
1890
  }
1860
1891
  }
1892
+ /** Emit both the specific state event ("start"/"pause"/"stop") and the unified "statechange" event. */
1893
+ _emitStateChange(state) {
1894
+ const eventName = state === "playing" ? "start" : state === "paused" ? "pause" : "stop";
1895
+ this._emit(eventName);
1896
+ this._emit("statechange", state);
1897
+ }
1861
1898
  /** Recompute _totalTicks from all track notes (at + duration). */
1862
1899
  _recomputeTotalTicks() {
1863
1900
  let max = 0;