@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.
- package/README.md +9 -7
- package/{chunk-NDUSYSHF.js → chunk-MEZE4P3U.js} +5 -5
- package/chunk-MEZE4P3U.js.map +7 -0
- package/docs/GUIDE.md +420 -0
- package/docs/MODULES.md +62 -62
- package/docs/modules.json +3332 -0
- package/index.d.ts +24 -251
- package/index.js +68 -260
- package/index.js.map +4 -4
- package/kits/compu-78/bd.flac +0 -0
- package/kits/compu-78/ch.flac +0 -0
- package/kits/compu-78/clap.flac +0 -0
- package/kits/compu-78/cym.flac +0 -0
- package/kits/compu-78/ht.flac +0 -0
- package/kits/compu-78/lt.flac +0 -0
- package/kits/compu-78/mt.flac +0 -0
- package/kits/compu-78/oh.flac +0 -0
- package/kits/compu-78/perc.flac +0 -0
- package/kits/compu-78/rim.flac +0 -0
- package/kits/compu-78/sd.flac +0 -0
- package/kits/compu-78.d.ts +7 -0
- package/kits/compu-78.js +22 -0
- package/kits/deep-analogue/bd.flac +0 -0
- package/kits/deep-analogue/ch.flac +0 -0
- package/kits/deep-analogue/clap.flac +0 -0
- package/kits/deep-analogue/cym.flac +0 -0
- package/kits/deep-analogue/ht.flac +0 -0
- package/kits/deep-analogue/lt.flac +0 -0
- package/kits/deep-analogue/mt.flac +0 -0
- package/kits/deep-analogue/oh.flac +0 -0
- package/kits/deep-analogue/perc.flac +0 -0
- package/kits/deep-analogue/rim.flac +0 -0
- package/kits/deep-analogue/sd.flac +0 -0
- package/kits/deep-analogue.d.ts +7 -0
- package/kits/deep-analogue.js +22 -0
- package/kits/dr5-studio/bd.flac +0 -0
- package/kits/dr5-studio/ch.flac +0 -0
- package/kits/dr5-studio/clap.flac +0 -0
- package/kits/dr5-studio/cym.flac +0 -0
- package/kits/dr5-studio/ht.flac +0 -0
- package/kits/dr5-studio/lt.flac +0 -0
- package/kits/dr5-studio/mt.flac +0 -0
- package/kits/dr5-studio/oh.flac +0 -0
- package/kits/dr5-studio/perc.flac +0 -0
- package/kits/dr5-studio/rim.flac +0 -0
- package/kits/dr5-studio/sd.flac +0 -0
- package/kits/dr5-studio.d.ts +7 -0
- package/kits/dr5-studio.js +22 -0
- package/kits/hard-cell/bd.flac +0 -0
- package/kits/hard-cell/ch.flac +0 -0
- package/kits/hard-cell/clap.flac +0 -0
- package/kits/hard-cell/cym.flac +0 -0
- package/kits/hard-cell/ht.flac +0 -0
- package/kits/hard-cell/lt.flac +0 -0
- package/kits/hard-cell/mt.flac +0 -0
- package/kits/hard-cell/oh.flac +0 -0
- package/kits/hard-cell/perc.flac +0 -0
- package/kits/hard-cell/rim.flac +0 -0
- package/kits/hard-cell/sd.flac +0 -0
- package/kits/hard-cell.d.ts +7 -0
- package/kits/hard-cell.js +22 -0
- package/kits/micro-click/bd.flac +0 -0
- package/kits/micro-click/ch.flac +0 -0
- package/kits/micro-click/clap.flac +0 -0
- package/kits/micro-click/cym.flac +0 -0
- package/kits/micro-click/ht.flac +0 -0
- package/kits/micro-click/lt.flac +0 -0
- package/kits/micro-click/mt.flac +0 -0
- package/kits/micro-click/oh.flac +0 -0
- package/kits/micro-click/perc.flac +0 -0
- package/kits/micro-click/rim.flac +0 -0
- package/kits/micro-click/sd.flac +0 -0
- package/kits/micro-click.d.ts +7 -0
- package/kits/micro-click.js +22 -0
- package/kits/roland-808/bd.flac +0 -0
- package/kits/roland-808/ch.flac +0 -0
- package/kits/roland-808/clap.flac +0 -0
- package/kits/roland-808/cym.flac +0 -0
- package/kits/roland-808/ht.flac +0 -0
- package/kits/roland-808/lt.flac +0 -0
- package/kits/roland-808/mt.flac +0 -0
- package/kits/roland-808/oh.flac +0 -0
- package/kits/roland-808/perc.flac +0 -0
- package/kits/roland-808/rim.flac +0 -0
- package/kits/roland-808/sd.flac +0 -0
- package/kits/roland-808.d.ts +7 -0
- package/kits/roland-808.js +22 -0
- package/kits/rx5-fm/bd.flac +0 -0
- package/kits/rx5-fm/ch.flac +0 -0
- package/kits/rx5-fm/clap.flac +0 -0
- package/kits/rx5-fm/cym.flac +0 -0
- package/kits/rx5-fm/ht.flac +0 -0
- package/kits/rx5-fm/lt.flac +0 -0
- package/kits/rx5-fm/mt.flac +0 -0
- package/kits/rx5-fm/oh.flac +0 -0
- package/kits/rx5-fm/perc.flac +0 -0
- package/kits/rx5-fm/rim.flac +0 -0
- package/kits/rx5-fm/sd.flac +0 -0
- package/kits/rx5-fm.d.ts +7 -0
- package/kits/rx5-fm.js +22 -0
- package/kits/tape-dust/bd.flac +0 -0
- package/kits/tape-dust/ch.flac +0 -0
- package/kits/tape-dust/clap.flac +0 -0
- package/kits/tape-dust/cym.flac +0 -0
- package/kits/tape-dust/ht.flac +0 -0
- package/kits/tape-dust/lt.flac +0 -0
- package/kits/tape-dust/mt.flac +0 -0
- package/kits/tape-dust/oh.flac +0 -0
- package/kits/tape-dust/perc.flac +0 -0
- package/kits/tape-dust/rim.flac +0 -0
- package/kits/tape-dust/sd.flac +0 -0
- package/kits/tape-dust.d.ts +7 -0
- package/kits/tape-dust.js +22 -0
- package/kits/tr-606/bd.flac +0 -0
- package/kits/tr-606/ch.flac +0 -0
- package/kits/tr-606/cym.flac +0 -0
- package/kits/tr-606/ht.flac +0 -0
- package/kits/tr-606/lt.flac +0 -0
- package/kits/tr-606/mt.flac +0 -0
- package/kits/tr-606/oh.flac +0 -0
- package/kits/tr-606/sd.flac +0 -0
- package/kits/tr-606.d.ts +7 -0
- package/kits/tr-606.js +19 -0
- package/kits/tr-707/bd.flac +0 -0
- package/kits/tr-707/ch.flac +0 -0
- package/kits/tr-707/clap.flac +0 -0
- package/kits/tr-707/cym.flac +0 -0
- package/kits/tr-707/ht.flac +0 -0
- package/kits/tr-707/lt.flac +0 -0
- package/kits/tr-707/mt.flac +0 -0
- package/kits/tr-707/oh.flac +0 -0
- package/kits/tr-707/perc.flac +0 -0
- package/kits/tr-707/rim.flac +0 -0
- package/kits/tr-707/sd.flac +0 -0
- package/kits/tr-707.d.ts +7 -0
- package/kits/tr-707.js +22 -0
- package/kits/tr-909/bd.flac +0 -0
- package/kits/tr-909/ch.flac +0 -0
- package/kits/tr-909/clap.flac +0 -0
- package/kits/tr-909/cym.flac +0 -0
- package/kits/tr-909/ht.flac +0 -0
- package/kits/tr-909/lt.flac +0 -0
- package/kits/tr-909/mt.flac +0 -0
- package/kits/tr-909/oh.flac +0 -0
- package/kits/tr-909/perc.flac +0 -0
- package/kits/tr-909/rim.flac +0 -0
- package/kits/tr-909/sd.flac +0 -0
- package/kits/tr-909.d.ts +7 -0
- package/kits/tr-909.js +22 -0
- package/meta.json +16 -16
- package/package.json +7 -1
- package/{worklets-inline-MUVFLGPY.js → worklets-inline-DFWINMCS.js} +2 -2
- package/chunk-NDUSYSHF.js.map +0 -7
- /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
|
+
```
|