@jpdutoit/squelchy 0.0.1 → 0.0.3

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.
Files changed (154) hide show
  1. package/README.md +9 -7
  2. package/{chunk-NDUSYSHF.js → chunk-MEZE4P3U.js} +5 -5
  3. package/chunk-MEZE4P3U.js.map +7 -0
  4. package/docs/GUIDE.md +420 -0
  5. package/docs/MODULES.md +62 -62
  6. package/docs/modules.json +3332 -0
  7. package/index.d.ts +24 -251
  8. package/index.js +68 -260
  9. package/index.js.map +4 -4
  10. package/kits/compu-78/bd.flac +0 -0
  11. package/kits/compu-78/ch.flac +0 -0
  12. package/kits/compu-78/clap.flac +0 -0
  13. package/kits/compu-78/cym.flac +0 -0
  14. package/kits/compu-78/ht.flac +0 -0
  15. package/kits/compu-78/lt.flac +0 -0
  16. package/kits/compu-78/mt.flac +0 -0
  17. package/kits/compu-78/oh.flac +0 -0
  18. package/kits/compu-78/perc.flac +0 -0
  19. package/kits/compu-78/rim.flac +0 -0
  20. package/kits/compu-78/sd.flac +0 -0
  21. package/kits/compu-78.d.ts +7 -0
  22. package/kits/compu-78.js +22 -0
  23. package/kits/deep-analogue/bd.flac +0 -0
  24. package/kits/deep-analogue/ch.flac +0 -0
  25. package/kits/deep-analogue/clap.flac +0 -0
  26. package/kits/deep-analogue/cym.flac +0 -0
  27. package/kits/deep-analogue/ht.flac +0 -0
  28. package/kits/deep-analogue/lt.flac +0 -0
  29. package/kits/deep-analogue/mt.flac +0 -0
  30. package/kits/deep-analogue/oh.flac +0 -0
  31. package/kits/deep-analogue/perc.flac +0 -0
  32. package/kits/deep-analogue/rim.flac +0 -0
  33. package/kits/deep-analogue/sd.flac +0 -0
  34. package/kits/deep-analogue.d.ts +7 -0
  35. package/kits/deep-analogue.js +22 -0
  36. package/kits/dr5-studio/bd.flac +0 -0
  37. package/kits/dr5-studio/ch.flac +0 -0
  38. package/kits/dr5-studio/clap.flac +0 -0
  39. package/kits/dr5-studio/cym.flac +0 -0
  40. package/kits/dr5-studio/ht.flac +0 -0
  41. package/kits/dr5-studio/lt.flac +0 -0
  42. package/kits/dr5-studio/mt.flac +0 -0
  43. package/kits/dr5-studio/oh.flac +0 -0
  44. package/kits/dr5-studio/perc.flac +0 -0
  45. package/kits/dr5-studio/rim.flac +0 -0
  46. package/kits/dr5-studio/sd.flac +0 -0
  47. package/kits/dr5-studio.d.ts +7 -0
  48. package/kits/dr5-studio.js +22 -0
  49. package/kits/hard-cell/bd.flac +0 -0
  50. package/kits/hard-cell/ch.flac +0 -0
  51. package/kits/hard-cell/clap.flac +0 -0
  52. package/kits/hard-cell/cym.flac +0 -0
  53. package/kits/hard-cell/ht.flac +0 -0
  54. package/kits/hard-cell/lt.flac +0 -0
  55. package/kits/hard-cell/mt.flac +0 -0
  56. package/kits/hard-cell/oh.flac +0 -0
  57. package/kits/hard-cell/perc.flac +0 -0
  58. package/kits/hard-cell/rim.flac +0 -0
  59. package/kits/hard-cell/sd.flac +0 -0
  60. package/kits/hard-cell.d.ts +7 -0
  61. package/kits/hard-cell.js +22 -0
  62. package/kits/micro-click/bd.flac +0 -0
  63. package/kits/micro-click/ch.flac +0 -0
  64. package/kits/micro-click/clap.flac +0 -0
  65. package/kits/micro-click/cym.flac +0 -0
  66. package/kits/micro-click/ht.flac +0 -0
  67. package/kits/micro-click/lt.flac +0 -0
  68. package/kits/micro-click/mt.flac +0 -0
  69. package/kits/micro-click/oh.flac +0 -0
  70. package/kits/micro-click/perc.flac +0 -0
  71. package/kits/micro-click/rim.flac +0 -0
  72. package/kits/micro-click/sd.flac +0 -0
  73. package/kits/micro-click.d.ts +7 -0
  74. package/kits/micro-click.js +22 -0
  75. package/kits/roland-808/bd.flac +0 -0
  76. package/kits/roland-808/ch.flac +0 -0
  77. package/kits/roland-808/clap.flac +0 -0
  78. package/kits/roland-808/cym.flac +0 -0
  79. package/kits/roland-808/ht.flac +0 -0
  80. package/kits/roland-808/lt.flac +0 -0
  81. package/kits/roland-808/mt.flac +0 -0
  82. package/kits/roland-808/oh.flac +0 -0
  83. package/kits/roland-808/perc.flac +0 -0
  84. package/kits/roland-808/rim.flac +0 -0
  85. package/kits/roland-808/sd.flac +0 -0
  86. package/kits/roland-808.d.ts +7 -0
  87. package/kits/roland-808.js +22 -0
  88. package/kits/rx5-fm/bd.flac +0 -0
  89. package/kits/rx5-fm/ch.flac +0 -0
  90. package/kits/rx5-fm/clap.flac +0 -0
  91. package/kits/rx5-fm/cym.flac +0 -0
  92. package/kits/rx5-fm/ht.flac +0 -0
  93. package/kits/rx5-fm/lt.flac +0 -0
  94. package/kits/rx5-fm/mt.flac +0 -0
  95. package/kits/rx5-fm/oh.flac +0 -0
  96. package/kits/rx5-fm/perc.flac +0 -0
  97. package/kits/rx5-fm/rim.flac +0 -0
  98. package/kits/rx5-fm/sd.flac +0 -0
  99. package/kits/rx5-fm.d.ts +7 -0
  100. package/kits/rx5-fm.js +22 -0
  101. package/kits/tape-dust/bd.flac +0 -0
  102. package/kits/tape-dust/ch.flac +0 -0
  103. package/kits/tape-dust/clap.flac +0 -0
  104. package/kits/tape-dust/cym.flac +0 -0
  105. package/kits/tape-dust/ht.flac +0 -0
  106. package/kits/tape-dust/lt.flac +0 -0
  107. package/kits/tape-dust/mt.flac +0 -0
  108. package/kits/tape-dust/oh.flac +0 -0
  109. package/kits/tape-dust/perc.flac +0 -0
  110. package/kits/tape-dust/rim.flac +0 -0
  111. package/kits/tape-dust/sd.flac +0 -0
  112. package/kits/tape-dust.d.ts +7 -0
  113. package/kits/tape-dust.js +22 -0
  114. package/kits/tr-606/bd.flac +0 -0
  115. package/kits/tr-606/ch.flac +0 -0
  116. package/kits/tr-606/cym.flac +0 -0
  117. package/kits/tr-606/ht.flac +0 -0
  118. package/kits/tr-606/lt.flac +0 -0
  119. package/kits/tr-606/mt.flac +0 -0
  120. package/kits/tr-606/oh.flac +0 -0
  121. package/kits/tr-606/sd.flac +0 -0
  122. package/kits/tr-606.d.ts +7 -0
  123. package/kits/tr-606.js +19 -0
  124. package/kits/tr-707/bd.flac +0 -0
  125. package/kits/tr-707/ch.flac +0 -0
  126. package/kits/tr-707/clap.flac +0 -0
  127. package/kits/tr-707/cym.flac +0 -0
  128. package/kits/tr-707/ht.flac +0 -0
  129. package/kits/tr-707/lt.flac +0 -0
  130. package/kits/tr-707/mt.flac +0 -0
  131. package/kits/tr-707/oh.flac +0 -0
  132. package/kits/tr-707/perc.flac +0 -0
  133. package/kits/tr-707/rim.flac +0 -0
  134. package/kits/tr-707/sd.flac +0 -0
  135. package/kits/tr-707.d.ts +7 -0
  136. package/kits/tr-707.js +22 -0
  137. package/kits/tr-909/bd.flac +0 -0
  138. package/kits/tr-909/ch.flac +0 -0
  139. package/kits/tr-909/clap.flac +0 -0
  140. package/kits/tr-909/cym.flac +0 -0
  141. package/kits/tr-909/ht.flac +0 -0
  142. package/kits/tr-909/lt.flac +0 -0
  143. package/kits/tr-909/mt.flac +0 -0
  144. package/kits/tr-909/oh.flac +0 -0
  145. package/kits/tr-909/perc.flac +0 -0
  146. package/kits/tr-909/rim.flac +0 -0
  147. package/kits/tr-909/sd.flac +0 -0
  148. package/kits/tr-909.d.ts +7 -0
  149. package/kits/tr-909.js +22 -0
  150. package/meta.json +16 -16
  151. package/package.json +7 -1
  152. package/{worklets-inline-MUVFLGPY.js → worklets-inline-DFWINMCS.js} +2 -2
  153. package/chunk-NDUSYSHF.js.map +0 -7
  154. /package/{worklets-inline-MUVFLGPY.js.map → worklets-inline-DFWINMCS.js.map} +0 -0
package/docs/GUIDE.md ADDED
@@ -0,0 +1,420 @@
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`. Each kit is its own import, and its audio ships inside the package. Import only the kits you use: your bundler copies and hashes just their sound files, and nothing is fetched from a third-party server. 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
+ | Import | Kit | Family | Voices |
298
+ |---|---|---|---|
299
+ | `import { Roland808 } from '@jpdutoit/squelchy/kits/roland-808'` | Roland TR-808 | Machines | bd sd rim clap ch oh lt mt ht cym perc |
300
+ | `import { Tr909 } from '@jpdutoit/squelchy/kits/tr-909'` | Roland TR-909 | Machines | bd sd rim clap ch oh lt mt ht cym perc |
301
+ | `import { Tr707 } from '@jpdutoit/squelchy/kits/tr-707'` | Roland TR-707 | Machines | bd sd rim clap ch oh lt mt ht cym perc |
302
+ | `import { Tr606 } from '@jpdutoit/squelchy/kits/tr-606'` | Roland TR-606 | Machines | bd sd ch oh lt mt ht cym |
303
+ | `import { Compu78 } from '@jpdutoit/squelchy/kits/compu-78'` | Compu-78 | Machines | bd sd rim clap ch oh lt mt ht cym perc |
304
+ | `import { Dr5Studio } from '@jpdutoit/squelchy/kits/dr5-studio'` | Studio Kit | Acoustic | bd sd rim clap ch oh lt mt ht cym perc |
305
+ | `import { TapeDust } from '@jpdutoit/squelchy/kits/tape-dust'` | Tape Dust | Character | bd sd rim clap ch oh lt mt ht cym perc |
306
+ | `import { HardCell } from '@jpdutoit/squelchy/kits/hard-cell'` | Hard Cell | Character | bd sd rim clap ch oh lt mt ht cym perc |
307
+ | `import { Rx5Fm } from '@jpdutoit/squelchy/kits/rx5-fm'` | Yamaha RX5 | Digital | bd sd rim clap ch oh lt mt ht cym perc |
308
+ | `import { MicroClick } from '@jpdutoit/squelchy/kits/micro-click'` | Micro Click | Digital | bd sd rim clap ch oh lt mt ht cym perc |
309
+ | `import { DeepAnalogue } from '@jpdutoit/squelchy/kits/deep-analogue'` | Deep Analogue | Acoustic | bd sd rim clap ch oh lt mt ht cym perc |
310
+
311
+ ```ts
312
+ import { createTransport, steps, loadSampleSet, SampleVoice, VOICE_KEYS } from '@jpdutoit/squelchy';
313
+ import { Tr909 } from '@jpdutoit/squelchy/kits/tr-909';
314
+
315
+ const ctx = new AudioContext();
316
+ console.log(Tr909.name, Tr909.license);
317
+
318
+ const kit = await loadSampleSet(ctx, Tr909);
319
+ console.log(VOICE_KEYS.filter((k) => !kit.buffers[k])); // voices this kit covers by fallback
320
+
321
+ const bd = SampleVoice.create(ctx, kit, 'bd');
322
+ const tom = SampleVoice.create(ctx, kit, 'lt');
323
+ bd.connect(ctx.destination);
324
+ tom.connect(ctx.destination);
325
+
326
+ const transport = createTransport(ctx);
327
+ transport.play(steps('x...x...x...x...'), bd);
328
+ transport.play([true, null, 3, null, 7, 12, null, null], tom); // numbers repitch in semitones
329
+ transport.start();
330
+ ```
331
+
332
+ A step value of `0` is a rest, so use `true` for an unshifted hit.
333
+
334
+ **How the audio gets into your build.** A kit's voice URLs are written as `new URL('./tr-909/bd.flac', import.meta.url)`. Vite (8.3 or newer, in dev and build), webpack 5 and Parcel all recognise that pattern: they copy each file of the kits you import into your output with a content hash and rewrite the URL. Loading the package unbundled, e.g. from a CDN like jsDelivr, resolves the files next to the kit module.
335
+
336
+ **Your own kit.** `loadSampleSet` takes any `SampleSetInfo`, so you can point voices at your own files:
337
+
338
+ ```ts
339
+ import { loadSampleSet, type SampleSetInfo } from '@jpdutoit/squelchy';
340
+
341
+ const ctx = new AudioContext();
342
+ const mine: SampleSetInfo = {
343
+ name: 'My kit', blurb: 'Field recordings', family: 'Character',
344
+ credit: 'Me', license: 'CC0', licenseUrl: 'https://creativecommons.org/publicdomain/zero/1.0/',
345
+ voices: { bd: '/audio/kick.wav', sd: new URL('./snare.wav', import.meta.url).href },
346
+ };
347
+ const kit = await loadSampleSet(ctx, mine);
348
+ ```
349
+
350
+ ## Offline rendering and custom clocks
351
+
352
+ 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.
353
+
354
+ ```ts
355
+ import { preload, createTransport, steps, sec, Kick, Reverb } from '@jpdutoit/squelchy';
356
+
357
+ const offline = new OfflineAudioContext(2, 48000 * 8, 48000); // 8 seconds
358
+ await preload(offline);
359
+ const transport = createTransport(offline, { bpm: 120 });
360
+ const kick = Kick.create(offline);
361
+ const reverb = Reverb.create(offline, { decay: sec(3) });
362
+ kick.connect(reverb.in);
363
+ reverb.out.connect(offline.destination);
364
+
365
+ transport.play(steps('x...x...x...x...'), kick);
366
+ transport.start(sec(0));
367
+ const rendered: AudioBuffer = await offline.startRendering();
368
+ ```
369
+
370
+ 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):
371
+
372
+ ```ts
373
+ import { createTransport } from '@jpdutoit/squelchy';
374
+
375
+ const ctx = new AudioContext();
376
+ const transport = createTransport(ctx, { clock: 'manual' });
377
+ transport.start();
378
+ setInterval(() => transport.tick(), 20);
379
+ ```
380
+
381
+ ## Module reference with jq
382
+
383
+ `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:
384
+
385
+ ```json
386
+ {
387
+ "Delay": {
388
+ "name": "Delay",
389
+ "panel": "DELAY",
390
+ "import": "import { Delay } from '@jpdutoit/squelchy';",
391
+ "summary": "Echo Effect",
392
+ "description": "…",
393
+ "categories": ["Time"],
394
+ "inputs": { "in": { "signal": "audio", "name": "Audio Input", "description": "…", "target": "AudioNode" } },
395
+ "outputs": { "out": { "signal": "audio", "name": "Audio Output", "description": "…", "type": "AudioNode" } },
396
+ "params": { "time": { "name": "Delay Time", "description": "…", "type": "Seconds", "unit": "s",
397
+ "default": 0.3, "min": 0.01, "max": 1, "scale": "log", "automatable": true } }
398
+ }
399
+ }
400
+ ```
401
+
402
+ - `panel` is the module's name on squelchy.studio.
403
+ - `signal` is one of `audio`, `modulation`, `gate` or `pitch`.
404
+ - An input's `target` is `AudioParam` when the input sums straight onto a parameter.
405
+ - `automatable: true` means `params.<key>` exists.
406
+
407
+ From a project with the package installed:
408
+
409
+ ```sh
410
+ DOCS=node_modules/@jpdutoit/squelchy/docs/modules.json
411
+
412
+ jq 'keys' "$DOCS" # every module
413
+ jq '.Delay' "$DOCS" # one module's full docs
414
+ jq '.Vcf.params.note' "$DOCS" # one parameter
415
+ jq '.Delay.params | map_values(.default)' "$DOCS" # defaults, shaped like create() params
416
+ jq '.Vco.inputs | keys' "$DOCS" # a module's inputs
417
+ jq -r 'to_entries[] | select(.value.categories | index("Time")) | .key' "$DOCS" # modules in a category
418
+ jq -r 'to_entries[] | select(any(.value.inputs[]; .signal == "gate")) | .key' "$DOCS" # modules with a gate input
419
+ jq -r 'to_entries[] | .key as $m | .value.params | to_entries[] | select(.value.automatable | not) | "\($m).\(.key)"' "$DOCS" # set()-only params
420
+ ```