smplr 0.23.0 → 0.25.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 +300 -130
- package/dist/index.d.mts +275 -33
- package/dist/index.d.ts +275 -33
- package/dist/index.js +819 -117
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +811 -117
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> `smplr` is a collection of sampled instruments for Web Audio API ready to be used with no setup required.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Quick start
|
|
8
8
|
|
|
9
9
|
**Play a note from a General MIDI soundfont:**
|
|
10
10
|
|
|
@@ -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
|
});
|
|
@@ -57,17 +57,13 @@ wav.downloadWav("arpeggio.wav");
|
|
|
57
57
|
|
|
58
58
|
See demo: https://danigb.github.io/smplr/
|
|
59
59
|
|
|
60
|
-
`smplr` is approaching 1.0. The 0.22.0 release lands the final batch of pre-1.0 API work — every documented `new X(ctx, opts)` keeps working, and the documented surface is intended to ship unchanged into 1.0. The formal stability commitment lands once the narrow `loader`/`scheduler` public interfaces sibling ticket is in (see [CHANGELOG](https://github.com/danigb/smplr/blob/main/CHANGELOG.md)).
|
|
61
|
-
|
|
62
|
-
> **Upgrading from an earlier 0.x?** No code changes are required — every documented `new X(ctx, opts)` keeps working. New code should drop the `new` (`X(ctx, opts)`) and prefer `await x.ready` over `await x.load`.
|
|
63
|
-
|
|
64
60
|
#### Library goals
|
|
65
61
|
|
|
66
62
|
- No setup: specifically, all samples are online, so no need for a server.
|
|
67
63
|
- Easy to use: everything should be intuitive for non-experienced developers
|
|
68
64
|
- Decent sounding: uses high quality open source samples. For better or worse, it is sample based 🤷
|
|
69
65
|
|
|
70
|
-
##
|
|
66
|
+
## Installation
|
|
71
67
|
|
|
72
68
|
You can install the library with a package manager or use it directly by importing from the browser.
|
|
73
69
|
|
|
@@ -105,17 +101,35 @@ You can import directly from the browser. For example:
|
|
|
105
101
|
|
|
106
102
|
The package needs to be served as a URL from a service like [unpkg](https://unpkg.com) or similar.
|
|
107
103
|
|
|
108
|
-
|
|
104
|
+
## Available instruments
|
|
109
105
|
|
|
110
|
-
|
|
106
|
+
`smplr` ships eleven instruments out of the box. Pick one and jump to its section in the [Instrument reference](#instrument-reference) for setup details.
|
|
111
107
|
|
|
112
|
-
|
|
108
|
+
| Instrument | Description | Names helper |
|
|
109
|
+
| ------------------------------------------- | ----------------------------------------- | ----------------------------- |
|
|
110
|
+
| [`Sampler`](#sampler) | Your own buffers or SFZ-style preset | — |
|
|
111
|
+
| [`Soundfont`](#soundfont) | General MIDI soundfonts | `getSoundfontNames()` |
|
|
112
|
+
| [`SplendidGrandPiano`](#splendidgrandpiano) | Sampled Steinway grand, 4 velocity layers | — |
|
|
113
|
+
| [`ElectricPiano`](#electric-piano) | CP80, PianetT, Wurlitzer, TX81Z | `getElectricPianoNames()` |
|
|
114
|
+
| [`DrumMachine`](#drum-machines) | Classic drum machines (TR-808, …) | `getDrumMachineNames()` |
|
|
115
|
+
| [`DrumAbuse`](#drumabuse) | ~210 machines (Synthabuse collection) | `getDrumAbuseMachineNames()` |
|
|
116
|
+
| [`Mallet`](#mallets) | VCSL mallets | `getMalletNames()` |
|
|
117
|
+
| [`Mellotron`](#mellotron) | Mellotron archive samples | `getMellotronNames()` |
|
|
118
|
+
| [`Smolken`](#smolken-double-bass) | Smolken double bass (Arco/Pizz/Switched) | `getSmolkenNames()` |
|
|
119
|
+
| [`Versilian`](#versilian) | VCSL multi-instrument (partial support) | `getVersilianInstruments()` * |
|
|
120
|
+
| [`Soundfont2`](#soundfont2) | Reads .sf2 files directly | — |
|
|
113
121
|
|
|
114
|
-
`
|
|
122
|
+
`*` `getVersilianInstruments` is async because the catalog is fetched from the network on first call (cached thereafter).
|
|
115
123
|
|
|
116
|
-
|
|
124
|
+
Each names helper returns the strings you can pass to the corresponding factory's `instrument` option.
|
|
125
|
+
|
|
126
|
+
If none of the bundled instruments fits your use case, you can author your own — see [Defining your own instrument](#defining-your-own-instrument).
|
|
117
127
|
|
|
118
|
-
|
|
128
|
+
## Using an instrument
|
|
129
|
+
|
|
130
|
+
Every smplr instrument follows the same usage pattern. This section covers what's shared across all of them; for instrument-specific options, see the [Instrument reference](#instrument-reference).
|
|
131
|
+
|
|
132
|
+
### Create and load
|
|
119
133
|
|
|
120
134
|
Every smplr instrument is a factory function: call it with an `AudioContext` and an options object to get back an instance.
|
|
121
135
|
|
|
@@ -127,29 +141,18 @@ const piano = SplendidGrandPiano(context, { decayTime: 0.5 });
|
|
|
127
141
|
const marimba = Soundfont(context, { instrument: "marimba" });
|
|
128
142
|
```
|
|
129
143
|
|
|
130
|
-
> **Compatibility note:** All factories also support the `new` keyword — `new SplendidGrandPiano(context)` produces the same instance as `SplendidGrandPiano(context)`. Code from earlier `smplr` versions keeps working unchanged. Editors will mark the `new` form as `@deprecated` to nudge new code toward the call form; both remain supported throughout the 1.x line.
|
|
131
|
-
|
|
132
144
|
#### Wait for audio loading
|
|
133
145
|
|
|
134
|
-
You can start playing notes as soon as one
|
|
146
|
+
You can start playing notes as soon as one sample is loaded. To wait for all of them, await either:
|
|
135
147
|
|
|
136
|
-
|
|
137
|
-
piano.load
|
|
138
|
-
// now the piano is fully loaded
|
|
139
|
-
});
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Since the promise returns the instrument instance, you can create and wait in a single line:
|
|
148
|
+
- `piano.ready` — resolves to `void` (preferred for new code).
|
|
149
|
+
- `piano.load` — resolves to the instrument itself, so you can create and await in one line:
|
|
143
150
|
|
|
144
151
|
```js
|
|
145
152
|
const piano = await SplendidGrandPiano(context).load;
|
|
146
153
|
```
|
|
147
154
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
> **New in 1.0:** prefer `await piano.ready` for new code. It resolves to `void` (not the instrument) and won't be removed — `.load` is kept as a deprecated alias for compatibility.
|
|
151
|
-
|
|
152
|
-
⚠️ In versions lower than 0.8.0 a `loaded()` function was exposed instead.
|
|
155
|
+
> Upgrading from older versions? See [MIGRATE.md](./MIGRATE.md).
|
|
153
156
|
|
|
154
157
|
#### Load progress
|
|
155
158
|
|
|
@@ -168,7 +171,7 @@ console.log(piano.loadProgress); // { loaded: 12, total: 48 }
|
|
|
168
171
|
|
|
169
172
|
`total` is known before loading starts, so you can display a determinate progress bar.
|
|
170
173
|
|
|
171
|
-
|
|
174
|
+
### Shared configuration options
|
|
172
175
|
|
|
173
176
|
All instruments share some configuration options, passed as the second argument to the factory. Every field is optional:
|
|
174
177
|
|
|
@@ -177,52 +180,14 @@ All instruments share some configuration options, passed as the second argument
|
|
|
177
180
|
- `pan`: stereo pan, -1 (full left) to +1 (full right). 0 by default.
|
|
178
181
|
- `destination`: the `AudioNode` the instrument writes to. `AudioContext.destination` by default.
|
|
179
182
|
- `volumeToGain`: a function to map MIDI volume to a linear gain. Uses the MIDI standard curve by default.
|
|
180
|
-
- `storage`: a [storage backend](#
|
|
183
|
+
- `storage`: a [storage backend](#caching-samples) used to fetch sample buffers. `HttpStorage` by default.
|
|
181
184
|
- `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, `
|
|
185
|
+
- `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
186
|
- `onLoadProgress`: a function called after each sample buffer is decoded. Receives `{ loaded, total }` where `total` is the full count known before loading starts.
|
|
184
187
|
- `onStart`: called when a note is dispatched to the audio engine. Receives the started note. See ⚠️ note under [Events](#events) on timing precision.
|
|
185
188
|
- `onEnded`: called when each voice's audio node ends. Receives the started note.
|
|
186
189
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
This package should be compatible with [standardized-audio-context](https://github.com/chrisguttandin/standardized-audio-context):
|
|
190
|
-
|
|
191
|
-
```js
|
|
192
|
-
import { AudioContext } from "standardized-audio-context";
|
|
193
|
-
|
|
194
|
-
const context = new AudioContext();
|
|
195
|
-
const piano = SplendidGrandPiano(context);
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
However, if you are using Typescript, you might need to "force cast" the types:
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
import { Soundfont } from "smplr";
|
|
202
|
-
import { AudioContext as StandardizedAudioContext } from "standardized-audio-context";
|
|
203
|
-
|
|
204
|
-
const context = new StandardizedAudioContext() as unknown as AudioContext;
|
|
205
|
-
const marimba = Soundfont(context, { instrument: "marimba" });
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
In case you need to use the `Reverb` module (or any other module that needs `AudioWorkletNode`) you need to enforce to use the one from `standardized-audio-context` package. Here is how:
|
|
209
|
-
|
|
210
|
-
```ts
|
|
211
|
-
import {
|
|
212
|
-
AudioWorkletNode,
|
|
213
|
-
IAudioContext,
|
|
214
|
-
AudioContext as StandardizedAudioContext,
|
|
215
|
-
} from "standardized-audio-context";
|
|
216
|
-
|
|
217
|
-
window.AudioWorkletNode = AudioWorkletNode as any;
|
|
218
|
-
const context = new StandardizedAudioContext() as unknown as AudioContext;
|
|
219
|
-
|
|
220
|
-
// ... rest of the code
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
See [standardized-audio-context issue #897](https://github.com/chrisguttandin/standardized-audio-context/issues/897) for background on why the cast is required.
|
|
224
|
-
|
|
225
|
-
### Play
|
|
190
|
+
### Play notes
|
|
226
191
|
|
|
227
192
|
#### Start and stop notes
|
|
228
193
|
|
|
@@ -289,7 +254,9 @@ sampler.start({
|
|
|
289
254
|
|
|
290
255
|
If `loop` is true but `loopStart` or `loopEnd` are not specified, 0 and total duration will be used by default, respectively.
|
|
291
256
|
|
|
292
|
-
|
|
257
|
+
### Output
|
|
258
|
+
|
|
259
|
+
#### Volume
|
|
293
260
|
|
|
294
261
|
Instrument `output` attribute represents the main output of the instrument. The `output.volume` getter/setter accepts a number where 0 means no volume, and 127 is max volume without amplification:
|
|
295
262
|
|
|
@@ -298,10 +265,24 @@ piano.output.volume = 80;
|
|
|
298
265
|
piano.output.volume; // => 80
|
|
299
266
|
```
|
|
300
267
|
|
|
301
|
-
`output.setVolume(n)` is kept as a deprecated alias and continues to work.
|
|
302
|
-
|
|
303
268
|
⚠️ `volume` is global to the instrument, but `velocity` is specific for each note.
|
|
304
269
|
|
|
270
|
+
#### Pan, detune, and reverse
|
|
271
|
+
|
|
272
|
+
Every instrument accepts a `pan` option at construction (`-1` = full left, `+1` = full right):
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
const drums = DrumMachine(context, { instrument: "TR-808", pan: -0.5 });
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Two universal setters mutate the playback defaults in place. They apply to notes scheduled **after** the call; in-flight notes are unaffected.
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
sampler.setDetune(100); // semitone up (100 cents) for all future notes
|
|
282
|
+
sampler.setReverse(true); // play samples reversed for all future notes
|
|
283
|
+
sampler.setReverse(false); // back to forward playback
|
|
284
|
+
```
|
|
285
|
+
|
|
305
286
|
#### MIDI CC
|
|
306
287
|
|
|
307
288
|
Set and read MIDI Control Change values on the instrument:
|
|
@@ -314,20 +295,28 @@ piano.setCC(64, 0); // sustain pedal off
|
|
|
314
295
|
|
|
315
296
|
Unset CCs default to `0` (matches MIDI's "undefined controller defaults to 0" convention).
|
|
316
297
|
|
|
317
|
-
|
|
298
|
+
### Effects
|
|
299
|
+
|
|
300
|
+
#### Reverb
|
|
318
301
|
|
|
319
|
-
|
|
302
|
+
A packaged version of the [DattorroReverbNode](https://github.com/khoin/DattorroReverbNode) algorithmic reverb is included.
|
|
303
|
+
|
|
304
|
+
Use `output.addEffect(name, effect, mix)` to connect an effect using a send bus:
|
|
320
305
|
|
|
321
306
|
```js
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
307
|
+
import { Reverb, SplendidGrandPiano } from "smplr";
|
|
308
|
+
const reverb = Reverb(context);
|
|
309
|
+
const piano = SplendidGrandPiano(context, { volume });
|
|
310
|
+
piano.output.addEffect("reverb", reverb, 0.2);
|
|
326
311
|
```
|
|
327
312
|
|
|
328
|
-
|
|
313
|
+
To change the mix level, use `output.setEffectMix(name, mix)`:
|
|
329
314
|
|
|
330
|
-
|
|
315
|
+
```js
|
|
316
|
+
piano.output.setEffectMix("reverb", 0.5);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Events
|
|
331
320
|
|
|
332
321
|
Two events are supported `onStart` and `onEnded`. Both callbacks will receive as parameter started note.
|
|
333
322
|
|
|
@@ -358,30 +347,18 @@ Global callbacks will be invoked regardless of whether local events are defined.
|
|
|
358
347
|
|
|
359
348
|
⚠️ The invocation time of `onStart` is not exact: it fires slightly before the audio actually starts, by up to the scheduler's lookahead window (200ms by default; configurable via the `scheduler` option — see [Shared configuration options](#shared-configuration-options)).
|
|
360
349
|
|
|
361
|
-
###
|
|
362
|
-
|
|
363
|
-
#### Reverb
|
|
364
|
-
|
|
365
|
-
A packaged version of the [DattorroReverbNode](https://github.com/khoin/DattorroReverbNode) algorithmic reverb is included.
|
|
366
|
-
|
|
367
|
-
Use `output.addEffect(name, effect, mix)` to connect an effect using a send bus:
|
|
368
|
-
|
|
369
|
-
```js
|
|
370
|
-
import { Reverb, SplendidGrandPiano } from "smplr";
|
|
371
|
-
const reverb = new Reverb(context);
|
|
372
|
-
const piano = SplendidGrandPiano(context, { volume });
|
|
373
|
-
piano.output.addEffect("reverb", reverb, 0.2);
|
|
374
|
-
```
|
|
350
|
+
### Dispose
|
|
375
351
|
|
|
376
|
-
|
|
352
|
+
When you're done with an instrument, call `dispose()` to stop all voices, tear down the audio graph, and stop the scheduler. The instance must not be used after this call.
|
|
377
353
|
|
|
378
354
|
```js
|
|
379
|
-
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
const piano = SplendidGrandPiano(context);
|
|
357
|
+
return () => piano.dispose();
|
|
358
|
+
}, []);
|
|
380
359
|
```
|
|
381
360
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
### Cache requests
|
|
361
|
+
### Caching samples
|
|
385
362
|
|
|
386
363
|
The default sample sets are hosted on GitHub Pages, which rate-limits requests per second. That can be a problem, especially in a development environment with hot reload (most React frameworks).
|
|
387
364
|
|
|
@@ -391,16 +368,54 @@ To cache samples in the browser, use a `CacheStorage` object:
|
|
|
391
368
|
import { SplendidGrandPiano, CacheStorage } from "smplr";
|
|
392
369
|
|
|
393
370
|
const context = new AudioContext();
|
|
394
|
-
const storage =
|
|
371
|
+
const storage = CacheStorage();
|
|
395
372
|
// First time the instrument loads, will fetch the samples from http. Subsequent times from cache.
|
|
396
373
|
const piano = SplendidGrandPiano(context, { storage });
|
|
397
374
|
```
|
|
398
375
|
|
|
399
376
|
⚠️ `CacheStorage` is based on the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache) and only works in secure environments that run over `https`. Check your framework's documentation for local-HTTPS setup — for example [next-dev-https](https://www.npmjs.com/package/next-dev-https) for Next.js or [vite-plugin-mkcert](https://github.com/liuweiGL/vite-plugin-mkcert) for Vite.
|
|
400
377
|
|
|
378
|
+
### Using with standardized-audio-context
|
|
379
|
+
|
|
380
|
+
This package should be compatible with [standardized-audio-context](https://github.com/chrisguttandin/standardized-audio-context):
|
|
381
|
+
|
|
382
|
+
```js
|
|
383
|
+
import { AudioContext } from "standardized-audio-context";
|
|
384
|
+
|
|
385
|
+
const context = new AudioContext();
|
|
386
|
+
const piano = SplendidGrandPiano(context);
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
However, if you are using Typescript, you might need to "force cast" the types:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
import { Soundfont } from "smplr";
|
|
393
|
+
import { AudioContext as StandardizedAudioContext } from "standardized-audio-context";
|
|
394
|
+
|
|
395
|
+
const context = new StandardizedAudioContext() as unknown as AudioContext;
|
|
396
|
+
const marimba = Soundfont(context, { instrument: "marimba" });
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
In case you need to use the `Reverb` module (or any other module that needs `AudioWorkletNode`) you need to enforce to use the one from `standardized-audio-context` package. Here is how:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
import {
|
|
403
|
+
AudioWorkletNode,
|
|
404
|
+
IAudioContext,
|
|
405
|
+
AudioContext as StandardizedAudioContext,
|
|
406
|
+
} from "standardized-audio-context";
|
|
407
|
+
|
|
408
|
+
window.AudioWorkletNode = AudioWorkletNode as any;
|
|
409
|
+
const context = new StandardizedAudioContext() as unknown as AudioContext;
|
|
410
|
+
|
|
411
|
+
// ... rest of the code
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
See [standardized-audio-context issue #897](https://github.com/chrisguttandin/standardized-audio-context/issues/897) for background on why the cast is required.
|
|
415
|
+
|
|
401
416
|
## Sequencer
|
|
402
417
|
|
|
403
|
-
`Sequencer` schedules notes from one or more tracks against any smplr instrument with sample-accurate timing.
|
|
418
|
+
`Sequencer` schedules notes from one or more tracks against any smplr instrument with sample-accurate timing.
|
|
404
419
|
|
|
405
420
|
```js
|
|
406
421
|
import { Sequencer, SplendidGrandPiano, DrumMachine } from "smplr";
|
|
@@ -409,7 +424,7 @@ const context = new AudioContext();
|
|
|
409
424
|
const piano = SplendidGrandPiano(context);
|
|
410
425
|
const drums = DrumMachine(context, { instrument: "TR-808" });
|
|
411
426
|
|
|
412
|
-
const seq =
|
|
427
|
+
const seq = Sequencer(context, { bpm: 120, loop: true });
|
|
413
428
|
|
|
414
429
|
seq.addTrack(piano, [
|
|
415
430
|
{ note: "C4", at: "1:1", duration: "4n" },
|
|
@@ -446,19 +461,55 @@ Note positions and durations accept several formats:
|
|
|
446
461
|
#### Constructor options
|
|
447
462
|
|
|
448
463
|
```js
|
|
449
|
-
const seq =
|
|
464
|
+
const seq = Sequencer(context, {
|
|
450
465
|
bpm: 120, // default 120
|
|
451
466
|
ppq: 480, // pulses per quarter note, default 480
|
|
452
|
-
timeSignature: 4, //
|
|
467
|
+
timeSignature: 4, // accepts `4` (→ 4/4) or `{ numerator, denominator }`
|
|
453
468
|
loop: false, // default false
|
|
454
469
|
loopStart: 0, // loop start position (ticks or string)
|
|
455
470
|
loopEnd: "2:1", // loop end position; defaults to end of longest track
|
|
456
471
|
lookaheadMs: 200, // scheduling lookahead, default 200
|
|
457
472
|
intervalMs: 50, // flush interval, default 50
|
|
458
473
|
humanize: { timingMs: 10, velocity: 8 }, // optional randomisation
|
|
474
|
+
stepSize: "16n", // optional: emit "step" events at this interval
|
|
459
475
|
});
|
|
460
476
|
```
|
|
461
477
|
|
|
478
|
+
`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.
|
|
479
|
+
|
|
480
|
+
#### Tracks
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
seq.addTrack(piano, notes); // append a track
|
|
484
|
+
seq.addTrack(drums, notes, { id: "drums", volume: 0.8 }); // with options
|
|
485
|
+
seq.removeTrack(piano); // remove by instrument reference
|
|
486
|
+
seq.clearTracks(); // remove every track
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`addTrack`'s third argument accepts:
|
|
490
|
+
|
|
491
|
+
| Field | Type | Description |
|
|
492
|
+
| ---------- | ------------------------------------------- | --------------------------------------------------------------------- |
|
|
493
|
+
| `id` | `string` | Stable id for `setTrackVolume` / `muteTrack` / `soloTrack`. |
|
|
494
|
+
| `humanize` | `{ timingMs?: number; velocity?: number }` | Per-track humanize. Overrides the sequencer-level setting when set. |
|
|
495
|
+
| `volume` | `number` | Multiplicative velocity scalar (default 1). `0.5` halves velocities. |
|
|
496
|
+
| `muted` | `boolean` | When true, this track does not dispatch notes. |
|
|
497
|
+
| `solo` | `boolean` | When true, only soloed tracks play. |
|
|
498
|
+
|
|
499
|
+
After `setPatterns` is called (see [Pattern chain](#pattern-chain-song-mode)), `addTrack` / `removeTrack` / `clearTracks` throw — the chain is owned by the patterns array.
|
|
500
|
+
|
|
501
|
+
#### Track mixer
|
|
502
|
+
|
|
503
|
+
```js
|
|
504
|
+
seq.setTrackVolume("drums", 0.6);
|
|
505
|
+
seq.muteTrack("drums");
|
|
506
|
+
seq.unmuteTrack("drums");
|
|
507
|
+
seq.soloTrack("lead");
|
|
508
|
+
seq.unsoloTrack("lead");
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
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.
|
|
512
|
+
|
|
462
513
|
#### Playback
|
|
463
514
|
|
|
464
515
|
```js
|
|
@@ -481,12 +532,17 @@ seq.stopNote("intro-c", time); // stop at a scheduled time
|
|
|
481
532
|
|
|
482
533
|
```js
|
|
483
534
|
seq.bpm = 140; // change BPM live, no glitch
|
|
484
|
-
seq.timeSignature = 3; //
|
|
535
|
+
seq.timeSignature = 3; // 3/4 (number → { numerator: 3, denominator: 4 })
|
|
536
|
+
seq.timeSignature = { numerator: 7, denominator: 8 }; // 7/8
|
|
537
|
+
|
|
538
|
+
seq.timeSignature; // → { numerator: 7, denominator: 8 }
|
|
485
539
|
|
|
486
540
|
seq.position; // current position as "bar:beat:tick" string
|
|
487
541
|
seq.position = "3:1"; // seek while playing or stopped
|
|
488
542
|
```
|
|
489
543
|
|
|
544
|
+
The `"beat"` event fires once per denominator-defined note: 4/4 → 4 beats per bar, 6/8 → 6 beats per bar, etc.
|
|
545
|
+
|
|
490
546
|
#### Loop
|
|
491
547
|
|
|
492
548
|
```js
|
|
@@ -531,12 +587,18 @@ seq.on("beat", (beat, time) => {
|
|
|
531
587
|
seq.on("bar", (bar, time) => {
|
|
532
588
|
ui.updateBar(bar);
|
|
533
589
|
});
|
|
590
|
+
seq.on("step", (stepIndex, time) => {
|
|
591
|
+
ui.flashStep(stepIndex); // only fires when `stepSize` is set in options
|
|
592
|
+
});
|
|
534
593
|
seq.on("loop", () => {
|
|
535
594
|
console.log("looped");
|
|
536
595
|
});
|
|
537
596
|
seq.on("end", () => {
|
|
538
597
|
console.log("done");
|
|
539
598
|
});
|
|
599
|
+
seq.on("patternChange", (patternIndex, time) => {
|
|
600
|
+
ui.highlightPattern(patternIndex); // fires when the chain advances
|
|
601
|
+
});
|
|
540
602
|
seq.on("start", () => {});
|
|
541
603
|
seq.on("stop", () => {});
|
|
542
604
|
seq.on("pause", () => {});
|
|
@@ -544,6 +606,9 @@ seq.on("pause", () => {});
|
|
|
544
606
|
seq.off("beat", handler); // remove a listener
|
|
545
607
|
```
|
|
546
608
|
|
|
609
|
+
The `"step"` event only fires when the sequencer was constructed with `stepSize` (e.g. `"16n"`).
|
|
610
|
+
The `"patternChange"` event only fires when more than one pattern is in the chain.
|
|
611
|
+
|
|
547
612
|
#### Note events
|
|
548
613
|
|
|
549
614
|
`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 +646,7 @@ seq.addTrack(piano, [
|
|
|
581
646
|
Add subtle randomisation to timing and velocity for a more natural feel:
|
|
582
647
|
|
|
583
648
|
```js
|
|
584
|
-
const seq =
|
|
649
|
+
const seq = Sequencer(context, {
|
|
585
650
|
bpm: 90,
|
|
586
651
|
humanize: { timingMs: 12, velocity: 8 },
|
|
587
652
|
});
|
|
@@ -590,9 +655,63 @@ const seq = new Sequencer(context, {
|
|
|
590
655
|
- `timingMs`: maximum random offset in milliseconds (±). Default 0.
|
|
591
656
|
- `velocity`: maximum random offset in MIDI velocity units (±). Default 0.
|
|
592
657
|
|
|
658
|
+
Per-track humanize (passed to `addTrack`) overrides the global setting:
|
|
659
|
+
|
|
660
|
+
```js
|
|
661
|
+
seq.addTrack(piano, notes, { humanize: { timingMs: 0, velocity: 0 } });
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
#### SequencerNote fields
|
|
665
|
+
|
|
666
|
+
| Field | Type | Description |
|
|
667
|
+
| ---------------------- | ------------------- | ---------------------------------------------------------------------------- |
|
|
668
|
+
| `note` | `string \| number` | Note name or MIDI number. |
|
|
669
|
+
| `at` | `string \| number` | Musical position (ticks or `"bar:beat[.frac][:ticks]"` / `"4n"` / `"1m"`). |
|
|
670
|
+
| `duration` | `string \| number?` | Duration; omit for a one-shot trigger. |
|
|
671
|
+
| `velocity` | `number?` | Velocity 0–127. Default 100. |
|
|
672
|
+
| `id` | `string \| number?` | Used as `noteId` in `noteOn` / `noteOff` events. Default: array index. |
|
|
673
|
+
| `chance` | `number?` | Probability 0–100 that this note fires on each pass. Re-rolled on every loop. |
|
|
674
|
+
| `ratchet` | `number?` | Expand into N sub-notes over `duration` (requires `duration`). |
|
|
675
|
+
| `ratchetVelocityDecay` | `number?` | Per-step velocity decay; each sub-note scaled by `(1 - decay)^i`. |
|
|
676
|
+
|
|
677
|
+
Example:
|
|
678
|
+
|
|
679
|
+
```js
|
|
680
|
+
seq.addTrack(drums, [
|
|
681
|
+
{ note: "hat", at: "1:4", duration: "8n", ratchet: 4, ratchetVelocityDecay: 0.2 },
|
|
682
|
+
{ note: "snare", at: "1:2", chance: 50 }, // fires 50% of the time
|
|
683
|
+
]);
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
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")`.
|
|
687
|
+
|
|
688
|
+
#### Pattern chain (song mode)
|
|
689
|
+
|
|
690
|
+
For multi-pattern arrangements (intro → verse → chorus), use `setPatterns`:
|
|
691
|
+
|
|
692
|
+
```js
|
|
693
|
+
seq.setPatterns([
|
|
694
|
+
{ tracks: [{ instrument: drums, notes: introNotes }], loopEnd: "1m" },
|
|
695
|
+
{ tracks: [{ instrument: drums, notes: verseNotes }], loopEnd: "2m" },
|
|
696
|
+
{ tracks: [{ instrument: drums, notes: chorusNotes }], loopEnd: "2m" },
|
|
697
|
+
]);
|
|
698
|
+
|
|
699
|
+
seq.chainOrder = [0, 1, 2, 1, 2]; // intro, verse, chorus, verse, chorus
|
|
700
|
+
seq.loop = true; // loop the whole chain
|
|
701
|
+
seq.start();
|
|
702
|
+
|
|
703
|
+
seq.on("patternChange", (idx) => ui.highlightPattern(idx));
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
- Each pattern's `tracks` entries accept the same `AddTrackOptions` (`id`, `humanize`, `volume`, `muted`, `solo`) as `addTrack`.
|
|
707
|
+
- `loopEnd` is per-pattern and defaults to the longest track in that pattern.
|
|
708
|
+
- `chainOrder` defaults to `[0, 1, …, n-1]`. Setting it lets you repeat or reorder patterns without duplicating data.
|
|
709
|
+
- With `loop: false` the chain plays once and emits `"end"`; with `loop: true` it cycles indefinitely and emits `"loop"` each time it wraps.
|
|
710
|
+
- Track mixer methods (`muteTrack`, `setTrackVolume`, etc.) operate on the currently-playing pattern — `muteTrack("lead")` only affects the pattern that owns the `"lead"` track.
|
|
711
|
+
|
|
593
712
|
---
|
|
594
713
|
|
|
595
|
-
##
|
|
714
|
+
## Offline rendering
|
|
596
715
|
|
|
597
716
|
Render audio offline (faster than real-time) and export it as a WAV file. Uses `OfflineAudioContext` under the hood.
|
|
598
717
|
|
|
@@ -641,7 +760,7 @@ If you already have an instrument loaded, pass the same `SampleLoader` to avoid
|
|
|
641
760
|
```js
|
|
642
761
|
import { SplendidGrandPiano, SampleLoader, renderOffline } from "smplr";
|
|
643
762
|
|
|
644
|
-
const loader =
|
|
763
|
+
const loader = SampleLoader(audioContext);
|
|
645
764
|
const piano = SplendidGrandPiano(audioContext, { loader });
|
|
646
765
|
await piano.load;
|
|
647
766
|
|
|
@@ -671,23 +790,9 @@ This will download a WAV file you can attach to your issue or pull request.
|
|
|
671
790
|
|
|
672
791
|
---
|
|
673
792
|
|
|
674
|
-
##
|
|
675
|
-
|
|
676
|
-
### Available instruments
|
|
793
|
+
## Instrument reference
|
|
677
794
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
| Factory | Names helper |
|
|
681
|
-
| --------------- | ---------------------------------------------- |
|
|
682
|
-
| `Soundfont` | `getSoundfontNames(): string[]` |
|
|
683
|
-
| `ElectricPiano` | `getElectricPianoNames(): string[]` |
|
|
684
|
-
| `Mallet` | `getMalletNames(): string[]` |
|
|
685
|
-
| `Mellotron` | `getMellotronNames(): string[]` |
|
|
686
|
-
| `DrumMachine` | `getDrumMachineNames(): string[]` |
|
|
687
|
-
| `Smolken` | `getSmolkenNames(): string[]` |
|
|
688
|
-
| `Versilian` | `getVersilianInstruments(): Promise<string[]>` |
|
|
689
|
-
|
|
690
|
-
`getVersilianInstruments` is async because the catalog is fetched from the network on first call (cached thereafter).
|
|
795
|
+
Detailed configuration for each bundled instrument. For the shared API (load, play, output, effects, events), see [Using an instrument](#using-an-instrument).
|
|
691
796
|
|
|
692
797
|
### Sampler
|
|
693
798
|
|
|
@@ -905,6 +1010,61 @@ drums.start("kick"); // Play the first sample of the group
|
|
|
905
1010
|
drums.start("kick-1"); // Play this specific sample
|
|
906
1011
|
```
|
|
907
1012
|
|
|
1013
|
+
### DrumAbuse
|
|
1014
|
+
|
|
1015
|
+
Sampled instrument for the [Synthabuse](https://www.youtube.com/watch?v=Ay-U9eYKmGA) drum-machine collection — 5 packs covering ~210 classic drum machines and synths. Samples hosted at `smpldsnds.github.io/drum-abuse-{pack}/`.
|
|
1016
|
+
|
|
1017
|
+
Two source modes: load a single machine's full kit, or load a cross-machine instrument list from a pack.
|
|
1018
|
+
|
|
1019
|
+
#### Machine mode
|
|
1020
|
+
|
|
1021
|
+
```js
|
|
1022
|
+
import { DrumAbuse, getDrumAbuseMachineNames } from "smplr";
|
|
1023
|
+
|
|
1024
|
+
const machines = getDrumAbuseMachineNames(); // ~210 machine ids
|
|
1025
|
+
|
|
1026
|
+
const context = new AudioContext();
|
|
1027
|
+
const drums = DrumAbuse(context, {
|
|
1028
|
+
source: { kind: "machine", machine: "roland-tr-808" },
|
|
1029
|
+
});
|
|
1030
|
+
await drums.load;
|
|
1031
|
+
|
|
1032
|
+
drums.start({ note: "kick" });
|
|
1033
|
+
|
|
1034
|
+
// Samples are grouped by instrument name, like DrumMachine:
|
|
1035
|
+
drums.getGroupNames(); // => ["kick", "snare", "hi-hat", ...]
|
|
1036
|
+
drums.getSampleNamesForGroup("kick"); // => ["kick/1", "kick/2", ...]
|
|
1037
|
+
drums.start({ note: "kick" }); // first sample in the group
|
|
1038
|
+
drums.start({ note: "kick/1" }); // a specific sample
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
If a machine has more than one sample set, pass `set` to pick a specific one. Omit to load the first set.
|
|
1042
|
+
|
|
1043
|
+
```js
|
|
1044
|
+
const drums = DrumAbuse(context, {
|
|
1045
|
+
source: { kind: "machine", machine: "roland-tr-808", set: "kit-a" },
|
|
1046
|
+
});
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
#### Pack mode
|
|
1050
|
+
|
|
1051
|
+
A pack is a cross-machine catalog of named instruments (e.g. all the kicks across `vol1`). Pass `source: { kind: "pack", pack, instrument }`:
|
|
1052
|
+
|
|
1053
|
+
```js
|
|
1054
|
+
import {
|
|
1055
|
+
DrumAbuse,
|
|
1056
|
+
getDrumAbusePackNames,
|
|
1057
|
+
getDrumAbuseMachinesForPack,
|
|
1058
|
+
} from "smplr";
|
|
1059
|
+
|
|
1060
|
+
getDrumAbusePackNames(); // => ["vol1", "vol2", "vol3", "vol4", "vol5"]
|
|
1061
|
+
getDrumAbuseMachinesForPack("vol1"); // => machine ids in vol1
|
|
1062
|
+
|
|
1063
|
+
const drums = DrumAbuse(new AudioContext(), {
|
|
1064
|
+
source: { kind: "pack", pack: "vol1", instrument: "bass-drum" },
|
|
1065
|
+
});
|
|
1066
|
+
```
|
|
1067
|
+
|
|
908
1068
|
### Smolken double bass
|
|
909
1069
|
|
|
910
1070
|
```js
|
|
@@ -933,16 +1093,16 @@ const context = new AudioContext();
|
|
|
933
1093
|
const versilian = Versilian(context, { instrument: instrumentNames[0] });
|
|
934
1094
|
```
|
|
935
1095
|
|
|
936
|
-
###
|
|
1096
|
+
### Soundfont2
|
|
937
1097
|
|
|
938
|
-
Sampler capable of reading .sf2 files directly
|
|
1098
|
+
Sampler capable of reading .sf2 files directly.
|
|
939
1099
|
|
|
940
1100
|
```ts
|
|
941
|
-
import {
|
|
1101
|
+
import { Soundfont2 } from "smplr";
|
|
942
1102
|
import { SoundFont2 } from "soundfont2";
|
|
943
1103
|
|
|
944
1104
|
const context = new AudioContext();
|
|
945
|
-
const sampler =
|
|
1105
|
+
const sampler = Soundfont2(context, {
|
|
946
1106
|
url: "https://smpldsnds.github.io/soundfonts/soundfonts/galaxy-electric-pianos.sf2",
|
|
947
1107
|
createSoundfont: (data) => new SoundFont2(data),
|
|
948
1108
|
});
|
|
@@ -958,6 +1118,16 @@ sampler.load.then(() => {
|
|
|
958
1118
|
|
|
959
1119
|
Still limited support. API may vary.
|
|
960
1120
|
|
|
1121
|
+
## Defining your own instrument
|
|
1122
|
+
|
|
1123
|
+
If none of the bundled instruments fits your use case, you can author your own with the `Instrument` builder and the `Smplr` interface.
|
|
1124
|
+
|
|
1125
|
+
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.
|
|
1126
|
+
|
|
1127
|
+
## Upgrading
|
|
1128
|
+
|
|
1129
|
+
`smplr` is approaching 1.0; pre-1.0 APIs keep working as deprecated aliases. See [MIGRATE.md](./MIGRATE.md) for the full compatibility table and [CHANGELOG](https://github.com/danigb/smplr/blob/main/CHANGELOG.md) for per-release detail.
|
|
1130
|
+
|
|
961
1131
|
## License
|
|
962
1132
|
|
|
963
1133
|
MIT License
|