@hraness/dawg 0.2.0 → 0.4.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/CHANGELOG.md +201 -0
- package/DAWG.md +762 -11
- package/README.md +79 -30
- package/core/chords.ts +1724 -0
- package/core/diff.ts +261 -0
- package/core/euclid.ts +670 -0
- package/core/fx.ts +1065 -0
- package/core/key.ts +43 -0
- package/core/kits.ts +320 -0
- package/core/params.ts +111 -0
- package/core/pitch.ts +60 -0
- package/core/rhythm.ts +287 -0
- package/core/score.ts +1054 -14
- package/core/sdk/eval-child.ts +125 -0
- package/core/sdk/eval.ts +260 -0
- package/core/sdk/print.ts +675 -0
- package/core/sdk/sync-chords.ts +54 -0
- package/core/sdk/v1.ts +4159 -0
- package/core/slug.ts +19 -0
- package/core/synth.ts +1001 -0
- package/package.json +8 -5
- package/src/agent/agent.ts +317 -16
- package/src/agent/brief.ts +50 -5
- package/src/agent/chord-tools.ts +357 -0
- package/src/agent/drum-tools.ts +135 -0
- package/src/agent/gateway.ts +215 -41
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +3 -18
- package/src/agent/pack-tools.ts +369 -0
- package/src/agent/planner.ts +55 -2
- package/src/agent/preview-tool.ts +270 -0
- package/src/agent/provider.ts +234 -106
- package/src/agent/rhythm-tools.ts +145 -0
- package/src/agent/sse.ts +31 -7
- package/src/agent/tools.ts +705 -14
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +35 -14
- package/src/agent/xcb.ts +233 -21
- package/src/audio/audition.ts +93 -0
- package/src/audio/cache.ts +160 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/effects/bus.ts +147 -0
- package/src/audio/effects/chain.ts +99 -0
- package/src/audio/effects/common.ts +251 -0
- package/src/audio/effects/convolution.ts +301 -0
- package/src/audio/effects/drive.ts +142 -0
- package/src/audio/effects/duck.ts +122 -0
- package/src/audio/effects/dynamics.ts +121 -0
- package/src/audio/effects/filter.ts +319 -0
- package/src/audio/effects/modulation.ts +174 -0
- package/src/audio/effects/space.ts +337 -0
- package/src/audio/engine.ts +430 -33
- package/src/audio/kits.ts +200 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/player.ts +19 -4
- package/src/audio/preview.ts +470 -0
- package/src/audio/random.ts +15 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +420 -0
- package/src/audio/samples.ts +1025 -0
- package/src/audio/synth/oscillators.ts +268 -0
- package/src/audio/synth/voice.ts +555 -0
- package/src/audio/synth/zzfx.ts +137 -0
- package/src/audio/wav.ts +550 -334
- package/src/audio/wavetable-maker.ts +717 -0
- package/src/audio/wavetable.ts +624 -0
- package/src/auth/cli.ts +146 -27
- package/src/auth/credentials.ts +167 -41
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +885 -128
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +25 -2
- package/src/auth/tui.ts +60 -43
- package/src/commands/drums.ts +299 -0
- package/src/commands/edit.ts +192 -0
- package/src/commands/fx.ts +360 -0
- package/src/commands/help.ts +535 -0
- package/src/commands/history.ts +32 -19
- package/src/commands/music.ts +34 -15
- package/src/commands/pack.ts +423 -0
- package/src/commands/rhythm.ts +230 -0
- package/src/commands/sample.ts +462 -0
- package/src/commands/synth.ts +229 -0
- package/src/commands/wavetable.ts +370 -0
- package/src/main.ts +2136 -170
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +186 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +397 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/media/wavetable.ts +202 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +49 -6
- package/src/session/attach.ts +3 -4
- package/src/session/daemon.ts +25 -8
- package/src/session/delta.ts +249 -0
- package/src/session/naming.ts +6 -38
- package/src/session/port.ts +26 -6
- package/src/session/rebase.ts +38 -8
- package/src/session/store.ts +116 -21
- package/src/tui/audition.ts +501 -0
- package/src/tui/euclid.ts +472 -0
- package/src/tui/menu.ts +2125 -0
- package/src/tui/play-chords.ts +538 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +984 -0
- package/src/tui/sketch.ts +108 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +684 -0
- package/tui/activity.ts +42 -3
- package/tui/app.ts +398 -33
- package/tui/drums.ts +44 -0
- package/tui/grammar.ts +281 -0
- package/tui/highway.ts +24 -2
- package/tui/layers.ts +14 -2
- package/tui/play-strip.ts +188 -0
package/core/fx.ts
ADDED
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The track effects chain: one table that drives validation, the SDK,
|
|
3
|
+
* printing, diffs, the `/menu`, the `fx` prompt command and the agent tools.
|
|
4
|
+
*
|
|
5
|
+
* Parameter names follow Strudel's where one exists (documented in
|
|
6
|
+
* DAWG.md "Effects"); the DSP is dawg's own (clean-room, from public
|
|
7
|
+
* documentation and standard DSP literature), in `src/audio/effects/`.
|
|
8
|
+
*
|
|
9
|
+
* `filter`, `delay` and `reverb` predate this table and keep their own
|
|
10
|
+
* track fields (`track.filter`, `track.delay`, `track.reverb`) and lanes so
|
|
11
|
+
* existing documents decode and render unchanged; every other effect lives
|
|
12
|
+
* under `track.fx.<name>` with every parameter stored once enabled.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { SYNTH_LANE_PARAMS } from "./synth.ts";
|
|
16
|
+
import {
|
|
17
|
+
FxValidationError,
|
|
18
|
+
normalizeParam,
|
|
19
|
+
normalizeParams,
|
|
20
|
+
isRecord,
|
|
21
|
+
type BooleanParam,
|
|
22
|
+
type EnumParam,
|
|
23
|
+
type FxValues,
|
|
24
|
+
type NumberParam,
|
|
25
|
+
type ParamSpec,
|
|
26
|
+
} from "./params.ts";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
FxValidationError,
|
|
30
|
+
normalizeParam,
|
|
31
|
+
normalizeParams,
|
|
32
|
+
type BooleanParam,
|
|
33
|
+
type EnumParam,
|
|
34
|
+
type FxValues,
|
|
35
|
+
type NumberParam,
|
|
36
|
+
type ParamSpec,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fixed processing order; mono stages run before pan, stereo after. The
|
|
41
|
+
* last two act at the mix rather than inside the track: `orbit` names the
|
|
42
|
+
* bus the track plays on and `duck` lowers another orbit at each note.
|
|
43
|
+
*/
|
|
44
|
+
export const FX_CHAIN = Object.freeze([
|
|
45
|
+
"filter",
|
|
46
|
+
"djf",
|
|
47
|
+
"autofilter",
|
|
48
|
+
"vowel",
|
|
49
|
+
"crush",
|
|
50
|
+
"distort",
|
|
51
|
+
"tremolo",
|
|
52
|
+
"compressor",
|
|
53
|
+
"pan",
|
|
54
|
+
"phaser",
|
|
55
|
+
"chorus",
|
|
56
|
+
"leslie",
|
|
57
|
+
"postgain",
|
|
58
|
+
"delay",
|
|
59
|
+
"reverb",
|
|
60
|
+
"orbit",
|
|
61
|
+
"duck",
|
|
62
|
+
] as const);
|
|
63
|
+
|
|
64
|
+
export type EffectSpec = Readonly<{
|
|
65
|
+
label: string;
|
|
66
|
+
doc: string;
|
|
67
|
+
/** Parameters in display order; the first `simple` are the menu's top level. */
|
|
68
|
+
params: Readonly<Record<string, ParamSpec>>;
|
|
69
|
+
simple: readonly string[];
|
|
70
|
+
/** One-line Strudel equivalent, for the mapping table. */
|
|
71
|
+
strudel: string;
|
|
72
|
+
}>;
|
|
73
|
+
|
|
74
|
+
/** Highest orbit number (`orbit`, `duck.orbit`). */
|
|
75
|
+
export const MAX_ORBIT = 16;
|
|
76
|
+
|
|
77
|
+
const LFO_SHAPES = ["sine", "tri", "square", "saw", "ramp", "random"] as const;
|
|
78
|
+
export type LfoShape = (typeof LFO_SHAPES)[number];
|
|
79
|
+
|
|
80
|
+
const rate = (fallback: number, strudel?: readonly string[]): NumberParam => ({
|
|
81
|
+
kind: "number",
|
|
82
|
+
min: 0.01,
|
|
83
|
+
max: 40,
|
|
84
|
+
default: fallback,
|
|
85
|
+
step: 0.1,
|
|
86
|
+
unit: "Hz",
|
|
87
|
+
automate: true,
|
|
88
|
+
doc: "LFO rate in Hz (used when sync is 0)",
|
|
89
|
+
...(strudel ? { strudel } : {}),
|
|
90
|
+
});
|
|
91
|
+
const sync = (fallback: number, strudel?: readonly string[]): NumberParam => ({
|
|
92
|
+
kind: "number",
|
|
93
|
+
min: 0,
|
|
94
|
+
max: 64,
|
|
95
|
+
default: fallback,
|
|
96
|
+
step: 0.25,
|
|
97
|
+
unit: "beats",
|
|
98
|
+
doc: "LFO cycle length in beats (tempo-synced); 0 uses rate in Hz",
|
|
99
|
+
...(strudel ? { strudel } : {}),
|
|
100
|
+
});
|
|
101
|
+
const mix = (fallback: number, strudel?: readonly string[]): NumberParam => ({
|
|
102
|
+
kind: "number",
|
|
103
|
+
min: 0,
|
|
104
|
+
max: 1,
|
|
105
|
+
default: fallback,
|
|
106
|
+
step: 0.05,
|
|
107
|
+
automate: true,
|
|
108
|
+
doc: "wet/dry balance 0..1",
|
|
109
|
+
...(strudel ? { strudel } : {}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Effects stored under `track.fx`. Every number is bounded; enabling an
|
|
114
|
+
* effect stores all of its parameters so a document says exactly what plays.
|
|
115
|
+
*/
|
|
116
|
+
export const FX_SPECS = Object.freeze({
|
|
117
|
+
autofilter: {
|
|
118
|
+
label: "auto filter",
|
|
119
|
+
doc: "filter whose cutoff an LFO sweeps around a centre, optionally opened by the input level",
|
|
120
|
+
simple: ["type", "cutoff", "depth", "sync", "shape"],
|
|
121
|
+
strudel:
|
|
122
|
+
"none built in (Strudel fakes it with lpf(sine.range(..)).seg(n)); names follow lpf/lpq",
|
|
123
|
+
params: {
|
|
124
|
+
type: {
|
|
125
|
+
kind: "enum",
|
|
126
|
+
values: ["lpf", "hpf", "bpf"],
|
|
127
|
+
default: "lpf",
|
|
128
|
+
doc: "filter type",
|
|
129
|
+
strudel: ["ftype-like: lpf/hpf/bpf"],
|
|
130
|
+
},
|
|
131
|
+
cutoff: {
|
|
132
|
+
kind: "number",
|
|
133
|
+
min: 20,
|
|
134
|
+
max: 20_000,
|
|
135
|
+
default: 1200,
|
|
136
|
+
step: "log",
|
|
137
|
+
unit: "Hz",
|
|
138
|
+
automate: true,
|
|
139
|
+
doc: "centre cutoff",
|
|
140
|
+
strudel: ["lpf", "cutoff"],
|
|
141
|
+
},
|
|
142
|
+
resonance: {
|
|
143
|
+
kind: "number",
|
|
144
|
+
min: 0,
|
|
145
|
+
max: 1,
|
|
146
|
+
default: 0.3,
|
|
147
|
+
step: 0.05,
|
|
148
|
+
automate: true,
|
|
149
|
+
doc: "resonance 0..1 (Q 0.707..8)",
|
|
150
|
+
strudel: ["lpq", "resonance"],
|
|
151
|
+
},
|
|
152
|
+
depth: {
|
|
153
|
+
kind: "number",
|
|
154
|
+
min: 0,
|
|
155
|
+
max: 6,
|
|
156
|
+
default: 2,
|
|
157
|
+
step: 0.25,
|
|
158
|
+
unit: "oct",
|
|
159
|
+
automate: true,
|
|
160
|
+
doc: "sweep width in octaves around the centre",
|
|
161
|
+
},
|
|
162
|
+
sync: sync(4),
|
|
163
|
+
rate: rate(0.5),
|
|
164
|
+
shape: {
|
|
165
|
+
kind: "enum",
|
|
166
|
+
values: LFO_SHAPES,
|
|
167
|
+
default: "sine",
|
|
168
|
+
doc: "LFO shape; random is sample-and-hold, one step per cycle",
|
|
169
|
+
},
|
|
170
|
+
phase: {
|
|
171
|
+
kind: "number",
|
|
172
|
+
min: 0,
|
|
173
|
+
max: 1,
|
|
174
|
+
default: 0,
|
|
175
|
+
step: 0.05,
|
|
176
|
+
doc: "LFO start phase in cycles",
|
|
177
|
+
},
|
|
178
|
+
follow: {
|
|
179
|
+
kind: "number",
|
|
180
|
+
min: -6,
|
|
181
|
+
max: 6,
|
|
182
|
+
default: 0,
|
|
183
|
+
step: 0.25,
|
|
184
|
+
unit: "oct",
|
|
185
|
+
automate: true,
|
|
186
|
+
doc: "envelope follower: octaves the cutoff moves at full input level",
|
|
187
|
+
strudel: ["lpenv (per note, see synth)"],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
djf: {
|
|
192
|
+
label: "dj filter",
|
|
193
|
+
doc: "one-knob filter: below 0.5 low-pass, above 0.5 high-pass, 0.5 open",
|
|
194
|
+
simple: ["value"],
|
|
195
|
+
strudel: "djf(0..1)",
|
|
196
|
+
params: {
|
|
197
|
+
value: {
|
|
198
|
+
kind: "number",
|
|
199
|
+
min: 0,
|
|
200
|
+
max: 1,
|
|
201
|
+
default: 0.5,
|
|
202
|
+
step: 0.02,
|
|
203
|
+
automate: true,
|
|
204
|
+
doc: "0 dark … 0.5 open … 1 thin",
|
|
205
|
+
strudel: ["djf"],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
vowel: {
|
|
210
|
+
label: "vowel",
|
|
211
|
+
doc: "formant filter bank: five band-passes per vowel",
|
|
212
|
+
simple: ["vowel", "mix"],
|
|
213
|
+
strudel: "vowel(a|e|i|o|u)",
|
|
214
|
+
params: {
|
|
215
|
+
vowel: {
|
|
216
|
+
kind: "enum",
|
|
217
|
+
values: [
|
|
218
|
+
"a",
|
|
219
|
+
"e",
|
|
220
|
+
"i",
|
|
221
|
+
"o",
|
|
222
|
+
"u",
|
|
223
|
+
"ae",
|
|
224
|
+
"aa",
|
|
225
|
+
"oe",
|
|
226
|
+
"ue",
|
|
227
|
+
"y",
|
|
228
|
+
"uh",
|
|
229
|
+
"un",
|
|
230
|
+
"en",
|
|
231
|
+
"an",
|
|
232
|
+
"on",
|
|
233
|
+
],
|
|
234
|
+
default: "a",
|
|
235
|
+
doc: "vowel formants (a e i o u plus the extended set)",
|
|
236
|
+
strudel: ["vowel"],
|
|
237
|
+
},
|
|
238
|
+
mix: mix(1),
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
crush: {
|
|
242
|
+
label: "bitcrush",
|
|
243
|
+
doc: "bit-depth and sample-rate reduction",
|
|
244
|
+
simple: ["bits", "coarse", "mix"],
|
|
245
|
+
strudel: "crush(bits) coarse(factor)",
|
|
246
|
+
params: {
|
|
247
|
+
bits: {
|
|
248
|
+
kind: "number",
|
|
249
|
+
min: 1,
|
|
250
|
+
max: 16,
|
|
251
|
+
default: 8,
|
|
252
|
+
step: 0.5,
|
|
253
|
+
automate: true,
|
|
254
|
+
doc: "bit depth: 1 heavy .. 16 nearly clean",
|
|
255
|
+
strudel: ["crush"],
|
|
256
|
+
},
|
|
257
|
+
coarse: {
|
|
258
|
+
kind: "number",
|
|
259
|
+
min: 1,
|
|
260
|
+
max: 64,
|
|
261
|
+
default: 1,
|
|
262
|
+
step: 1,
|
|
263
|
+
integer: true,
|
|
264
|
+
doc: "sample-and-hold factor: 1 off, 2 half rate, 3 a third…",
|
|
265
|
+
strudel: ["coarse"],
|
|
266
|
+
},
|
|
267
|
+
mix: mix(1),
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
distort: {
|
|
271
|
+
label: "distortion",
|
|
272
|
+
doc: "waveshaper with drive, post-tone and automatic gain compensation",
|
|
273
|
+
simple: ["drive", "tone", "mix"],
|
|
274
|
+
strudel: 'distort("amount:postgain:type"), shape(amount)',
|
|
275
|
+
params: {
|
|
276
|
+
drive: {
|
|
277
|
+
kind: "number",
|
|
278
|
+
min: 0,
|
|
279
|
+
max: 10,
|
|
280
|
+
default: 2,
|
|
281
|
+
step: 0.25,
|
|
282
|
+
automate: true,
|
|
283
|
+
doc: "drive 0..10 (Strudel distort amount)",
|
|
284
|
+
strudel: ["distort", "dist"],
|
|
285
|
+
},
|
|
286
|
+
type: {
|
|
287
|
+
kind: "enum",
|
|
288
|
+
values: [
|
|
289
|
+
"soft",
|
|
290
|
+
"hard",
|
|
291
|
+
"cubic",
|
|
292
|
+
"diode",
|
|
293
|
+
"asym",
|
|
294
|
+
"fold",
|
|
295
|
+
"sinefold",
|
|
296
|
+
"chebyshev",
|
|
297
|
+
"scurve",
|
|
298
|
+
"shape",
|
|
299
|
+
],
|
|
300
|
+
default: "soft",
|
|
301
|
+
doc: "curve: soft (tanh), hard clip, cubic, diode, asym, fold, sinefold, chebyshev, scurve, shape (Strudel shape's curve)",
|
|
302
|
+
strudel: ["distort type (3rd field)", "shape → type shape"],
|
|
303
|
+
},
|
|
304
|
+
tone: {
|
|
305
|
+
kind: "number",
|
|
306
|
+
min: 200,
|
|
307
|
+
max: 20_000,
|
|
308
|
+
default: 8000,
|
|
309
|
+
step: "log",
|
|
310
|
+
unit: "Hz",
|
|
311
|
+
automate: true,
|
|
312
|
+
doc: "low-pass after the shaper; tames fizz",
|
|
313
|
+
},
|
|
314
|
+
mix: mix(1),
|
|
315
|
+
postgain: {
|
|
316
|
+
kind: "number",
|
|
317
|
+
min: 0,
|
|
318
|
+
max: 2,
|
|
319
|
+
default: 1,
|
|
320
|
+
step: 0.05,
|
|
321
|
+
doc: "linear gain after compensation (Strudel distort postgain)",
|
|
322
|
+
strudel: ["distort postgain"],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
tremolo: {
|
|
327
|
+
label: "tremolo",
|
|
328
|
+
doc: "volume modulation",
|
|
329
|
+
simple: ["sync", "depth", "shape"],
|
|
330
|
+
strudel:
|
|
331
|
+
"tremolosync/tremolodepth/tremoloshape/tremoloskew/tremolophase (am)",
|
|
332
|
+
params: {
|
|
333
|
+
sync: sync(0.5, ["tremolosync", "tremsync"]),
|
|
334
|
+
rate: rate(4, ["tremolo"]),
|
|
335
|
+
depth: {
|
|
336
|
+
kind: "number",
|
|
337
|
+
min: 0,
|
|
338
|
+
max: 1,
|
|
339
|
+
default: 0.5,
|
|
340
|
+
step: 0.05,
|
|
341
|
+
automate: true,
|
|
342
|
+
doc: "how far the level dips, 0..1",
|
|
343
|
+
strudel: ["tremolodepth", "tremdepth"],
|
|
344
|
+
},
|
|
345
|
+
shape: {
|
|
346
|
+
kind: "enum",
|
|
347
|
+
values: LFO_SHAPES.filter((shape) => shape !== "random"),
|
|
348
|
+
default: "sine",
|
|
349
|
+
doc: "LFO shape",
|
|
350
|
+
strudel: ["tremoloshape", "tremshape"],
|
|
351
|
+
},
|
|
352
|
+
skew: {
|
|
353
|
+
kind: "number",
|
|
354
|
+
min: 0,
|
|
355
|
+
max: 1,
|
|
356
|
+
default: 0.5,
|
|
357
|
+
step: 0.05,
|
|
358
|
+
doc: "where in the cycle the peak falls (0.5 symmetric)",
|
|
359
|
+
strudel: ["tremoloskew", "tremskew"],
|
|
360
|
+
},
|
|
361
|
+
phase: {
|
|
362
|
+
kind: "number",
|
|
363
|
+
min: 0,
|
|
364
|
+
max: 1,
|
|
365
|
+
default: 0,
|
|
366
|
+
step: 0.05,
|
|
367
|
+
doc: "start phase in cycles",
|
|
368
|
+
strudel: ["tremolophase", "tremphase"],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
compressor: {
|
|
373
|
+
label: "compressor",
|
|
374
|
+
doc: "feed-forward RMS-ish compressor with soft knee and make-up gain",
|
|
375
|
+
simple: ["threshold", "ratio", "makeup"],
|
|
376
|
+
strudel: 'compressor("threshold:ratio:knee:attack:release")',
|
|
377
|
+
params: {
|
|
378
|
+
threshold: {
|
|
379
|
+
kind: "number",
|
|
380
|
+
min: -60,
|
|
381
|
+
max: 0,
|
|
382
|
+
default: -18,
|
|
383
|
+
step: 1,
|
|
384
|
+
unit: "dB",
|
|
385
|
+
automate: true,
|
|
386
|
+
doc: "level where compression starts",
|
|
387
|
+
strudel: ["compressor threshold"],
|
|
388
|
+
},
|
|
389
|
+
ratio: {
|
|
390
|
+
kind: "number",
|
|
391
|
+
min: 1,
|
|
392
|
+
max: 20,
|
|
393
|
+
default: 4,
|
|
394
|
+
step: 0.5,
|
|
395
|
+
doc: "input:output above threshold",
|
|
396
|
+
strudel: ["compressorRatio"],
|
|
397
|
+
},
|
|
398
|
+
knee: {
|
|
399
|
+
kind: "number",
|
|
400
|
+
min: 0,
|
|
401
|
+
max: 24,
|
|
402
|
+
default: 6,
|
|
403
|
+
step: 1,
|
|
404
|
+
unit: "dB",
|
|
405
|
+
doc: "soft-knee width",
|
|
406
|
+
strudel: ["compressorKnee"],
|
|
407
|
+
},
|
|
408
|
+
attack: {
|
|
409
|
+
kind: "number",
|
|
410
|
+
min: 0.0001,
|
|
411
|
+
max: 1,
|
|
412
|
+
default: 0.01,
|
|
413
|
+
step: 0.005,
|
|
414
|
+
unit: "s",
|
|
415
|
+
doc: "attack time",
|
|
416
|
+
strudel: ["compressorAttack"],
|
|
417
|
+
},
|
|
418
|
+
release: {
|
|
419
|
+
kind: "number",
|
|
420
|
+
min: 0.01,
|
|
421
|
+
max: 2,
|
|
422
|
+
default: 0.15,
|
|
423
|
+
step: 0.05,
|
|
424
|
+
unit: "s",
|
|
425
|
+
doc: "release time",
|
|
426
|
+
strudel: ["compressorRelease"],
|
|
427
|
+
},
|
|
428
|
+
makeup: {
|
|
429
|
+
kind: "number",
|
|
430
|
+
min: 0,
|
|
431
|
+
max: 24,
|
|
432
|
+
default: 5,
|
|
433
|
+
step: 0.5,
|
|
434
|
+
unit: "dB",
|
|
435
|
+
automate: true,
|
|
436
|
+
doc: "gain after compression",
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
phaser: {
|
|
441
|
+
label: "phaser",
|
|
442
|
+
doc: "four-stage all-pass phaser",
|
|
443
|
+
simple: ["rate", "depth"],
|
|
444
|
+
strudel: "phaser/phaserdepth/phasercenter/phasersweep",
|
|
445
|
+
params: {
|
|
446
|
+
rate: rate(0.5, ["phaser", "ph"]),
|
|
447
|
+
sync: sync(0),
|
|
448
|
+
depth: {
|
|
449
|
+
kind: "number",
|
|
450
|
+
min: 0,
|
|
451
|
+
max: 1,
|
|
452
|
+
default: 0.75,
|
|
453
|
+
step: 0.05,
|
|
454
|
+
automate: true,
|
|
455
|
+
doc: "notch depth (wet amount)",
|
|
456
|
+
strudel: ["phaserdepth", "phd", "phasdp"],
|
|
457
|
+
},
|
|
458
|
+
center: {
|
|
459
|
+
kind: "number",
|
|
460
|
+
min: 100,
|
|
461
|
+
max: 10_000,
|
|
462
|
+
default: 1000,
|
|
463
|
+
step: "log",
|
|
464
|
+
unit: "Hz",
|
|
465
|
+
doc: "sweep centre",
|
|
466
|
+
strudel: ["phasercenter", "phc"],
|
|
467
|
+
},
|
|
468
|
+
sweep: {
|
|
469
|
+
kind: "number",
|
|
470
|
+
min: 0,
|
|
471
|
+
max: 8000,
|
|
472
|
+
default: 2000,
|
|
473
|
+
step: 100,
|
|
474
|
+
unit: "Hz",
|
|
475
|
+
doc: "sweep range",
|
|
476
|
+
strudel: ["phasersweep", "phs"],
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
chorus: {
|
|
481
|
+
label: "chorus",
|
|
482
|
+
doc: "stereo chorus: two modulated delay taps in quadrature",
|
|
483
|
+
simple: ["rate", "depth", "mix"],
|
|
484
|
+
strudel:
|
|
485
|
+
"none in superdough (SuperDirt has no chorus either); standard parameters",
|
|
486
|
+
params: {
|
|
487
|
+
rate: rate(0.8),
|
|
488
|
+
depth: {
|
|
489
|
+
kind: "number",
|
|
490
|
+
min: 0,
|
|
491
|
+
max: 1,
|
|
492
|
+
default: 0.4,
|
|
493
|
+
step: 0.05,
|
|
494
|
+
automate: true,
|
|
495
|
+
doc: "modulation depth (0..1 → 0..6 ms)",
|
|
496
|
+
},
|
|
497
|
+
mix: mix(0.5),
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
leslie: {
|
|
501
|
+
label: "leslie",
|
|
502
|
+
doc: "rotary speaker: Doppler vibrato plus left/right amplitude rotation",
|
|
503
|
+
simple: ["mix", "rate"],
|
|
504
|
+
strudel: "leslie(wet) lrate(Hz) lsize(0..1)",
|
|
505
|
+
params: {
|
|
506
|
+
mix: mix(1, ["leslie"]),
|
|
507
|
+
rate: {
|
|
508
|
+
...rate(6.7, ["lrate"]),
|
|
509
|
+
doc: "rotation in Hz: 6.7 fast, 0.7 slow",
|
|
510
|
+
},
|
|
511
|
+
size: {
|
|
512
|
+
kind: "number",
|
|
513
|
+
min: 0,
|
|
514
|
+
max: 1,
|
|
515
|
+
default: 0.5,
|
|
516
|
+
step: 0.05,
|
|
517
|
+
doc: "cabinet size: Doppler (pitch warble) amount",
|
|
518
|
+
strudel: ["lsize"],
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
postgain: {
|
|
523
|
+
label: "post gain",
|
|
524
|
+
doc: "linear gain after every insert, before the delay and reverb sends",
|
|
525
|
+
simple: ["gain"],
|
|
526
|
+
strudel: "postgain(gain)",
|
|
527
|
+
params: {
|
|
528
|
+
gain: {
|
|
529
|
+
kind: "number",
|
|
530
|
+
min: 0,
|
|
531
|
+
max: 4,
|
|
532
|
+
default: 1,
|
|
533
|
+
step: 0.05,
|
|
534
|
+
automate: true,
|
|
535
|
+
doc: "linear gain 0..4",
|
|
536
|
+
strudel: ["postgain", "post"],
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
orbit: {
|
|
541
|
+
label: "orbit",
|
|
542
|
+
doc: "the bus this track plays on (1 when off); another track's duck targets it",
|
|
543
|
+
simple: ["orbit"],
|
|
544
|
+
strudel: "orbit(n)",
|
|
545
|
+
params: {
|
|
546
|
+
orbit: {
|
|
547
|
+
kind: "number",
|
|
548
|
+
min: 1,
|
|
549
|
+
max: MAX_ORBIT,
|
|
550
|
+
default: 2,
|
|
551
|
+
step: 1,
|
|
552
|
+
integer: true,
|
|
553
|
+
doc: "orbit number; tracks without this effect are on orbit 1",
|
|
554
|
+
strudel: ["orbit", "o"],
|
|
555
|
+
},
|
|
556
|
+
shared: {
|
|
557
|
+
kind: "boolean",
|
|
558
|
+
default: false,
|
|
559
|
+
optional: true,
|
|
560
|
+
doc: "share one delay and one reverb with the orbit's other shared tracks, as Strudel orbits do (this track's delay/reverb mix becomes its send level)",
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
duck: {
|
|
565
|
+
label: "duck",
|
|
566
|
+
doc: "sidechain ducking: each note of this track dips the target orbit, then it recovers",
|
|
567
|
+
simple: ["orbit", "depth", "attack"],
|
|
568
|
+
strudel: "duckorbit(n) duckdepth(0..1) duckattack(s)",
|
|
569
|
+
params: {
|
|
570
|
+
orbit: {
|
|
571
|
+
kind: "number",
|
|
572
|
+
min: 1,
|
|
573
|
+
max: MAX_ORBIT,
|
|
574
|
+
default: 1,
|
|
575
|
+
step: 1,
|
|
576
|
+
integer: true,
|
|
577
|
+
doc: "target orbit to duck (never this track itself)",
|
|
578
|
+
strudel: ["duckorbit", "duck"],
|
|
579
|
+
},
|
|
580
|
+
depth: {
|
|
581
|
+
kind: "number",
|
|
582
|
+
min: 0,
|
|
583
|
+
max: 1,
|
|
584
|
+
default: 1,
|
|
585
|
+
step: 0.05,
|
|
586
|
+
doc: "how far the target dips: 1 to silence, 0 not at all",
|
|
587
|
+
strudel: ["duckdepth"],
|
|
588
|
+
},
|
|
589
|
+
attack: {
|
|
590
|
+
kind: "number",
|
|
591
|
+
min: 0.001,
|
|
592
|
+
max: 4,
|
|
593
|
+
default: 0.1,
|
|
594
|
+
step: 0.01,
|
|
595
|
+
unit: "s",
|
|
596
|
+
doc: "time the target takes to come back to full level",
|
|
597
|
+
strudel: ["duckattack", "duckatt", "datt"],
|
|
598
|
+
},
|
|
599
|
+
onset: {
|
|
600
|
+
kind: "number",
|
|
601
|
+
min: 0,
|
|
602
|
+
max: 0.5,
|
|
603
|
+
default: 0.003,
|
|
604
|
+
step: 0.001,
|
|
605
|
+
unit: "s",
|
|
606
|
+
doc: "time the dip takes to reach full depth",
|
|
607
|
+
strudel: ["duckonset"],
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
} satisfies Record<string, EffectSpec>);
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* The three effects that predate `track.fx` and keep their own track fields.
|
|
615
|
+
* Parameters marked `optional` are new: absent, the effect renders exactly
|
|
616
|
+
* as it did before they existed. `lane` names the legacy automation lane.
|
|
617
|
+
*/
|
|
618
|
+
export const TRACK_EFFECT_SPECS = Object.freeze({
|
|
619
|
+
filter: {
|
|
620
|
+
label: "filter",
|
|
621
|
+
doc: "resonant biquad: low-pass, high-pass or band-pass",
|
|
622
|
+
simple: ["type", "cutoff", "resonance"],
|
|
623
|
+
strudel: "lpf/hpf/bpf (cutoff, hcutoff, bandf) with lpq/hpq/bpq and ftype",
|
|
624
|
+
params: {
|
|
625
|
+
type: {
|
|
626
|
+
kind: "enum",
|
|
627
|
+
values: ["lpf", "hpf", "bpf"],
|
|
628
|
+
default: "lpf",
|
|
629
|
+
optional: true,
|
|
630
|
+
doc: "lpf low-pass (default), hpf high-pass, bpf band-pass",
|
|
631
|
+
strudel: ["lpf", "hpf", "bpf"],
|
|
632
|
+
},
|
|
633
|
+
ftype: {
|
|
634
|
+
kind: "enum",
|
|
635
|
+
values: ["12db", "24db", "ladder"],
|
|
636
|
+
default: "12db",
|
|
637
|
+
optional: true,
|
|
638
|
+
doc: "slope: 12db biquad (default), 24db two biquads, ladder 4-pole (low-pass only)",
|
|
639
|
+
strudel: ["ftype"],
|
|
640
|
+
},
|
|
641
|
+
cutoff: {
|
|
642
|
+
kind: "number",
|
|
643
|
+
min: 20,
|
|
644
|
+
max: 20_000,
|
|
645
|
+
default: 2000,
|
|
646
|
+
step: "log",
|
|
647
|
+
unit: "Hz",
|
|
648
|
+
automate: true,
|
|
649
|
+
doc: "cutoff (lpf/hpf) or centre (bpf) frequency",
|
|
650
|
+
strudel: [
|
|
651
|
+
"lpf",
|
|
652
|
+
"cutoff",
|
|
653
|
+
"ctf",
|
|
654
|
+
"lp",
|
|
655
|
+
"hpf",
|
|
656
|
+
"hcutoff",
|
|
657
|
+
"bpf",
|
|
658
|
+
"bandf",
|
|
659
|
+
],
|
|
660
|
+
},
|
|
661
|
+
resonance: {
|
|
662
|
+
kind: "number",
|
|
663
|
+
min: 0,
|
|
664
|
+
max: 1,
|
|
665
|
+
default: 0,
|
|
666
|
+
step: 0.05,
|
|
667
|
+
automate: true,
|
|
668
|
+
doc: "resonance 0..1 (Q 0.707..8)",
|
|
669
|
+
strudel: ["lpq", "resonance", "hpq", "hresonance", "bpq", "bandq"],
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
delay: {
|
|
674
|
+
label: "delay",
|
|
675
|
+
doc: "tempo-synced stereo delay send; ping-pong with a high-cut on the repeats by default",
|
|
676
|
+
simple: ["beats", "feedback", "mix"],
|
|
677
|
+
strudel: 'delay("level:time:feedback"), delaytime, delayfeedback',
|
|
678
|
+
params: {
|
|
679
|
+
beats: {
|
|
680
|
+
kind: "number",
|
|
681
|
+
min: 0.0625,
|
|
682
|
+
max: 4,
|
|
683
|
+
default: 0.75,
|
|
684
|
+
step: 0.0625,
|
|
685
|
+
unit: "beats",
|
|
686
|
+
doc: "delay time in beats (0.75 = dotted eighth)",
|
|
687
|
+
strudel: ["delaytime (seconds = beats·60/bpm)"],
|
|
688
|
+
},
|
|
689
|
+
feedback: {
|
|
690
|
+
kind: "number",
|
|
691
|
+
min: 0,
|
|
692
|
+
max: 0.9,
|
|
693
|
+
default: 0.35,
|
|
694
|
+
step: 0.05,
|
|
695
|
+
automate: true,
|
|
696
|
+
doc: "fraction of each echo fed back",
|
|
697
|
+
strudel: ["delayfeedback", "delayfb", "dfb"],
|
|
698
|
+
},
|
|
699
|
+
mix: {
|
|
700
|
+
kind: "number",
|
|
701
|
+
min: 0,
|
|
702
|
+
max: 1,
|
|
703
|
+
default: 0.25,
|
|
704
|
+
step: 0.05,
|
|
705
|
+
automate: true,
|
|
706
|
+
doc: "wet level",
|
|
707
|
+
strudel: ["delay"],
|
|
708
|
+
},
|
|
709
|
+
time: {
|
|
710
|
+
kind: "number",
|
|
711
|
+
min: 0,
|
|
712
|
+
max: 4,
|
|
713
|
+
default: 0,
|
|
714
|
+
step: 0.01,
|
|
715
|
+
unit: "s",
|
|
716
|
+
optional: true,
|
|
717
|
+
doc: "delay time in seconds; 0 or absent uses beats",
|
|
718
|
+
strudel: ["delaytime", "delayt", "dt"],
|
|
719
|
+
},
|
|
720
|
+
pingpong: {
|
|
721
|
+
kind: "boolean",
|
|
722
|
+
default: true,
|
|
723
|
+
optional: true,
|
|
724
|
+
doc: "repeats alternate left/right (absent: the original cross-fed stereo)",
|
|
725
|
+
},
|
|
726
|
+
highcut: {
|
|
727
|
+
kind: "number",
|
|
728
|
+
min: 500,
|
|
729
|
+
max: 20_000,
|
|
730
|
+
default: 5000,
|
|
731
|
+
step: "log",
|
|
732
|
+
unit: "Hz",
|
|
733
|
+
optional: true,
|
|
734
|
+
doc: "low-pass inside the feedback loop so repeats darken",
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
reverb: {
|
|
739
|
+
label: "reverb",
|
|
740
|
+
doc: "algorithmic stereo reverb send (eight combs, four allpasses per side)",
|
|
741
|
+
simple: ["mix", "size"],
|
|
742
|
+
strudel: 'room("level:size"), roomsize, roomfade, roomlp, roomdim',
|
|
743
|
+
params: {
|
|
744
|
+
mix: {
|
|
745
|
+
kind: "number",
|
|
746
|
+
min: 0,
|
|
747
|
+
max: 1,
|
|
748
|
+
default: 0.3,
|
|
749
|
+
step: 0.05,
|
|
750
|
+
automate: true,
|
|
751
|
+
doc: "wet level",
|
|
752
|
+
strudel: ["room"],
|
|
753
|
+
},
|
|
754
|
+
size: {
|
|
755
|
+
kind: "number",
|
|
756
|
+
min: 0,
|
|
757
|
+
max: 1,
|
|
758
|
+
default: 0.5,
|
|
759
|
+
step: 0.05,
|
|
760
|
+
doc: "room size (Strudel roomsize 0..10 = size·10)",
|
|
761
|
+
strudel: ["roomsize", "rsize", "sz", "size"],
|
|
762
|
+
},
|
|
763
|
+
fade: {
|
|
764
|
+
kind: "number",
|
|
765
|
+
min: 0.1,
|
|
766
|
+
max: 20,
|
|
767
|
+
default: 2,
|
|
768
|
+
step: 0.1,
|
|
769
|
+
unit: "s",
|
|
770
|
+
optional: true,
|
|
771
|
+
doc: "decay time to -60 dB; overrides the size-derived decay",
|
|
772
|
+
strudel: ["roomfade", "rfade"],
|
|
773
|
+
},
|
|
774
|
+
lowpass: {
|
|
775
|
+
kind: "number",
|
|
776
|
+
min: 200,
|
|
777
|
+
max: 20_000,
|
|
778
|
+
default: 8000,
|
|
779
|
+
step: "log",
|
|
780
|
+
unit: "Hz",
|
|
781
|
+
optional: true,
|
|
782
|
+
doc: "low-pass on the reverb input",
|
|
783
|
+
strudel: ["roomlp", "rlp"],
|
|
784
|
+
},
|
|
785
|
+
dim: {
|
|
786
|
+
kind: "number",
|
|
787
|
+
min: 200,
|
|
788
|
+
max: 20_000,
|
|
789
|
+
default: 3000,
|
|
790
|
+
step: "log",
|
|
791
|
+
unit: "Hz",
|
|
792
|
+
optional: true,
|
|
793
|
+
doc: "damping: the tail darkens toward this frequency as it decays",
|
|
794
|
+
strudel: ["roomdim", "rdim"],
|
|
795
|
+
},
|
|
796
|
+
predelay: {
|
|
797
|
+
kind: "number",
|
|
798
|
+
min: 0,
|
|
799
|
+
max: 0.5,
|
|
800
|
+
default: 0.02,
|
|
801
|
+
step: 0.005,
|
|
802
|
+
unit: "s",
|
|
803
|
+
optional: true,
|
|
804
|
+
doc: "gap before the tail starts",
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
} satisfies Record<string, EffectSpec>);
|
|
809
|
+
|
|
810
|
+
export type TrackEffectName = keyof typeof TRACK_EFFECT_SPECS;
|
|
811
|
+
export const TRACK_EFFECT_NAMES = Object.freeze(
|
|
812
|
+
Object.keys(TRACK_EFFECT_SPECS) as TrackEffectName[],
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
/** Every effect the `fx` grammar and the menu address, in chain order. */
|
|
816
|
+
export type EffectName = TrackEffectName | FxName;
|
|
817
|
+
export const EFFECT_NAMES: readonly EffectName[] = Object.freeze(
|
|
818
|
+
FX_CHAIN.filter((stage): stage is EffectName => stage !== "pan"),
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* The core set, in chain order: the effects the menu and brief lead with.
|
|
823
|
+
* The rest (dj filter, vowel, bitcrush, phaser, leslie, post gain) are
|
|
824
|
+
* there for Strudel parity.
|
|
825
|
+
*/
|
|
826
|
+
export const CORE_EFFECTS: readonly EffectName[] = Object.freeze([
|
|
827
|
+
"filter",
|
|
828
|
+
"autofilter",
|
|
829
|
+
"distort",
|
|
830
|
+
"tremolo",
|
|
831
|
+
"compressor",
|
|
832
|
+
"chorus",
|
|
833
|
+
"delay",
|
|
834
|
+
"reverb",
|
|
835
|
+
] as const);
|
|
836
|
+
|
|
837
|
+
export function isEffectName(value: unknown): value is EffectName {
|
|
838
|
+
return (
|
|
839
|
+
typeof value === "string" &&
|
|
840
|
+
(EFFECT_NAMES as readonly string[]).includes(value)
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export function effectSpec(name: EffectName): EffectSpec {
|
|
845
|
+
return (
|
|
846
|
+
name in TRACK_EFFECT_SPECS
|
|
847
|
+
? TRACK_EFFECT_SPECS[name as TrackEffectName]
|
|
848
|
+
: FX_SPECS[name as FxName]
|
|
849
|
+
) as EffectSpec;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Legacy automation lanes for track-effect parameters. */
|
|
853
|
+
export const TRACK_EFFECT_LANES: Readonly<Record<string, string>> =
|
|
854
|
+
Object.freeze({
|
|
855
|
+
"filter-cutoff": "filter",
|
|
856
|
+
"filter-resonance": "resonance",
|
|
857
|
+
"delay-feedback": "delay-feedback",
|
|
858
|
+
"delay-mix": "delay-mix",
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
export type FxName = keyof typeof FX_SPECS;
|
|
862
|
+
/** `fx` effect names in chain order. */
|
|
863
|
+
export const FX_NAMES = Object.freeze(
|
|
864
|
+
FX_CHAIN.filter((stage): stage is FxName =>
|
|
865
|
+
Object.prototype.hasOwnProperty.call(FX_SPECS, stage),
|
|
866
|
+
),
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
/** One enabled effect's stored parameters. */
|
|
870
|
+
/** `track.fx`: only enabled effects appear. */
|
|
871
|
+
export type TrackFx = Readonly<Partial<Record<FxName, FxValues>>>;
|
|
872
|
+
|
|
873
|
+
export function isFxName(value: unknown): value is FxName {
|
|
874
|
+
return (
|
|
875
|
+
typeof value === "string" &&
|
|
876
|
+
Object.prototype.hasOwnProperty.call(FX_SPECS, value)
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export function fxSpec(name: FxName): EffectSpec {
|
|
881
|
+
return FX_SPECS[name] as EffectSpec;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Thrown by the normalizers; the score wraps it in its own error type. */
|
|
885
|
+
/** Validates `track.fx`; `undefined`/`null`/`{}` means none. */
|
|
886
|
+
export function normalizeFx(input: unknown): TrackFx | undefined {
|
|
887
|
+
if (input === undefined || input === null) return undefined;
|
|
888
|
+
if (!isRecord(input))
|
|
889
|
+
throw new FxValidationError("track fx must be an object or null");
|
|
890
|
+
const out: Partial<Record<FxName, FxValues>> = {};
|
|
891
|
+
for (const [name, value] of Object.entries(input)) {
|
|
892
|
+
if (!isFxName(name))
|
|
893
|
+
throw new FxValidationError(
|
|
894
|
+
`unknown effect "${name}" (effects: ${FX_NAMES.join(", ")})`,
|
|
895
|
+
);
|
|
896
|
+
if (value === undefined || value === null) continue;
|
|
897
|
+
out[name] = normalizeParams(fxSpec(name).params, value, `fx ${name}`);
|
|
898
|
+
}
|
|
899
|
+
// Canonical key order (chain order) so equal documents print equally.
|
|
900
|
+
const ordered: Partial<Record<FxName, FxValues>> = {};
|
|
901
|
+
for (const name of FX_NAMES) if (out[name]) ordered[name] = out[name];
|
|
902
|
+
return Object.keys(ordered).length > 0 ? Object.freeze(ordered) : undefined;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** Automation lane name of an `fx` parameter, e.g. `distort-drive`. */
|
|
906
|
+
export type FxLane = `${FxName | "reverb" | "synth"}-${string}`;
|
|
907
|
+
|
|
908
|
+
/** Every automatable `fx` parameter as `{ lane, effect, param, spec }`. */
|
|
909
|
+
export const FX_LANES: readonly Readonly<{
|
|
910
|
+
lane: FxLane;
|
|
911
|
+
effect: FxName | "reverb" | "synth";
|
|
912
|
+
param: string;
|
|
913
|
+
spec: NumberParam;
|
|
914
|
+
}>[] = Object.freeze([
|
|
915
|
+
...[...FX_NAMES, "reverb" as const].flatMap((effect) =>
|
|
916
|
+
Object.entries(effectSpec(effect).params)
|
|
917
|
+
.filter(
|
|
918
|
+
(entry): entry is [string, NumberParam] =>
|
|
919
|
+
entry[1].kind === "number" && entry[1].automate === true,
|
|
920
|
+
)
|
|
921
|
+
.map(([param, spec]) =>
|
|
922
|
+
Object.freeze({
|
|
923
|
+
lane: `${effect}-${param}` as FxLane,
|
|
924
|
+
effect,
|
|
925
|
+
param,
|
|
926
|
+
spec,
|
|
927
|
+
}),
|
|
928
|
+
),
|
|
929
|
+
),
|
|
930
|
+
// Synth voice parameters (core/synth.ts), read at each note's onset.
|
|
931
|
+
...SYNTH_LANE_PARAMS.map(({ param, spec }) =>
|
|
932
|
+
Object.freeze({
|
|
933
|
+
lane: `synth-${param}` as FxLane,
|
|
934
|
+
effect: "synth" as const,
|
|
935
|
+
param,
|
|
936
|
+
spec,
|
|
937
|
+
}),
|
|
938
|
+
),
|
|
939
|
+
]);
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Named starting points, shown first in the menu (`fx <effect> preset
|
|
943
|
+
* <name>`). A preset is applied over the effect's defaults, so every
|
|
944
|
+
* parameter it leaves out takes its default.
|
|
945
|
+
*/
|
|
946
|
+
export const FX_PRESETS: Readonly<
|
|
947
|
+
Partial<Record<EffectName, Readonly<Record<string, FxValues>>>>
|
|
948
|
+
> = Object.freeze({
|
|
949
|
+
filter: {
|
|
950
|
+
warm: { type: "lpf", cutoff: 1800, resonance: 0.1 },
|
|
951
|
+
dark: { type: "lpf", cutoff: 600, resonance: 0.2 },
|
|
952
|
+
acid: { type: "lpf", ftype: "ladder", cutoff: 900, resonance: 0.7 },
|
|
953
|
+
thin: { type: "hpf", cutoff: 400, resonance: 0.1 },
|
|
954
|
+
telephone: { type: "bpf", cutoff: 1500, resonance: 0.5 },
|
|
955
|
+
},
|
|
956
|
+
djf: { dark: { value: 0.3 }, thin: { value: 0.7 } },
|
|
957
|
+
autofilter: {
|
|
958
|
+
"slow-sweep": { sync: 8, depth: 2.5, shape: "sine", cutoff: 1000 },
|
|
959
|
+
wobble: { sync: 0.5, depth: 2, shape: "sine", cutoff: 600, resonance: 0.5 },
|
|
960
|
+
"s&h": {
|
|
961
|
+
sync: 0.25,
|
|
962
|
+
depth: 2,
|
|
963
|
+
shape: "random",
|
|
964
|
+
cutoff: 1500,
|
|
965
|
+
resonance: 0.4,
|
|
966
|
+
},
|
|
967
|
+
"hpf-rise": { type: "hpf", sync: 16, depth: 3, shape: "saw", cutoff: 400 },
|
|
968
|
+
"env-follow": {
|
|
969
|
+
sync: 0,
|
|
970
|
+
rate: 0.01,
|
|
971
|
+
depth: 0,
|
|
972
|
+
follow: 3,
|
|
973
|
+
cutoff: 400,
|
|
974
|
+
resonance: 0.4,
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
vowel: { a: { vowel: "a" }, o: { vowel: "o" }, ee: { vowel: "i" } },
|
|
978
|
+
crush: {
|
|
979
|
+
"8-bit": { bits: 8, coarse: 1 },
|
|
980
|
+
lofi: { bits: 6, coarse: 4, mix: 0.7 },
|
|
981
|
+
destroy: { bits: 3, coarse: 8 },
|
|
982
|
+
},
|
|
983
|
+
distort: {
|
|
984
|
+
warm: { drive: 1.5, type: "soft", tone: 6000, mix: 1 },
|
|
985
|
+
crunch: { drive: 4, type: "cubic", tone: 5000 },
|
|
986
|
+
fuzz: { drive: 7, type: "hard", tone: 3500 },
|
|
987
|
+
fold: { drive: 5, type: "fold", tone: 7000, mix: 0.7 },
|
|
988
|
+
shape: { drive: 5, type: "shape", tone: 9000 },
|
|
989
|
+
},
|
|
990
|
+
tremolo: {
|
|
991
|
+
gentle: { sync: 1, depth: 0.3, shape: "sine" },
|
|
992
|
+
"eighth-chop": { sync: 0.5, depth: 0.9, shape: "square" },
|
|
993
|
+
pulse: { sync: 0.25, depth: 0.6, shape: "tri" },
|
|
994
|
+
},
|
|
995
|
+
compressor: {
|
|
996
|
+
gentle: { threshold: -18, ratio: 2, attack: 0.02, release: 0.2, makeup: 3 },
|
|
997
|
+
punch: { threshold: -20, ratio: 4, attack: 0.03, release: 0.1, makeup: 6 },
|
|
998
|
+
squash: {
|
|
999
|
+
threshold: -30,
|
|
1000
|
+
ratio: 10,
|
|
1001
|
+
attack: 0.002,
|
|
1002
|
+
release: 0.08,
|
|
1003
|
+
makeup: 12,
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
phaser: { slow: { rate: 0.2, depth: 0.7 }, fast: { rate: 2, depth: 0.8 } },
|
|
1007
|
+
chorus: {
|
|
1008
|
+
subtle: { rate: 0.6, depth: 0.25, mix: 0.35 },
|
|
1009
|
+
wide: { rate: 0.8, depth: 0.5, mix: 0.5 },
|
|
1010
|
+
seasick: { rate: 3, depth: 0.9, mix: 0.6 },
|
|
1011
|
+
},
|
|
1012
|
+
leslie: { fast: { rate: 6.7, mix: 1 }, slow: { rate: 0.7, mix: 1 } },
|
|
1013
|
+
delay: {
|
|
1014
|
+
"ping-pong": {
|
|
1015
|
+
beats: 0.75,
|
|
1016
|
+
feedback: 0.35,
|
|
1017
|
+
mix: 0.25,
|
|
1018
|
+
pingpong: true,
|
|
1019
|
+
highcut: 5000,
|
|
1020
|
+
},
|
|
1021
|
+
"dotted-eighth": {
|
|
1022
|
+
beats: 0.75,
|
|
1023
|
+
feedback: 0.4,
|
|
1024
|
+
mix: 0.3,
|
|
1025
|
+
pingpong: false,
|
|
1026
|
+
highcut: 6000,
|
|
1027
|
+
},
|
|
1028
|
+
slapback: {
|
|
1029
|
+
beats: 0.25,
|
|
1030
|
+
feedback: 0.1,
|
|
1031
|
+
mix: 0.25,
|
|
1032
|
+
pingpong: false,
|
|
1033
|
+
highcut: 8000,
|
|
1034
|
+
},
|
|
1035
|
+
dub: {
|
|
1036
|
+
beats: 1.5,
|
|
1037
|
+
feedback: 0.6,
|
|
1038
|
+
mix: 0.35,
|
|
1039
|
+
pingpong: true,
|
|
1040
|
+
highcut: 2500,
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
reverb: {
|
|
1044
|
+
room: { mix: 0.2, size: 0.3, predelay: 0.01, dim: 6000 },
|
|
1045
|
+
hall: { mix: 0.3, size: 0.8, fade: 3.5, predelay: 0.03, dim: 4000 },
|
|
1046
|
+
plate: { mix: 0.25, size: 0.6, fade: 2, predelay: 0, dim: 9000 },
|
|
1047
|
+
ambient: {
|
|
1048
|
+
mix: 0.5,
|
|
1049
|
+
size: 1,
|
|
1050
|
+
fade: 8,
|
|
1051
|
+
predelay: 0.05,
|
|
1052
|
+
lowpass: 5000,
|
|
1053
|
+
dim: 2500,
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
duck: {
|
|
1057
|
+
pump: { orbit: 2, depth: 0.85, attack: 0.25 },
|
|
1058
|
+
subtle: { orbit: 2, depth: 0.4, attack: 0.12 },
|
|
1059
|
+
gate: { orbit: 2, depth: 1, attack: 0.05 },
|
|
1060
|
+
},
|
|
1061
|
+
} satisfies Partial<Record<EffectName, Record<string, FxValues>>>);
|
|
1062
|
+
|
|
1063
|
+
export function effectPresetNames(effect: EffectName): readonly string[] {
|
|
1064
|
+
return Object.keys(FX_PRESETS[effect] ?? {});
|
|
1065
|
+
}
|