smplr 0.23.0 → 0.24.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 +137 -17
- package/dist/index.d.mts +226 -33
- package/dist/index.d.ts +226 -33
- package/dist/index.js +332 -112
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +331 -112
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ const context = new AudioContext();
|
|
|
25
25
|
const piano = SplendidGrandPiano(context);
|
|
26
26
|
const drums = DrumMachine(context, { instrument: "TR-808" });
|
|
27
27
|
|
|
28
|
-
const seq =
|
|
28
|
+
const seq = Sequencer(context, { bpm: 110, loop: true });
|
|
29
29
|
seq.addTrack(piano, [
|
|
30
30
|
{ note: "C4", at: "1:1", duration: "4n" },
|
|
31
31
|
{ note: "E4", at: "1:2", duration: "4n" },
|
|
@@ -47,7 +47,7 @@ import { SplendidGrandPiano, Reverb, renderOffline } from "smplr";
|
|
|
47
47
|
|
|
48
48
|
const wav = await renderOffline(async (context) => {
|
|
49
49
|
const piano = await SplendidGrandPiano(context).load;
|
|
50
|
-
piano.output.addEffect("reverb",
|
|
50
|
+
piano.output.addEffect("reverb", Reverb(context), 0.3);
|
|
51
51
|
["C4", "E4", "G4", "C5"].forEach((note, i) => {
|
|
52
52
|
piano.start({ note, time: i * 0.4, duration: 0.4 });
|
|
53
53
|
});
|
|
@@ -111,7 +111,7 @@ The package needs to be served as a URL from a service like [unpkg](https://unpk
|
|
|
111
111
|
|
|
112
112
|
### Defining an instrument
|
|
113
113
|
|
|
114
|
-
`smplr` ships ten instruments out of the box — `SplendidGrandPiano`, `Soundfont`, `DrumMachine`, `ElectricPiano`, `Mallet`, `Mellotron`, `Smolken`, `Versilian`, `Sampler`, `
|
|
114
|
+
`smplr` ships ten instruments out of the box — `SplendidGrandPiano`, `Soundfont`, `DrumMachine`, `ElectricPiano`, `Mallet`, `Mellotron`, `Smolken`, `Versilian`, `Sampler`, `Soundfont2`. If none of them fit your use case, you can author your own with the `Instrument` builder and the `Smplr` interface.
|
|
115
115
|
|
|
116
116
|
See **[Defining an instrument](./AUTHORING.md)** for the full authoring guide — sync and async examples, third-party package layout, and how to use `Smplr` as a TypeScript type for generic helpers.
|
|
117
117
|
|
|
@@ -179,7 +179,7 @@ All instruments share some configuration options, passed as the second argument
|
|
|
179
179
|
- `volumeToGain`: a function to map MIDI volume to a linear gain. Uses the MIDI standard curve by default.
|
|
180
180
|
- `storage`: a [storage backend](#cache-requests) used to fetch sample buffers. `HttpStorage` by default.
|
|
181
181
|
- `loader`: a shared `SampleLoader` instance. Pass the same loader to multiple instruments to cache buffers across them (see [Buffer reuse](#buffer-reuse)).
|
|
182
|
-
- `scheduler`: a shared `Scheduler` instance. Construct your own to tune scheduling — for example, `
|
|
182
|
+
- `scheduler`: a shared `Scheduler` instance. Construct your own to tune scheduling — for example, `Scheduler(context, { lookaheadMs: 100, intervalMs: 25 })` — or omit to get a per-instrument default.
|
|
183
183
|
- `onLoadProgress`: a function called after each sample buffer is decoded. Receives `{ loaded, total }` where `total` is the full count known before loading starts.
|
|
184
184
|
- `onStart`: called when a note is dispatched to the audio engine. Receives the started note. See ⚠️ note under [Events](#events) on timing precision.
|
|
185
185
|
- `onEnded`: called when each voice's audio node ends. Receives the started note.
|
|
@@ -302,6 +302,22 @@ piano.output.volume; // => 80
|
|
|
302
302
|
|
|
303
303
|
⚠️ `volume` is global to the instrument, but `velocity` is specific for each note.
|
|
304
304
|
|
|
305
|
+
#### Pan, detune, and reverse
|
|
306
|
+
|
|
307
|
+
Every instrument accepts a `pan` option at construction (`-1` = full left, `+1` = full right):
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
const drums = DrumMachine(context, { instrument: "TR-808", pan: -0.5 });
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Two universal setters mutate the playback defaults in place. They apply to notes scheduled **after** the call; in-flight notes are unaffected.
|
|
314
|
+
|
|
315
|
+
```js
|
|
316
|
+
sampler.setDetune(100); // semitone up (100 cents) for all future notes
|
|
317
|
+
sampler.setReverse(true); // play samples reversed for all future notes
|
|
318
|
+
sampler.setReverse(false); // back to forward playback
|
|
319
|
+
```
|
|
320
|
+
|
|
305
321
|
#### MIDI CC
|
|
306
322
|
|
|
307
323
|
Set and read MIDI Control Change values on the instrument:
|
|
@@ -368,7 +384,7 @@ Use `output.addEffect(name, effect, mix)` to connect an effect using a send bus:
|
|
|
368
384
|
|
|
369
385
|
```js
|
|
370
386
|
import { Reverb, SplendidGrandPiano } from "smplr";
|
|
371
|
-
const reverb =
|
|
387
|
+
const reverb = Reverb(context);
|
|
372
388
|
const piano = SplendidGrandPiano(context, { volume });
|
|
373
389
|
piano.output.addEffect("reverb", reverb, 0.2);
|
|
374
390
|
```
|
|
@@ -391,7 +407,7 @@ To cache samples in the browser, use a `CacheStorage` object:
|
|
|
391
407
|
import { SplendidGrandPiano, CacheStorage } from "smplr";
|
|
392
408
|
|
|
393
409
|
const context = new AudioContext();
|
|
394
|
-
const storage =
|
|
410
|
+
const storage = CacheStorage();
|
|
395
411
|
// First time the instrument loads, will fetch the samples from http. Subsequent times from cache.
|
|
396
412
|
const piano = SplendidGrandPiano(context, { storage });
|
|
397
413
|
```
|
|
@@ -400,7 +416,7 @@ const piano = SplendidGrandPiano(context, { storage });
|
|
|
400
416
|
|
|
401
417
|
## Sequencer
|
|
402
418
|
|
|
403
|
-
`Sequencer` schedules notes from one or more tracks against any smplr instrument with sample-accurate timing.
|
|
419
|
+
`Sequencer` schedules notes from one or more tracks against any smplr instrument with sample-accurate timing. Constructed as `Sequencer(context, opts)` (the `new Sequencer(...)` form also still works as a deprecated alias).
|
|
404
420
|
|
|
405
421
|
```js
|
|
406
422
|
import { Sequencer, SplendidGrandPiano, DrumMachine } from "smplr";
|
|
@@ -409,7 +425,7 @@ const context = new AudioContext();
|
|
|
409
425
|
const piano = SplendidGrandPiano(context);
|
|
410
426
|
const drums = DrumMachine(context, { instrument: "TR-808" });
|
|
411
427
|
|
|
412
|
-
const seq =
|
|
428
|
+
const seq = Sequencer(context, { bpm: 120, loop: true });
|
|
413
429
|
|
|
414
430
|
seq.addTrack(piano, [
|
|
415
431
|
{ note: "C4", at: "1:1", duration: "4n" },
|
|
@@ -446,19 +462,55 @@ Note positions and durations accept several formats:
|
|
|
446
462
|
#### Constructor options
|
|
447
463
|
|
|
448
464
|
```js
|
|
449
|
-
const seq =
|
|
465
|
+
const seq = Sequencer(context, {
|
|
450
466
|
bpm: 120, // default 120
|
|
451
467
|
ppq: 480, // pulses per quarter note, default 480
|
|
452
|
-
timeSignature: 4, //
|
|
468
|
+
timeSignature: 4, // accepts `4` (→ 4/4) or `{ numerator, denominator }`
|
|
453
469
|
loop: false, // default false
|
|
454
470
|
loopStart: 0, // loop start position (ticks or string)
|
|
455
471
|
loopEnd: "2:1", // loop end position; defaults to end of longest track
|
|
456
472
|
lookaheadMs: 200, // scheduling lookahead, default 200
|
|
457
473
|
intervalMs: 50, // flush interval, default 50
|
|
458
474
|
humanize: { timingMs: 10, velocity: 8 }, // optional randomisation
|
|
475
|
+
stepSize: "16n", // optional: emit "step" events at this interval
|
|
459
476
|
});
|
|
460
477
|
```
|
|
461
478
|
|
|
479
|
+
`timeSignature` accepts a plain number (interpreted as `{ numerator: n, denominator: 4 }`) or a full object such as `{ numerator: 7, denominator: 8 }` for 7/8 time. The `seq.timeSignature` getter always returns the `{ numerator, denominator }` form.
|
|
480
|
+
|
|
481
|
+
#### Tracks
|
|
482
|
+
|
|
483
|
+
```js
|
|
484
|
+
seq.addTrack(piano, notes); // append a track
|
|
485
|
+
seq.addTrack(drums, notes, { id: "drums", volume: 0.8 }); // with options
|
|
486
|
+
seq.removeTrack(piano); // remove by instrument reference
|
|
487
|
+
seq.clearTracks(); // remove every track
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
`addTrack`'s third argument accepts:
|
|
491
|
+
|
|
492
|
+
| Field | Type | Description |
|
|
493
|
+
| ---------- | ------------------------------------------- | --------------------------------------------------------------------- |
|
|
494
|
+
| `id` | `string` | Stable id for `setTrackVolume` / `muteTrack` / `soloTrack`. |
|
|
495
|
+
| `humanize` | `{ timingMs?: number; velocity?: number }` | Per-track humanize. Overrides the sequencer-level setting when set. |
|
|
496
|
+
| `volume` | `number` | Multiplicative velocity scalar (default 1). `0.5` halves velocities. |
|
|
497
|
+
| `muted` | `boolean` | When true, this track does not dispatch notes. |
|
|
498
|
+
| `solo` | `boolean` | When true, only soloed tracks play. |
|
|
499
|
+
|
|
500
|
+
After `setPatterns` is called (see [Pattern chain](#pattern-chain-song-mode)), `addTrack` / `removeTrack` / `clearTracks` throw — the chain is owned by the patterns array.
|
|
501
|
+
|
|
502
|
+
#### Track mixer
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
seq.setTrackVolume("drums", 0.6);
|
|
506
|
+
seq.muteTrack("drums");
|
|
507
|
+
seq.unmuteTrack("drums");
|
|
508
|
+
seq.soloTrack("lead");
|
|
509
|
+
seq.unsoloTrack("lead");
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Mixer methods operate on the **currently-playing pattern** (so per-pattern mute/solo state is automatic when using a pattern chain). Calls with an unknown id are no-ops.
|
|
513
|
+
|
|
462
514
|
#### Playback
|
|
463
515
|
|
|
464
516
|
```js
|
|
@@ -481,12 +533,17 @@ seq.stopNote("intro-c", time); // stop at a scheduled time
|
|
|
481
533
|
|
|
482
534
|
```js
|
|
483
535
|
seq.bpm = 140; // change BPM live, no glitch
|
|
484
|
-
seq.timeSignature = 3; //
|
|
536
|
+
seq.timeSignature = 3; // 3/4 (number → { numerator: 3, denominator: 4 })
|
|
537
|
+
seq.timeSignature = { numerator: 7, denominator: 8 }; // 7/8
|
|
538
|
+
|
|
539
|
+
seq.timeSignature; // → { numerator: 7, denominator: 8 }
|
|
485
540
|
|
|
486
541
|
seq.position; // current position as "bar:beat:tick" string
|
|
487
542
|
seq.position = "3:1"; // seek while playing or stopped
|
|
488
543
|
```
|
|
489
544
|
|
|
545
|
+
The `"beat"` event fires once per denominator-defined note: 4/4 → 4 beats per bar, 6/8 → 6 beats per bar, etc.
|
|
546
|
+
|
|
490
547
|
#### Loop
|
|
491
548
|
|
|
492
549
|
```js
|
|
@@ -531,12 +588,18 @@ seq.on("beat", (beat, time) => {
|
|
|
531
588
|
seq.on("bar", (bar, time) => {
|
|
532
589
|
ui.updateBar(bar);
|
|
533
590
|
});
|
|
591
|
+
seq.on("step", (stepIndex, time) => {
|
|
592
|
+
ui.flashStep(stepIndex); // only fires when `stepSize` is set in options
|
|
593
|
+
});
|
|
534
594
|
seq.on("loop", () => {
|
|
535
595
|
console.log("looped");
|
|
536
596
|
});
|
|
537
597
|
seq.on("end", () => {
|
|
538
598
|
console.log("done");
|
|
539
599
|
});
|
|
600
|
+
seq.on("patternChange", (patternIndex, time) => {
|
|
601
|
+
ui.highlightPattern(patternIndex); // fires when the chain advances
|
|
602
|
+
});
|
|
540
603
|
seq.on("start", () => {});
|
|
541
604
|
seq.on("stop", () => {});
|
|
542
605
|
seq.on("pause", () => {});
|
|
@@ -544,6 +607,9 @@ seq.on("pause", () => {});
|
|
|
544
607
|
seq.off("beat", handler); // remove a listener
|
|
545
608
|
```
|
|
546
609
|
|
|
610
|
+
The `"step"` event only fires when the sequencer was constructed with `stepSize` (e.g. `"16n"`).
|
|
611
|
+
The `"patternChange"` event only fires when more than one pattern is in the chain.
|
|
612
|
+
|
|
547
613
|
#### Note events
|
|
548
614
|
|
|
549
615
|
`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.
|
|
@@ -581,7 +647,7 @@ seq.addTrack(piano, [
|
|
|
581
647
|
Add subtle randomisation to timing and velocity for a more natural feel:
|
|
582
648
|
|
|
583
649
|
```js
|
|
584
|
-
const seq =
|
|
650
|
+
const seq = Sequencer(context, {
|
|
585
651
|
bpm: 90,
|
|
586
652
|
humanize: { timingMs: 12, velocity: 8 },
|
|
587
653
|
});
|
|
@@ -590,6 +656,60 @@ const seq = new Sequencer(context, {
|
|
|
590
656
|
- `timingMs`: maximum random offset in milliseconds (±). Default 0.
|
|
591
657
|
- `velocity`: maximum random offset in MIDI velocity units (±). Default 0.
|
|
592
658
|
|
|
659
|
+
Per-track humanize (passed to `addTrack`) overrides the global setting:
|
|
660
|
+
|
|
661
|
+
```js
|
|
662
|
+
seq.addTrack(piano, notes, { humanize: { timingMs: 0, velocity: 0 } });
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
#### SequencerNote fields
|
|
666
|
+
|
|
667
|
+
| Field | Type | Description |
|
|
668
|
+
| ---------------------- | ------------------- | ---------------------------------------------------------------------------- |
|
|
669
|
+
| `note` | `string \| number` | Note name or MIDI number. |
|
|
670
|
+
| `at` | `string \| number` | Musical position (ticks or `"bar:beat[.frac][:ticks]"` / `"4n"` / `"1m"`). |
|
|
671
|
+
| `duration` | `string \| number?` | Duration; omit for a one-shot trigger. |
|
|
672
|
+
| `velocity` | `number?` | Velocity 0–127. Default 100. |
|
|
673
|
+
| `id` | `string \| number?` | Used as `noteId` in `noteOn` / `noteOff` events. Default: array index. |
|
|
674
|
+
| `chance` | `number?` | Probability 0–100 that this note fires on each pass. Re-rolled on every loop. |
|
|
675
|
+
| `ratchet` | `number?` | Expand into N sub-notes over `duration` (requires `duration`). |
|
|
676
|
+
| `ratchetVelocityDecay` | `number?` | Per-step velocity decay; each sub-note scaled by `(1 - decay)^i`. |
|
|
677
|
+
|
|
678
|
+
Example:
|
|
679
|
+
|
|
680
|
+
```js
|
|
681
|
+
seq.addTrack(drums, [
|
|
682
|
+
{ note: "hat", at: "1:4", duration: "8n", ratchet: 4, ratchetVelocityDecay: 0.2 },
|
|
683
|
+
{ note: "snare", at: "1:2", chance: 50 }, // fires 50% of the time
|
|
684
|
+
]);
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
When `ratchet > 1`, each sub-note's `noteId` is suffixed with `#0`, `#1`, etc., so you can stop an individual sub-voice via `seq.stopNote("id#0")`.
|
|
688
|
+
|
|
689
|
+
#### Pattern chain (song mode)
|
|
690
|
+
|
|
691
|
+
For multi-pattern arrangements (intro → verse → chorus), use `setPatterns`:
|
|
692
|
+
|
|
693
|
+
```js
|
|
694
|
+
seq.setPatterns([
|
|
695
|
+
{ tracks: [{ instrument: drums, notes: introNotes }], loopEnd: "1m" },
|
|
696
|
+
{ tracks: [{ instrument: drums, notes: verseNotes }], loopEnd: "2m" },
|
|
697
|
+
{ tracks: [{ instrument: drums, notes: chorusNotes }], loopEnd: "2m" },
|
|
698
|
+
]);
|
|
699
|
+
|
|
700
|
+
seq.chainOrder = [0, 1, 2, 1, 2]; // intro, verse, chorus, verse, chorus
|
|
701
|
+
seq.loop = true; // loop the whole chain
|
|
702
|
+
seq.start();
|
|
703
|
+
|
|
704
|
+
seq.on("patternChange", (idx) => ui.highlightPattern(idx));
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
- Each pattern's `tracks` entries accept the same `AddTrackOptions` (`id`, `humanize`, `volume`, `muted`, `solo`) as `addTrack`.
|
|
708
|
+
- `loopEnd` is per-pattern and defaults to the longest track in that pattern.
|
|
709
|
+
- `chainOrder` defaults to `[0, 1, …, n-1]`. Setting it lets you repeat or reorder patterns without duplicating data.
|
|
710
|
+
- With `loop: false` the chain plays once and emits `"end"`; with `loop: true` it cycles indefinitely and emits `"loop"` each time it wraps.
|
|
711
|
+
- Track mixer methods (`muteTrack`, `setTrackVolume`, etc.) operate on the currently-playing pattern — `muteTrack("lead")` only affects the pattern that owns the `"lead"` track.
|
|
712
|
+
|
|
593
713
|
---
|
|
594
714
|
|
|
595
715
|
## Export Audio
|
|
@@ -641,7 +761,7 @@ If you already have an instrument loaded, pass the same `SampleLoader` to avoid
|
|
|
641
761
|
```js
|
|
642
762
|
import { SplendidGrandPiano, SampleLoader, renderOffline } from "smplr";
|
|
643
763
|
|
|
644
|
-
const loader =
|
|
764
|
+
const loader = SampleLoader(audioContext);
|
|
645
765
|
const piano = SplendidGrandPiano(audioContext, { loader });
|
|
646
766
|
await piano.load;
|
|
647
767
|
|
|
@@ -933,16 +1053,16 @@ const context = new AudioContext();
|
|
|
933
1053
|
const versilian = Versilian(context, { instrument: instrumentNames[0] });
|
|
934
1054
|
```
|
|
935
1055
|
|
|
936
|
-
###
|
|
1056
|
+
### Soundfont2
|
|
937
1057
|
|
|
938
|
-
Sampler capable of reading .sf2 files directly
|
|
1058
|
+
Sampler capable of reading .sf2 files directly. Previously named `Soundfont2Sampler`; the old name remains as a deprecated alias.
|
|
939
1059
|
|
|
940
1060
|
```ts
|
|
941
|
-
import {
|
|
1061
|
+
import { Soundfont2 } from "smplr";
|
|
942
1062
|
import { SoundFont2 } from "soundfont2";
|
|
943
1063
|
|
|
944
1064
|
const context = new AudioContext();
|
|
945
|
-
const sampler =
|
|
1065
|
+
const sampler = Soundfont2(context, {
|
|
946
1066
|
url: "https://smpldsnds.github.io/soundfonts/soundfonts/galaxy-electric-pianos.sf2",
|
|
947
1067
|
createSoundfont: (data) => new SoundFont2(data),
|
|
948
1068
|
});
|