@jpdutoit/squelchy 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/docs/GUIDE.md +398 -0
- package/docs/MODULES.md +57 -57
- package/docs/modules.json +3332 -0
- package/index.d.ts +17 -10
- package/index.js.map +2 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @jpdutoit/squelchy
|
|
2
2
|
|
|
3
|
-
The modules behind [Squelchy](https://squelchy.studio), the modular synth in your browser, as plain Web Audio building blocks. Oscillators, filters, envelopes, effects and drum kits behave like native nodes: you `connect()` them, and
|
|
3
|
+
The modules behind [Squelchy](https://squelchy.studio), the modular synth in your browser, as plain Web Audio building blocks. Oscillators, filters, envelopes, effects and drum kits behave like native nodes: you `connect()` them, and parameters that drive the sound 1:1 are real `AudioParam`s you can automate.
|
|
4
4
|
|
|
5
5
|
- **Zero dependencies, zero asset setup.** AudioWorklet processors are inlined and registered from blob URLs.
|
|
6
6
|
- **Tree-shakeable.** Import one module and only its worklets end up in your bundle.
|
|
@@ -26,7 +26,7 @@ lfo.out.connect(vco.modDetune); // vibrato
|
|
|
26
26
|
vco.out.connect(delay.in);
|
|
27
27
|
delay.out.connect(ctx.destination);
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// Parameters that map 1:1 onto the DSP are native AudioParams.
|
|
30
30
|
delay.params.mix.linearRampToValueAtTime(0.8, ctx.currentTime + 2);
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -67,7 +67,8 @@ bd.trigger(now(ctx)); // times are typed Seconds; now(ctx) is ctx.currentTime
|
|
|
67
67
|
|
|
68
68
|
## Reference
|
|
69
69
|
|
|
70
|
-
- `docs/
|
|
71
|
-
- `
|
|
70
|
+
- `docs/GUIDE.md`: recipes for scheduling (transport options, automation at musical positions, synced visuals), sequencing synth voices, `pulse()` actions, units, sample kits and offline rendering.
|
|
71
|
+
- `docs/MODULES.md`: every module's ports, parameters, ranges and defaults, and which parameters are automatable (the rest change instantly with `set()`).
|
|
72
|
+
- `docs/modules.json`: the same reference keyed by export name, for `jq` (e.g. `jq '.Delay' node_modules/@jpdutoit/squelchy/docs/modules.json`).
|
|
72
73
|
- Types need the `DOM` lib in your `tsconfig.json`.
|
|
73
74
|
- Under a strict Content-Security-Policy, allow `worker-src blob:` so the worklets can load.
|
package/docs/GUIDE.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# @jpdutoit/squelchy: guide
|
|
2
|
+
|
|
3
|
+
Short recipes for the parts the module reference doesn't cover: working with modules, typed units, the transport for scheduling, sequencing synth voices, one-shot actions, sample kits and offline rendering. Every TypeScript example here is compile-checked against the published types before each release.
|
|
4
|
+
|
|
5
|
+
- `docs/MODULES.md`: every module's ports and parameters, for reading.
|
|
6
|
+
- `docs/modules.json`: the same reference as JSON, for `jq` and scripts ([see below](#module-reference-with-jq)).
|
|
7
|
+
|
|
8
|
+
## Modules
|
|
9
|
+
|
|
10
|
+
`X.create(ctx, params?)` builds a module. Each port is a property named by its slug, so you wire modules with plain `connect()`. `input(slug)` and `output(slug)` return the same objects.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { preload, Vco, Lfo, Vcf, semitones, hz, note } from '@jpdutoit/squelchy';
|
|
14
|
+
|
|
15
|
+
const ctx = new AudioContext();
|
|
16
|
+
await preload(ctx); // once per context, before any create()
|
|
17
|
+
|
|
18
|
+
const vco = Vco.create(ctx, { offset: semitones(45) }); // unpatched, offset is the note: 45 = A2
|
|
19
|
+
const lfo = Lfo.create(ctx, { frequency: hz(0.2), amount: semitones(18) });
|
|
20
|
+
const filter = Vcf.create(ctx, { note: note('C6'), resonance: 4 });
|
|
21
|
+
|
|
22
|
+
vco.out.connect(filter.in);
|
|
23
|
+
lfo.out.connect(filter.modFreq); // CV inputs sum onto the knob's value
|
|
24
|
+
filter.out.connect(ctx.destination);
|
|
25
|
+
|
|
26
|
+
// Same connection, spelled out:
|
|
27
|
+
lfo.output('out').connect(filter.input('modFreq'));
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Changing parameters: `set()` vs `params`
|
|
31
|
+
|
|
32
|
+
- **`params.<key>`** is a native `AudioParam` for every parameter that drives the DSP 1:1. Use it for anything timed: ramps, curves and `setValueAtTime`.
|
|
33
|
+
- **`set(key, value)`** changes any parameter immediately. Some parameters are `set()`-only: switches (enums), and values that rebuild something internally (a waveshaper curve, a filter bank, a resonance mapping). The types enforce this, and the module reference marks every parameter as automatable or `set()`-only.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { preload, Vcf, note } from '@jpdutoit/squelchy';
|
|
37
|
+
|
|
38
|
+
const ctx = new AudioContext();
|
|
39
|
+
await preload(ctx);
|
|
40
|
+
const filter = Vcf.create(ctx);
|
|
41
|
+
|
|
42
|
+
filter.set('filterType', 'bandpass'); // switch: set() only
|
|
43
|
+
filter.set('resonance', 8); // set() only
|
|
44
|
+
filter.params.note.setTargetAtTime(note('C7'), ctx.currentTime, 0.5); // native automation
|
|
45
|
+
|
|
46
|
+
// @ts-expect-error resonance is not a native AudioParam
|
|
47
|
+
filter.params.resonance;
|
|
48
|
+
|
|
49
|
+
filter.dispose(); // disconnect and release when you're done
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Units
|
|
53
|
+
|
|
54
|
+
Notes, semitones, Hz, seconds and beats are distinct types, so passing seconds where a note is expected won't compile. At runtime they are plain numbers with zero overhead. Dimensionless values (mix, feedback, drive) are plain `number`.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import {
|
|
58
|
+
note, noteToHz, hzToNote, transpose, interval, semitones, cents, centsToSemitones,
|
|
59
|
+
db, dbToGain, gainToDb, ms, msToSeconds, sec, hz, now, Vco,
|
|
60
|
+
type Seconds,
|
|
61
|
+
} from '@jpdutoit/squelchy';
|
|
62
|
+
|
|
63
|
+
const ctx = new AudioContext();
|
|
64
|
+
|
|
65
|
+
note('A4'); // 69 (MIDI numbering, 69 = A4 = 440 Hz)
|
|
66
|
+
noteToHz(note('A4')); // 440
|
|
67
|
+
hzToNote(hz(261.63)); // ≈ 60 (C4)
|
|
68
|
+
transpose(note('C4'), semitones(7)); // 67 (G4)
|
|
69
|
+
interval(note('C4'), note('G4')); // 7 semitones
|
|
70
|
+
centsToSemitones(cents(50)); // 0.5
|
|
71
|
+
dbToGain(db(-6)); // ≈ 0.5
|
|
72
|
+
gainToDb(0.5); // ≈ -6 dB
|
|
73
|
+
msToSeconds(ms(250)); // 0.25 s
|
|
74
|
+
|
|
75
|
+
// Arithmetic gives back a plain number; re-brand the result with a helper or a cast.
|
|
76
|
+
const later: Seconds = sec(now(ctx) + 0.5);
|
|
77
|
+
|
|
78
|
+
// @ts-expect-error a bare number is not Semitones; write semitones(60)
|
|
79
|
+
Vco.create(ctx, { offset: 60 });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Pitch signals.** V/OCT outputs are typed `Signal<Note>`: absolute key positions, 69 = A4, 1 unit = 1 semitone. Web Audio's `connect()` can't check its destination, so the type protects the functions you write. `asPitch()` marks a deliberate crossover, like using an LFO as a pitch source:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { preload, Lfo, Vco, asPitch, type Signal, type Note } from '@jpdutoit/squelchy';
|
|
86
|
+
|
|
87
|
+
const ctx = new AudioContext();
|
|
88
|
+
await preload(ctx);
|
|
89
|
+
|
|
90
|
+
function playOn(pitch: Signal<Note>) {
|
|
91
|
+
const vco = Vco.create(ctx);
|
|
92
|
+
pitch.connect(vco.voct);
|
|
93
|
+
return vco;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const lfo = Lfo.create(ctx);
|
|
97
|
+
// @ts-expect-error an LFO's output is CV, not pitch
|
|
98
|
+
playOn(lfo.out);
|
|
99
|
+
playOn(asPitch(lfo.out)); // deliberate: you said so
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Scheduling with the transport
|
|
103
|
+
|
|
104
|
+
Musical positions are plain numbers in **beats** (quarter notes): bar 2 of a 4/4 song starts at beat 4, and a 16th note is 0.25. Audio times are `ctx.currentTime` seconds, as everywhere in Web Audio. Events are written ahead of time with exact start times, so timing doesn't depend on the main thread.
|
|
105
|
+
|
|
106
|
+
Bars and beats count from 0, as on a timeline: `{ bar: 0 }` is the first bar and `{ bar: 1 }` starts at beat 4.
|
|
107
|
+
|
|
108
|
+
A **pattern** is a plain array, one element per step. A **voice** is anything with `trigger(time, hit)`, or a plain function `(time, hit) => void`.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { createTransport, steps, beats, Kick, Hat } from '@jpdutoit/squelchy';
|
|
112
|
+
|
|
113
|
+
const ctx = new AudioContext();
|
|
114
|
+
const transport = createTransport(ctx, { bpm: 120, beatsPerBar: 4 });
|
|
115
|
+
const kick = Kick.create(ctx);
|
|
116
|
+
const hat = Hat.create(ctx);
|
|
117
|
+
kick.connect(ctx.destination);
|
|
118
|
+
hat.connect(ctx.destination);
|
|
119
|
+
|
|
120
|
+
transport.play(steps('x... x... x... x...'), kick); // spaces and | are ignored
|
|
121
|
+
transport.play(steps('..x. ..x. ..x. ..x.'), hat, {
|
|
122
|
+
every: beats(0.25), // step length in beats (default: 16ths)
|
|
123
|
+
swing: 0.3, // 0..1, delays every odd step
|
|
124
|
+
gate: 0.5, // note length as a fraction of a step
|
|
125
|
+
start: 'bar', // 'now' | 'beat' | 'bar' | { bar: n } | { beat: n }; default 'bar'
|
|
126
|
+
loop: true, // true = forever, false = once, n = n times
|
|
127
|
+
});
|
|
128
|
+
transport.start();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Pattern steps
|
|
132
|
+
|
|
133
|
+
- **Hits:** any value. `true` from `steps()`, a `Note` from `notes()`, or whatever your voice understands.
|
|
134
|
+
- **Rests:** `null`, `undefined`, `false`, `0`, `'.'` and `'-'`.
|
|
135
|
+
- **Per-step velocity and length:** `{ value, velocity, length }`, where `length` is in steps.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { createTransport, notes, note, beats, NoteCv, type Step, type Note } from '@jpdutoit/squelchy';
|
|
139
|
+
|
|
140
|
+
const ctx = new AudioContext();
|
|
141
|
+
const transport = createTransport(ctx);
|
|
142
|
+
const cv = NoteCv.create(ctx);
|
|
143
|
+
|
|
144
|
+
const melody = notes('C3 . Eb3 G3 - C4 . G3 .'); // '.' and '-' are rests
|
|
145
|
+
const bass: Step<Note>[] = [
|
|
146
|
+
{ value: note('C2'), velocity: 1, length: 3 }, // accented, held for 3 steps
|
|
147
|
+
null, null,
|
|
148
|
+
{ value: note('C2'), velocity: 0.5 },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
transport.play(melody, cv);
|
|
152
|
+
transport.play(bass, cv, { every: beats(0.5) });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Plain functions work as voices, so any existing Web Audio code can follow a pattern:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { createTransport, steps } from '@jpdutoit/squelchy';
|
|
159
|
+
|
|
160
|
+
const ctx = new AudioContext();
|
|
161
|
+
const transport = createTransport(ctx);
|
|
162
|
+
|
|
163
|
+
transport.play(steps('x.x. x..x'), (time, hit) => {
|
|
164
|
+
const osc = ctx.createOscillator();
|
|
165
|
+
osc.frequency.value = hit.step === 0 ? 880 : 440;
|
|
166
|
+
osc.connect(ctx.destination);
|
|
167
|
+
osc.start(time);
|
|
168
|
+
osc.stop(time + hit.duration);
|
|
169
|
+
});
|
|
170
|
+
transport.start();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Changing things while playing
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { createTransport, steps, beats, now, Hat } from '@jpdutoit/squelchy';
|
|
177
|
+
|
|
178
|
+
const ctx = new AudioContext();
|
|
179
|
+
const transport = createTransport(ctx, { bpm: 100 });
|
|
180
|
+
const hat = Hat.create(ctx);
|
|
181
|
+
hat.connect(ctx.destination);
|
|
182
|
+
transport.start();
|
|
183
|
+
|
|
184
|
+
const hats = transport.play(steps('x.x.x.x.'), hat);
|
|
185
|
+
hats.set(steps('xxxxxxxx')); // swaps at the next loop, so it stays in time
|
|
186
|
+
hats.stop('bar'); // stop at the next bar line (no argument = stop now)
|
|
187
|
+
hats.stop({ bar: 8 }); // or at a specific bar
|
|
188
|
+
|
|
189
|
+
transport.bpm = 128; // tempo changes apply from the next scheduled event
|
|
190
|
+
|
|
191
|
+
transport.beatAt(now(ctx)); // where are we, in beats?
|
|
192
|
+
transport.timeAt(beats(16)); // when is bar 5, in seconds?
|
|
193
|
+
transport.stop(); // stop everything
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Automation at musical positions
|
|
197
|
+
|
|
198
|
+
`transport.at(position, fn)` calls `fn(time)` just ahead of that musical position, with its exact audio time, so you can schedule any `AudioParam` in musical time. It returns a cancel function.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { createTransport, preload, Vcf, note, sec } from '@jpdutoit/squelchy';
|
|
202
|
+
|
|
203
|
+
const ctx = new AudioContext();
|
|
204
|
+
await preload(ctx);
|
|
205
|
+
const transport = createTransport(ctx, { bpm: 120 });
|
|
206
|
+
const filter = Vcf.create(ctx, { note: note('C4') });
|
|
207
|
+
filter.out.connect(ctx.destination);
|
|
208
|
+
|
|
209
|
+
const bar = sec((transport.beatsPerBar * 60) / transport.bpm);
|
|
210
|
+
|
|
211
|
+
// Open the filter over two bars, starting at bar 3.
|
|
212
|
+
const cancel = transport.at({ bar: 2 }, (t) => {
|
|
213
|
+
filter.params.note.setValueAtTime(note('C4'), t);
|
|
214
|
+
filter.params.note.linearRampToValueAtTime(note('C7'), t + 2 * bar);
|
|
215
|
+
});
|
|
216
|
+
transport.start();
|
|
217
|
+
// cancel(); // if you change your mind before it fires
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Syncing visuals
|
|
221
|
+
|
|
222
|
+
Hits are scheduled ahead of time, so don't draw from inside a voice. Use `onHit` instead: it fires when the hit is actually **heard**, including output latency.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { createTransport, steps, Kick } from '@jpdutoit/squelchy';
|
|
226
|
+
|
|
227
|
+
const ctx = new AudioContext();
|
|
228
|
+
const transport = createTransport(ctx);
|
|
229
|
+
const kick = Kick.create(ctx);
|
|
230
|
+
kick.connect(ctx.destination);
|
|
231
|
+
const highlight = (step: number) => { /* light up step `step` */ };
|
|
232
|
+
|
|
233
|
+
transport.play(steps('x...x...'), kick, { onHit: (hit) => highlight(hit.step) }); // one playback
|
|
234
|
+
const unsubscribe = transport.onHit((hit) => console.log(hit.beat, hit.value)); // every playback
|
|
235
|
+
transport.start();
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Sequencing a synth voice
|
|
239
|
+
|
|
240
|
+
`NoteCv` turns pattern hits into two control signals: `gate` (high for each note's length) and `pitch` (the note number). Patch them like hardware: pitch into a VCO's `voct`, gate into an envelope's `trigger`. With `voct` patched, the VCO's `offset` becomes a transpose, so set it to 0 to play the notes as written.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { preload, createTransport, notes, semitones, sec, NoteCv, Vco, Envelope, Vcf, note } from '@jpdutoit/squelchy';
|
|
244
|
+
|
|
245
|
+
const ctx = new AudioContext();
|
|
246
|
+
await preload(ctx);
|
|
247
|
+
const transport = createTransport(ctx, { bpm: 124 });
|
|
248
|
+
|
|
249
|
+
const cv = NoteCv.create(ctx);
|
|
250
|
+
const vco = Vco.create(ctx, { offset: semitones(0), skew: 1 });
|
|
251
|
+
const env = Envelope.create(ctx, { attack: sec(0.005), decay: sec(0.25), sustain: 0.2, release: sec(0.1), depth: semitones(36) });
|
|
252
|
+
const filter = Vcf.create(ctx, { note: note('C4'), resonance: 6 });
|
|
253
|
+
|
|
254
|
+
cv.pitch.connect(vco.voct);
|
|
255
|
+
cv.gate.connect(env.trigger);
|
|
256
|
+
vco.out.connect(filter.in);
|
|
257
|
+
env.env.connect(filter.modFreq); // the envelope also sweeps the cutoff, by `depth` semitones
|
|
258
|
+
filter.out.connect(env.in); // Envelope has a built-in VCA: in → out
|
|
259
|
+
env.out.connect(ctx.destination);
|
|
260
|
+
|
|
261
|
+
transport.play(notes('C2 C2 . C3 . C2 Eb2 . G2 . C2 . Bb1 . C2 .'), cv, { gate: 0.6 });
|
|
262
|
+
transport.start();
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## One-shot actions with `pulse()`
|
|
266
|
+
|
|
267
|
+
Actions like a tape stop or kicking the spring tank are **gate inputs**. They react to a rising edge, like a trigger cable on hardware. `pulse(input, time)` fires one at an exact time. It works the same whether it's called from a button, a pattern or `transport.at`, and it sums with anything else patched into that input.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { preload, createTransport, steps, pulse, sec, TapeStop, SpringReverb, Kick } from '@jpdutoit/squelchy';
|
|
271
|
+
|
|
272
|
+
const ctx = new AudioContext();
|
|
273
|
+
await preload(ctx);
|
|
274
|
+
const transport = createTransport(ctx);
|
|
275
|
+
|
|
276
|
+
const kick = Kick.create(ctx);
|
|
277
|
+
const spring = SpringReverb.create(ctx, { mix: 0.4 });
|
|
278
|
+
const tape = TapeStop.create(ctx, { time: sec(1.5) });
|
|
279
|
+
kick.connect(spring.in);
|
|
280
|
+
spring.out.connect(tape.in);
|
|
281
|
+
tape.out.connect(ctx.destination);
|
|
282
|
+
|
|
283
|
+
transport.play(steps('x...x...x...x...'), kick);
|
|
284
|
+
transport.play(steps('........x.......'), (t) => pulse(spring.knock, t)); // kick the tank every other bar
|
|
285
|
+
transport.at({ bar: 4 }, (t) => pulse(tape.trigger, t)); // tape stop at bar 5…
|
|
286
|
+
transport.at({ bar: 5 }, (t) => pulse(tape.trigger, t)); // …and back up a bar later
|
|
287
|
+
|
|
288
|
+
// From a button, right now:
|
|
289
|
+
const onClick = () => pulse(tape.trigger);
|
|
290
|
+
transport.start();
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Sample kits
|
|
294
|
+
|
|
295
|
+
Every kit is CC0 or public domain, and each one carries its own `license`. `SampleSets` is only metadata; audio is fetched when you call `loadSampleSet`. Voices are `bd sd rim clap ch oh lt mt ht cym perc` (`VOICE_KEYS`). If a kit lacks a voice, you get the closest one it has, e.g. `oh` falls back to `ch`, then `cym`.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { createTransport, steps, loadSampleSet, SampleSets, SampleVoice, VOICE_KEYS } from '@jpdutoit/squelchy';
|
|
299
|
+
|
|
300
|
+
const ctx = new AudioContext();
|
|
301
|
+
for (const [id, set] of Object.entries(SampleSets)) console.log(id, set.name, set.license);
|
|
302
|
+
|
|
303
|
+
const kit = await loadSampleSet(ctx, SampleSets['tr-909']);
|
|
304
|
+
console.log(VOICE_KEYS.filter((k) => !kit.buffers[k])); // voices this kit covers by fallback
|
|
305
|
+
|
|
306
|
+
const bd = SampleVoice.create(ctx, kit, 'bd');
|
|
307
|
+
const tom = SampleVoice.create(ctx, kit, 'lt');
|
|
308
|
+
bd.connect(ctx.destination);
|
|
309
|
+
tom.connect(ctx.destination);
|
|
310
|
+
|
|
311
|
+
const transport = createTransport(ctx);
|
|
312
|
+
transport.play(steps('x...x...x...x...'), bd);
|
|
313
|
+
transport.play([true, null, 3, null, 7, 12, null, null], tom); // numbers repitch in semitones
|
|
314
|
+
transport.start();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
A step value of `0` is a rest, so use `true` for an unshifted hit.
|
|
318
|
+
|
|
319
|
+
**Self-hosting the audio.** Kits load from https://squelchy.studio by default. To serve them yourself, mirror the files at the same root-relative paths (`SampleSets[id].voices`) and pass your origin:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import { loadSampleSet, SampleSets } from '@jpdutoit/squelchy';
|
|
323
|
+
|
|
324
|
+
const ctx = new AudioContext();
|
|
325
|
+
const kit = await loadSampleSet(ctx, SampleSets['roland-808'], { baseUrl: 'https://cdn.example.com' });
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Offline rendering and custom clocks
|
|
329
|
+
|
|
330
|
+
On an `OfflineAudioContext`, the transport schedules the whole render up front, so the result is exact and needs no timers. Handy for bouncing loops and for tests.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { preload, createTransport, steps, sec, Kick, Reverb } from '@jpdutoit/squelchy';
|
|
334
|
+
|
|
335
|
+
const offline = new OfflineAudioContext(2, 48000 * 8, 48000); // 8 seconds
|
|
336
|
+
await preload(offline);
|
|
337
|
+
const transport = createTransport(offline, { bpm: 120 });
|
|
338
|
+
const kick = Kick.create(offline);
|
|
339
|
+
const reverb = Reverb.create(offline, { decay: sec(3) });
|
|
340
|
+
kick.connect(reverb.in);
|
|
341
|
+
reverb.out.connect(offline.destination);
|
|
342
|
+
|
|
343
|
+
transport.play(steps('x...x...x...x...'), kick);
|
|
344
|
+
transport.start(sec(0));
|
|
345
|
+
const rendered: AudioBuffer = await offline.startRendering();
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
A realtime transport runs its own 25 ms timer. Pass `clock: 'manual'` to drive it yourself, for example from a worker timer or a test loop. Call `tick()` more often than the `lookahead` (default 0.1 s):
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { createTransport } from '@jpdutoit/squelchy';
|
|
352
|
+
|
|
353
|
+
const ctx = new AudioContext();
|
|
354
|
+
const transport = createTransport(ctx, { clock: 'manual' });
|
|
355
|
+
transport.start();
|
|
356
|
+
setInterval(() => transport.tick(), 20);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Module reference with jq
|
|
360
|
+
|
|
361
|
+
`docs/modules.json` is the whole module reference as one object, keyed by export name. It contains what you need to use a module and nothing internal:
|
|
362
|
+
|
|
363
|
+
```json
|
|
364
|
+
{
|
|
365
|
+
"Delay": {
|
|
366
|
+
"name": "Delay",
|
|
367
|
+
"panel": "DELAY",
|
|
368
|
+
"import": "import { Delay } from '@jpdutoit/squelchy';",
|
|
369
|
+
"summary": "Echo Effect",
|
|
370
|
+
"description": "…",
|
|
371
|
+
"categories": ["Time"],
|
|
372
|
+
"inputs": { "in": { "signal": "audio", "name": "Audio Input", "description": "…", "target": "AudioNode" } },
|
|
373
|
+
"outputs": { "out": { "signal": "audio", "name": "Audio Output", "description": "…", "type": "AudioNode" } },
|
|
374
|
+
"params": { "time": { "name": "Delay Time", "description": "…", "type": "Seconds", "unit": "s",
|
|
375
|
+
"default": 0.3, "min": 0.01, "max": 1, "scale": "log", "automatable": true } }
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
- `panel` is the module's name on squelchy.studio.
|
|
381
|
+
- `signal` is one of `audio`, `modulation`, `gate` or `pitch`.
|
|
382
|
+
- An input's `target` is `AudioParam` when the input sums straight onto a parameter.
|
|
383
|
+
- `automatable: true` means `params.<key>` exists.
|
|
384
|
+
|
|
385
|
+
From a project with the package installed:
|
|
386
|
+
|
|
387
|
+
```sh
|
|
388
|
+
DOCS=node_modules/@jpdutoit/squelchy/docs/modules.json
|
|
389
|
+
|
|
390
|
+
jq 'keys' "$DOCS" # every module
|
|
391
|
+
jq '.Delay' "$DOCS" # one module's full docs
|
|
392
|
+
jq '.Vcf.params.note' "$DOCS" # one parameter
|
|
393
|
+
jq '.Delay.params | map_values(.default)' "$DOCS" # defaults, shaped like create() params
|
|
394
|
+
jq '.Vco.inputs | keys' "$DOCS" # a module's inputs
|
|
395
|
+
jq -r 'to_entries[] | select(.value.categories | index("Time")) | .key' "$DOCS" # modules in a category
|
|
396
|
+
jq -r 'to_entries[] | select(any(.value.inputs[]; .signal == "gate")) | .key' "$DOCS" # modules with a gate input
|
|
397
|
+
jq -r 'to_entries[] | .key as $m | .value.params | to_entries[] | select(.value.automatable | not) | "\($m).\(.key)"' "$DOCS" # set()-only params
|
|
398
|
+
```
|