@hraness/dawg 0.5.0 → 0.6.1
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 +63 -0
- package/DAWG.md +592 -49
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +9 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +689 -2
- package/core/granular.ts +619 -0
- package/core/instruments.ts +442 -0
- package/core/keys.ts +955 -0
- package/core/resonators.ts +862 -0
- package/core/score.ts +529 -8
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +247 -16
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2417 -40
- package/core/sections.ts +44 -10
- package/core/strings.ts +1080 -0
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +5 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -2
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +13 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +42 -2
- package/src/agent/preview-tool.ts +7 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +726 -17
- package/src/agent/xcb-agent.ts +13 -0
- package/src/audio/arrange.ts +42 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/envelope.ts +161 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +23 -3
- package/src/audio/effects/common.ts +14 -0
- package/src/audio/effects/convolution.ts +106 -1
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/engine.ts +56 -6
- package/src/audio/fit.ts +447 -0
- package/src/audio/granular.ts +995 -0
- package/src/audio/instrument-check.ts +137 -0
- package/src/audio/instruments.ts +140 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +485 -0
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +476 -0
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +56 -0
- package/src/audio/live.ts +361 -15
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +17 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +295 -0
- package/src/audio/sampler.ts +328 -35
- package/src/audio/samples.ts +56 -8
- package/src/audio/strings/body.ts +263 -0
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +615 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +200 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +191 -19
- package/src/audio/winds/engine.ts +302 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +80 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/voice.ts +479 -0
- package/src/commands/expression.ts +111 -23
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +35 -1
- package/src/commands/granular.ts +432 -0
- package/src/commands/help.ts +159 -5
- package/src/commands/keys.ts +629 -0
- package/src/commands/modal.ts +310 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +264 -0
- package/src/commands/sample.ts +52 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +201 -0
- package/src/commands/strum.ts +473 -0
- package/src/commands/time.ts +4 -1
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +339 -15
- package/src/project/check.ts +17 -0
- package/src/render.ts +49 -4
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +934 -9
- package/src/tui/modal-menu.ts +145 -0
- package/src/tui/performance-menu.ts +42 -0
- package/src/tui/play-chords.ts +104 -3
- package/src/tui/play-mode.ts +1 -0
- package/src/tui/play-session.ts +70 -2
- package/src/tui/wind-menu.ts +144 -0
package/core/keys.ts
ADDED
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modelled pianos (0.6 "Instruments", keys lane): the piano families, the one
|
|
3
|
+
* `KEYS_PARAMS` table that drives validation, printing, the menu, the
|
|
4
|
+
* `keys` prompt command, the `set_keys` agent tool and the `keys-<param>`
|
|
5
|
+
* automation lanes, and the named presets.
|
|
6
|
+
*
|
|
7
|
+
* A track plays the modelled piano only when its instrument is a piano
|
|
8
|
+
* family (`grand`, `upright`, `felt`, `honkytonk`, `prepared`) AND it carries
|
|
9
|
+
* `keys` (possibly `{}`). A stored `instrument: "piano"` stays the legacy
|
|
10
|
+
* tone forever; every new write of the word `piano` stores `grand` with
|
|
11
|
+
* `keys: {}` (see `pianoWrite`).
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
FxValidationError,
|
|
15
|
+
isRecord,
|
|
16
|
+
normalizeParams,
|
|
17
|
+
type NumberParam,
|
|
18
|
+
type ParamSpec,
|
|
19
|
+
} from "./params.ts";
|
|
20
|
+
|
|
21
|
+
/** The piano families this lane models (the `Track.instrument` values). */
|
|
22
|
+
export const PIANO_FAMILIES = Object.freeze([
|
|
23
|
+
"grand",
|
|
24
|
+
"upright",
|
|
25
|
+
"felt",
|
|
26
|
+
"honkytonk",
|
|
27
|
+
"prepared",
|
|
28
|
+
] as const);
|
|
29
|
+
|
|
30
|
+
export type PianoFamily = (typeof PIANO_FAMILIES)[number];
|
|
31
|
+
|
|
32
|
+
export function isPianoFamily(
|
|
33
|
+
instrument: string | undefined,
|
|
34
|
+
): instrument is PianoFamily {
|
|
35
|
+
return (PIANO_FAMILIES as readonly string[]).includes(instrument ?? "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Electric keys (0.6.1, keys-electric): the Rhodes-style tine piano, the
|
|
40
|
+
* Wurlitzer-style reed piano and the clavinet. They share `Track.keys` and
|
|
41
|
+
* the `KEYS_PARAMS` table with the pianos; `KEYS_PARAM_FAMILIES` says which
|
|
42
|
+
* rows each family reads.
|
|
43
|
+
*/
|
|
44
|
+
export const ELECTRIC_FAMILIES = Object.freeze([
|
|
45
|
+
"epiano",
|
|
46
|
+
"wurli",
|
|
47
|
+
"clav",
|
|
48
|
+
] as const);
|
|
49
|
+
|
|
50
|
+
export type ElectricFamily = (typeof ELECTRIC_FAMILIES)[number];
|
|
51
|
+
|
|
52
|
+
export function isElectricFamily(
|
|
53
|
+
instrument: string | undefined,
|
|
54
|
+
): instrument is ElectricFamily {
|
|
55
|
+
return (ELECTRIC_FAMILIES as readonly string[]).includes(instrument ?? "");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Why una corda would not sound on this track (0.6.1), or undefined when it
|
|
60
|
+
* does: only the modelled pianos with a `keys` object read `softPedal`.
|
|
61
|
+
*/
|
|
62
|
+
export function softPedalNote(
|
|
63
|
+
track: Readonly<{ id: string; instrument: string; keys?: unknown }>,
|
|
64
|
+
): string | undefined {
|
|
65
|
+
if (!isPianoFamily(track.instrument))
|
|
66
|
+
return `${track.id} is ${track.instrument}; una corda acts on the modelled pianos (${PIANO_FAMILIES.join(" ")})`;
|
|
67
|
+
if (track.keys === undefined)
|
|
68
|
+
return `${track.id} has no modelled piano yet; keys preset ${track.instrument} first`;
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The organ families (f061-organ): tonewheel, combo and pipe organs. */
|
|
73
|
+
export const ORGAN_FAMILIES = Object.freeze([
|
|
74
|
+
"tonewheel",
|
|
75
|
+
"combo",
|
|
76
|
+
"pipe",
|
|
77
|
+
] as const);
|
|
78
|
+
|
|
79
|
+
export type OrganFamily = (typeof ORGAN_FAMILIES)[number];
|
|
80
|
+
|
|
81
|
+
export function isOrganFamily(
|
|
82
|
+
instrument: string | undefined,
|
|
83
|
+
): instrument is OrganFamily {
|
|
84
|
+
return (ORGAN_FAMILIES as readonly string[]).includes(instrument ?? "");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Every keys family (the instruments that read `Track.keys`). */
|
|
88
|
+
export const KEYS_FAMILIES = Object.freeze([
|
|
89
|
+
...PIANO_FAMILIES,
|
|
90
|
+
...ELECTRIC_FAMILIES,
|
|
91
|
+
...ORGAN_FAMILIES,
|
|
92
|
+
] as const);
|
|
93
|
+
|
|
94
|
+
export type KeysFamily = (typeof KEYS_FAMILIES)[number];
|
|
95
|
+
|
|
96
|
+
export function isKeysFamily(
|
|
97
|
+
instrument: string | undefined,
|
|
98
|
+
): instrument is KeysFamily {
|
|
99
|
+
return (KEYS_FAMILIES as readonly string[]).includes(instrument ?? "");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Body EQ voicings (soundboard and case colour). */
|
|
103
|
+
export const PIANO_BODIES = Object.freeze([
|
|
104
|
+
"grand",
|
|
105
|
+
"upright",
|
|
106
|
+
"felt",
|
|
107
|
+
"honkytonk",
|
|
108
|
+
"prepared",
|
|
109
|
+
] as const);
|
|
110
|
+
|
|
111
|
+
export type PianoBody = (typeof PIANO_BODIES)[number];
|
|
112
|
+
|
|
113
|
+
const unit = (
|
|
114
|
+
value: number,
|
|
115
|
+
doc: string,
|
|
116
|
+
automate = false,
|
|
117
|
+
step = 0.05,
|
|
118
|
+
): NumberParam => ({
|
|
119
|
+
kind: "number",
|
|
120
|
+
min: 0,
|
|
121
|
+
max: 1,
|
|
122
|
+
default: value,
|
|
123
|
+
step,
|
|
124
|
+
...(automate ? { automate } : {}),
|
|
125
|
+
doc,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Every keys parameter, in storage and print order. Defaults are the grand's;
|
|
130
|
+
* a family's own defaults (`PIANO_FAMILY_DEFAULTS`) sit between these and
|
|
131
|
+
* the track's overrides. `automate` parameters have a `keys-<param>` lane,
|
|
132
|
+
* read at each note's onset.
|
|
133
|
+
*/
|
|
134
|
+
export const KEYS_PARAMS: Readonly<Record<string, ParamSpec>> = Object.freeze({
|
|
135
|
+
hardness: unit(
|
|
136
|
+
0.5,
|
|
137
|
+
"hammer felt hardness: brightness at a given velocity",
|
|
138
|
+
true,
|
|
139
|
+
),
|
|
140
|
+
touch: unit(1, "velocity sensitivity (0 plays every note at 0.8)", true),
|
|
141
|
+
inharm: {
|
|
142
|
+
kind: "number",
|
|
143
|
+
min: 0,
|
|
144
|
+
max: 4,
|
|
145
|
+
default: 1,
|
|
146
|
+
step: 0.1,
|
|
147
|
+
unit: "x",
|
|
148
|
+
doc: "inharmonicity multiplier (0 harmonic, 1 grand, 2.5 upright)",
|
|
149
|
+
},
|
|
150
|
+
unison: {
|
|
151
|
+
kind: "number",
|
|
152
|
+
min: 0,
|
|
153
|
+
max: 30,
|
|
154
|
+
default: 0.7,
|
|
155
|
+
step: 0.5,
|
|
156
|
+
unit: "c",
|
|
157
|
+
doc: "detune spread of the unison strings in cents",
|
|
158
|
+
},
|
|
159
|
+
decay: {
|
|
160
|
+
kind: "number",
|
|
161
|
+
min: 0.1,
|
|
162
|
+
max: 4,
|
|
163
|
+
default: 1,
|
|
164
|
+
step: 0.1,
|
|
165
|
+
unit: "x",
|
|
166
|
+
automate: true,
|
|
167
|
+
doc: "sustain time multiplier",
|
|
168
|
+
},
|
|
169
|
+
release: {
|
|
170
|
+
kind: "number",
|
|
171
|
+
min: 0.1,
|
|
172
|
+
max: 4,
|
|
173
|
+
default: 1,
|
|
174
|
+
step: 0.1,
|
|
175
|
+
unit: "x",
|
|
176
|
+
automate: true,
|
|
177
|
+
doc: "damper time multiplier (how fast a released key stops)",
|
|
178
|
+
},
|
|
179
|
+
strike: {
|
|
180
|
+
kind: "number",
|
|
181
|
+
min: 0.04,
|
|
182
|
+
max: 0.3,
|
|
183
|
+
default: 0.12,
|
|
184
|
+
step: 0.01,
|
|
185
|
+
doc: "hammer position along the string",
|
|
186
|
+
},
|
|
187
|
+
after: unit(0.3, "aftersound share (the slow second stage of the decay)"),
|
|
188
|
+
knock: unit(0.5, "soundboard knock and hammer thump", true),
|
|
189
|
+
noise: unit(0.25, "key-off and damper mechanics", true),
|
|
190
|
+
felt: unit(0, "felt strip between hammers and strings", true),
|
|
191
|
+
prep: unit(0, "share of keys carrying a preparation (seeded per key)"),
|
|
192
|
+
width: unit(0.6, "keyboard stereo spread, bass left and treble right"),
|
|
193
|
+
stretch: unit(
|
|
194
|
+
1,
|
|
195
|
+
"octave stretch from the strings' own inharmonicity; 0 keeps tuning exact",
|
|
196
|
+
),
|
|
197
|
+
body: {
|
|
198
|
+
kind: "enum",
|
|
199
|
+
values: PIANO_BODIES,
|
|
200
|
+
default: "grand",
|
|
201
|
+
optional: true,
|
|
202
|
+
doc: "body EQ voicing (default: the family's own)",
|
|
203
|
+
},
|
|
204
|
+
vib: {
|
|
205
|
+
kind: "number",
|
|
206
|
+
min: 0,
|
|
207
|
+
max: 64,
|
|
208
|
+
default: 0,
|
|
209
|
+
step: 0.25,
|
|
210
|
+
unit: "Hz",
|
|
211
|
+
doc: "pitch wobble rate (tape wow); 0 is off; note vibrato replaces it",
|
|
212
|
+
strudel: ["vib", "vibrato", "v"],
|
|
213
|
+
},
|
|
214
|
+
vibmod: {
|
|
215
|
+
kind: "number",
|
|
216
|
+
min: 0,
|
|
217
|
+
max: 24,
|
|
218
|
+
default: 0.5,
|
|
219
|
+
step: 0.05,
|
|
220
|
+
unit: "st",
|
|
221
|
+
doc: "pitch wobble depth in semitones",
|
|
222
|
+
strudel: ["vibmod", "vmod"],
|
|
223
|
+
},
|
|
224
|
+
// keys-electric (0.6.1): epiano, wurli and clav rows.
|
|
225
|
+
bark: unit(0.35, "pickup drive: growl when played hard (epiano, wurli)"),
|
|
226
|
+
bell: unit(0.5, "tine or reed bell ping (epiano, wurli)"),
|
|
227
|
+
tone: {
|
|
228
|
+
kind: "number",
|
|
229
|
+
min: 0,
|
|
230
|
+
max: 12000,
|
|
231
|
+
default: 0,
|
|
232
|
+
step: 100,
|
|
233
|
+
unit: "Hz",
|
|
234
|
+
automate: true,
|
|
235
|
+
doc: "output low-pass of the electric keys; 0 is off",
|
|
236
|
+
},
|
|
237
|
+
pickup: {
|
|
238
|
+
kind: "enum",
|
|
239
|
+
values: ["neck", "bridge", "both", "out"],
|
|
240
|
+
default: "both",
|
|
241
|
+
doc: "clav pickup switch; out is both pickups out of phase",
|
|
242
|
+
},
|
|
243
|
+
mute: unit(0, "clav mute slider: damps the upper partials"),
|
|
244
|
+
vibe: unit(
|
|
245
|
+
0,
|
|
246
|
+
"suitcase stereo vibrato depth: antiphase left/right pan (epiano)",
|
|
247
|
+
true,
|
|
248
|
+
),
|
|
249
|
+
vibehz: {
|
|
250
|
+
kind: "number",
|
|
251
|
+
min: 0.5,
|
|
252
|
+
max: 12,
|
|
253
|
+
default: 4,
|
|
254
|
+
step: 0.25,
|
|
255
|
+
unit: "Hz",
|
|
256
|
+
doc: "suitcase vibrato rate (epiano)",
|
|
257
|
+
},
|
|
258
|
+
trem: unit(
|
|
259
|
+
0,
|
|
260
|
+
"tremolo depth: reed piano at 5.6 Hz (wurli), pipe tremulant (pipe)",
|
|
261
|
+
true,
|
|
262
|
+
),
|
|
263
|
+
// keys-electric (0.6.1): piano sympathetic resonance (pianos only; 0 keeps
|
|
264
|
+
// 0.6.0 renders byte-identical).
|
|
265
|
+
sym: unit(
|
|
266
|
+
0,
|
|
267
|
+
"sympathetic string resonance while the sustain pedal is down; 0 is off",
|
|
268
|
+
),
|
|
269
|
+
// organ (f061-organ): tonewheel, combo and pipe rows (ORGAN_ROWS says
|
|
270
|
+
// which family reads each). The drawbars, registers and stops strings
|
|
271
|
+
// live in ORGAN_TEXT (ParamSpec has no text kind).
|
|
272
|
+
perc: {
|
|
273
|
+
kind: "enum",
|
|
274
|
+
values: ["off", "2nd", "3rd"],
|
|
275
|
+
default: "off",
|
|
276
|
+
doc: "tonewheel percussion harmonic, single-trigger; on mutes the 1' bar",
|
|
277
|
+
},
|
|
278
|
+
percdecay: {
|
|
279
|
+
kind: "enum",
|
|
280
|
+
values: ["fast", "slow"],
|
|
281
|
+
default: "fast",
|
|
282
|
+
doc: "tonewheel percussion decay (fast 0.6 s, slow 1.8 s)",
|
|
283
|
+
},
|
|
284
|
+
click: unit(0.5, "tonewheel key click (contact bounce)"),
|
|
285
|
+
scanner: {
|
|
286
|
+
kind: "enum",
|
|
287
|
+
values: ["off", "v1", "v2", "v3", "c1", "c2", "c3"],
|
|
288
|
+
default: "c3",
|
|
289
|
+
doc: "tonewheel scanner vibrato (v1-v3) or chorus (c1-c3)",
|
|
290
|
+
},
|
|
291
|
+
drive: unit(0.15, "organ preamp overdrive", true),
|
|
292
|
+
rotary: {
|
|
293
|
+
kind: "enum",
|
|
294
|
+
values: ["slow", "fast", "stop"],
|
|
295
|
+
default: "slow",
|
|
296
|
+
doc: "rotary speaker at the start (lane keys-rotary: 0 stop, 1 slow, 2 fast)",
|
|
297
|
+
},
|
|
298
|
+
voice: {
|
|
299
|
+
kind: "enum",
|
|
300
|
+
values: ["flute", "reed", "bright"],
|
|
301
|
+
default: "reed",
|
|
302
|
+
doc: "combo register timbre: filtered square, reed pulse, bright saw",
|
|
303
|
+
},
|
|
304
|
+
chiff: unit(0.4, "pipe speech: the noise at each flue pipe's attack"),
|
|
305
|
+
wind: unit(0.3, "pipe wind instability (slow pitch and level drift)"),
|
|
306
|
+
percvol: {
|
|
307
|
+
kind: "enum",
|
|
308
|
+
values: ["normal", "soft"],
|
|
309
|
+
default: "normal",
|
|
310
|
+
doc: "tonewheel percussion volume: soft is about 6 dB down and keeps the drawbars at full level",
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The families each keys parameter applies to (menus and listings show a
|
|
316
|
+
* track only its family's rows). A parameter missing here is a piano row.
|
|
317
|
+
*/
|
|
318
|
+
export const KEYS_PARAM_FAMILIES: Readonly<
|
|
319
|
+
Record<string, readonly KeysFamily[]>
|
|
320
|
+
> = Object.freeze({
|
|
321
|
+
hardness: KEYS_FAMILIES,
|
|
322
|
+
touch: KEYS_FAMILIES,
|
|
323
|
+
decay: KEYS_FAMILIES,
|
|
324
|
+
release: KEYS_FAMILIES,
|
|
325
|
+
width: KEYS_FAMILIES,
|
|
326
|
+
vib: KEYS_FAMILIES,
|
|
327
|
+
vibmod: KEYS_FAMILIES,
|
|
328
|
+
bark: ["epiano", "wurli"],
|
|
329
|
+
bell: ["epiano", "wurli"],
|
|
330
|
+
tone: ELECTRIC_FAMILIES,
|
|
331
|
+
pickup: ["clav"],
|
|
332
|
+
mute: ["clav"],
|
|
333
|
+
vibe: ["epiano"],
|
|
334
|
+
vibehz: ["epiano"],
|
|
335
|
+
trem: ["wurli", "pipe"],
|
|
336
|
+
// organ (f061-organ): the organ rows; organs list theirs in ORGAN_ROWS.
|
|
337
|
+
perc: ["tonewheel"],
|
|
338
|
+
percdecay: ["tonewheel"],
|
|
339
|
+
percvol: ["tonewheel"],
|
|
340
|
+
click: ["tonewheel"],
|
|
341
|
+
scanner: ["tonewheel"],
|
|
342
|
+
drive: ["tonewheel", "combo"],
|
|
343
|
+
rotary: ["tonewheel", "combo"],
|
|
344
|
+
voice: ["combo"],
|
|
345
|
+
chiff: ["pipe"],
|
|
346
|
+
wind: ["pipe"],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
/** The keys parameters a family reads, in `KEYS_PARAMS` order. */
|
|
350
|
+
export function keysParamsFor(instrument: string | undefined): string[] {
|
|
351
|
+
if (isOrganFamily(instrument)) return [...ORGAN_ROWS[instrument]];
|
|
352
|
+
const family = isKeysFamily(instrument) ? instrument : "grand";
|
|
353
|
+
return Object.keys(KEYS_PARAMS).filter((name) => {
|
|
354
|
+
const families = KEYS_PARAM_FAMILIES[name];
|
|
355
|
+
return families ? families.includes(family) : isPianoFamily(family);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Organ text rows (f061-organ), stored as strings after the KEYS_PARAMS
|
|
361
|
+
* values: `drawbars` nine digits 0-8 (16' 5⅓' 8' 4' 2⅔' 2' 1⅗' 1⅓' 1'),
|
|
362
|
+
* `registers` five digits 0-8 (combo 16' 8' 4' 2⅔' 2'), `stops` pipe stop
|
|
363
|
+
* names and registrations separated by spaces.
|
|
364
|
+
*/
|
|
365
|
+
export const ORGAN_TEXT = Object.freeze({
|
|
366
|
+
drawbars: Object.freeze({
|
|
367
|
+
digits: 9,
|
|
368
|
+
default: "888000000",
|
|
369
|
+
doc: "tonewheel drawbars, nine digits 0-8: 16' 5⅓' 8' 4' 2⅔' 2' 1⅗' 1⅓' 1'",
|
|
370
|
+
}),
|
|
371
|
+
registers: Object.freeze({
|
|
372
|
+
digits: 5,
|
|
373
|
+
default: "08800",
|
|
374
|
+
doc: "combo registers, five digits 0-8: 16' 8' 4' 2⅔' 2'",
|
|
375
|
+
}),
|
|
376
|
+
stops: Object.freeze({
|
|
377
|
+
digits: 0,
|
|
378
|
+
default: "principal8 octave4 fifteenth2 mixture",
|
|
379
|
+
doc: "pipe stops (or a registration: plenum flutes cornet reeds strings full)",
|
|
380
|
+
}),
|
|
381
|
+
} as const);
|
|
382
|
+
|
|
383
|
+
export type OrganTextParam = keyof typeof ORGAN_TEXT;
|
|
384
|
+
|
|
385
|
+
export function isOrganText(name: string): name is OrganTextParam {
|
|
386
|
+
return Object.prototype.hasOwnProperty.call(ORGAN_TEXT, name);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Drawbar footages in drawbar order (labels for the menu and docs). */
|
|
390
|
+
export const DRAWBAR_FEET = Object.freeze([
|
|
391
|
+
"16'",
|
|
392
|
+
"5⅓'",
|
|
393
|
+
"8'",
|
|
394
|
+
"4'",
|
|
395
|
+
"2⅔'",
|
|
396
|
+
"2'",
|
|
397
|
+
"1⅗'",
|
|
398
|
+
"1⅓'",
|
|
399
|
+
"1'",
|
|
400
|
+
]);
|
|
401
|
+
|
|
402
|
+
/** Combo register footages in register order. */
|
|
403
|
+
export const REGISTER_FEET = Object.freeze(["16'", "8'", "4'", "2⅔'", "2'"]);
|
|
404
|
+
|
|
405
|
+
/** Pipe stops, low to high pitch with the reeds last. */
|
|
406
|
+
export const PIPE_STOPS = Object.freeze([
|
|
407
|
+
"subbass16",
|
|
408
|
+
"bourdon16",
|
|
409
|
+
"principal8",
|
|
410
|
+
"flute8",
|
|
411
|
+
"gedackt8",
|
|
412
|
+
"gamba8",
|
|
413
|
+
"celeste8",
|
|
414
|
+
"octave4",
|
|
415
|
+
"flute4",
|
|
416
|
+
"nazard",
|
|
417
|
+
"fifteenth2",
|
|
418
|
+
"piccolo2",
|
|
419
|
+
"tierce",
|
|
420
|
+
"larigot",
|
|
421
|
+
"mixture",
|
|
422
|
+
"trombone16",
|
|
423
|
+
"trumpet8",
|
|
424
|
+
"oboe8",
|
|
425
|
+
"krummhorn8",
|
|
426
|
+
] as const);
|
|
427
|
+
|
|
428
|
+
/** Named registrations: a word that stands for a set of stops. */
|
|
429
|
+
export const PIPE_REGISTRATIONS: Readonly<Record<string, readonly string[]>> =
|
|
430
|
+
Object.freeze({
|
|
431
|
+
plenum: ["subbass16", "principal8", "octave4", "fifteenth2", "mixture"],
|
|
432
|
+
flutes: ["gedackt8", "flute4"],
|
|
433
|
+
cornet: ["flute8", "flute4", "nazard", "piccolo2", "tierce"],
|
|
434
|
+
reeds: ["trumpet8", "oboe8", "nazard", "tierce", "larigot"],
|
|
435
|
+
strings: ["gamba8", "celeste8"],
|
|
436
|
+
full: PIPE_STOPS.filter((stop) => stop !== "celeste8"),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
/** The stop names a `stops` value sounds (registrations expanded, deduped). */
|
|
440
|
+
export function pipeStops(text: string): string[] {
|
|
441
|
+
const out: string[] = [];
|
|
442
|
+
for (const word of text.trim().split(/\s+/)) {
|
|
443
|
+
const names = PIPE_REGISTRATIONS[word] ?? [word];
|
|
444
|
+
for (const name of names) if (!out.includes(name)) out.push(name);
|
|
445
|
+
}
|
|
446
|
+
return out.filter((name) => (PIPE_STOPS as readonly string[]).includes(name));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Validates one organ text value; returns the stored string. */
|
|
450
|
+
export function normalizeOrganText(
|
|
451
|
+
name: OrganTextParam,
|
|
452
|
+
value: unknown,
|
|
453
|
+
): string {
|
|
454
|
+
if (typeof value !== "string")
|
|
455
|
+
throw new FxValidationError(`keys ${name} must be a string`);
|
|
456
|
+
const spec = ORGAN_TEXT[name];
|
|
457
|
+
if (spec.digits > 0) {
|
|
458
|
+
if (!new RegExp(`^[0-8]{${spec.digits}}$`).test(value))
|
|
459
|
+
throw new FxValidationError(
|
|
460
|
+
`keys ${name} must be ${spec.digits} digits 0-8 (like ${spec.default})`,
|
|
461
|
+
);
|
|
462
|
+
return value;
|
|
463
|
+
}
|
|
464
|
+
const words = value.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
465
|
+
if (words.length === 0)
|
|
466
|
+
throw new FxValidationError("keys stops must name at least one stop");
|
|
467
|
+
for (const word of words)
|
|
468
|
+
if (
|
|
469
|
+
!(PIPE_STOPS as readonly string[]).includes(word) &&
|
|
470
|
+
!Object.prototype.hasOwnProperty.call(PIPE_REGISTRATIONS, word)
|
|
471
|
+
)
|
|
472
|
+
throw new FxValidationError(
|
|
473
|
+
`keys stops: unknown stop "${word}" (stops ${PIPE_STOPS.join(" ")}; registrations ${Object.keys(PIPE_REGISTRATIONS).join(" ")})`,
|
|
474
|
+
);
|
|
475
|
+
return words.join(" ");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** The KEYS_PARAMS rows and text rows each organ family reads. */
|
|
479
|
+
export const ORGAN_ROWS: Readonly<Record<OrganFamily, readonly string[]>> =
|
|
480
|
+
Object.freeze({
|
|
481
|
+
tonewheel: [
|
|
482
|
+
"drawbars",
|
|
483
|
+
"perc",
|
|
484
|
+
"percdecay",
|
|
485
|
+
"percvol",
|
|
486
|
+
"click",
|
|
487
|
+
"scanner",
|
|
488
|
+
"drive",
|
|
489
|
+
"rotary",
|
|
490
|
+
],
|
|
491
|
+
combo: ["registers", "voice", "vib", "vibmod", "drive", "rotary"],
|
|
492
|
+
pipe: ["stops", "chiff", "wind", "trem"],
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
/** Every organ-only row name (KEYS_PARAMS rows and text rows). */
|
|
496
|
+
export const ORGAN_PARAM_NAMES: readonly string[] = Object.freeze([
|
|
497
|
+
"perc",
|
|
498
|
+
"percdecay",
|
|
499
|
+
"click",
|
|
500
|
+
"scanner",
|
|
501
|
+
"drive",
|
|
502
|
+
"rotary",
|
|
503
|
+
"voice",
|
|
504
|
+
"chiff",
|
|
505
|
+
"wind",
|
|
506
|
+
"trem",
|
|
507
|
+
"percvol",
|
|
508
|
+
...Object.keys(ORGAN_TEXT),
|
|
509
|
+
]);
|
|
510
|
+
|
|
511
|
+
/** Each organ family's own defaults over KEYS_PARAMS. */
|
|
512
|
+
export const ORGAN_FAMILY_DEFAULTS: Readonly<
|
|
513
|
+
Record<OrganFamily, Readonly<Record<string, number | string>>>
|
|
514
|
+
> = Object.freeze({
|
|
515
|
+
tonewheel: Object.freeze({ drawbars: ORGAN_TEXT.drawbars.default }),
|
|
516
|
+
combo: Object.freeze({
|
|
517
|
+
registers: ORGAN_TEXT.registers.default,
|
|
518
|
+
vib: 6,
|
|
519
|
+
vibmod: 0.12,
|
|
520
|
+
drive: 0,
|
|
521
|
+
rotary: "stop",
|
|
522
|
+
}),
|
|
523
|
+
pipe: Object.freeze({ stops: ORGAN_TEXT.stops.default }),
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* The rows a keys family shows and reads, in display order (an organ's
|
|
528
|
+
* ORGAN_ROWS, else its KEYS_PARAM_FAMILIES rows).
|
|
529
|
+
*/
|
|
530
|
+
export function keysRowsFor(instrument: string | undefined): string[] {
|
|
531
|
+
return keysParamsFor(instrument);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** The parameters shown first in the menu and `keys` listing. */
|
|
535
|
+
export const KEYS_SIMPLE = Object.freeze([
|
|
536
|
+
"hardness",
|
|
537
|
+
"decay",
|
|
538
|
+
"release",
|
|
539
|
+
"felt",
|
|
540
|
+
]);
|
|
541
|
+
|
|
542
|
+
/** The electric families' first rows. */
|
|
543
|
+
export const ELECTRIC_SIMPLE: Readonly<
|
|
544
|
+
Record<ElectricFamily, readonly string[]>
|
|
545
|
+
> = Object.freeze({
|
|
546
|
+
epiano: Object.freeze(["bark", "bell", "tone", "vibe"]),
|
|
547
|
+
wurli: Object.freeze(["bark", "bell", "tone", "trem"]),
|
|
548
|
+
clav: Object.freeze(["pickup", "mute", "tone", "decay"]),
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
/** The first rows for a keys family. */
|
|
552
|
+
export function keysSimpleFor(
|
|
553
|
+
instrument: string | undefined,
|
|
554
|
+
): readonly string[] {
|
|
555
|
+
if (isOrganFamily(instrument)) return ORGAN_ROWS[instrument];
|
|
556
|
+
return isElectricFamily(instrument)
|
|
557
|
+
? ELECTRIC_SIMPLE[instrument]
|
|
558
|
+
: KEYS_SIMPLE;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export type TrackKeys = Readonly<
|
|
562
|
+
Record<string, number | string> & { preset?: string }
|
|
563
|
+
>;
|
|
564
|
+
|
|
565
|
+
/** Each family's own defaults over the grand's (`KEYS_PARAMS`). */
|
|
566
|
+
export const PIANO_FAMILY_DEFAULTS: Readonly<
|
|
567
|
+
Record<PianoFamily, Readonly<Record<string, number | string>>>
|
|
568
|
+
> = Object.freeze({
|
|
569
|
+
grand: Object.freeze({ body: "grand" }),
|
|
570
|
+
upright: Object.freeze({ inharm: 2.5, decay: 0.6, body: "upright" }),
|
|
571
|
+
felt: Object.freeze({
|
|
572
|
+
felt: 1,
|
|
573
|
+
hardness: 0.3,
|
|
574
|
+
noise: 0.6,
|
|
575
|
+
body: "felt",
|
|
576
|
+
}),
|
|
577
|
+
honkytonk: Object.freeze({ unison: 16, hardness: 0.7, body: "honkytonk" }),
|
|
578
|
+
prepared: Object.freeze({ prep: 0.6, body: "prepared" }),
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
/** Each electric family's own defaults over `KEYS_PARAMS`. */
|
|
582
|
+
export const ELECTRIC_FAMILY_DEFAULTS: Readonly<
|
|
583
|
+
Record<ElectricFamily, Readonly<Record<string, number | string>>>
|
|
584
|
+
> = Object.freeze({
|
|
585
|
+
epiano: Object.freeze({ width: 0.3, tone: 6000 }),
|
|
586
|
+
wurli: Object.freeze({ width: 0.3, bark: 0.5, bell: 0.2, tone: 5000 }),
|
|
587
|
+
clav: Object.freeze({ width: 0.3 }),
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
/** Effects a preset sets with the voice (applied over the effect defaults). */
|
|
591
|
+
export type KeysPresetFx = Readonly<{
|
|
592
|
+
/** The track filter (`Track.filter`). */
|
|
593
|
+
filter?: Readonly<{ cutoff: number; resonance: number; type?: "lpf" }>;
|
|
594
|
+
/** Chain effects (`Track.fx`), merged over the track's own. */
|
|
595
|
+
fx?: Readonly<Record<string, Readonly<Record<string, number | string>>>>;
|
|
596
|
+
reverb?: Readonly<{ mix: number; size: number }>;
|
|
597
|
+
}>;
|
|
598
|
+
|
|
599
|
+
export type KeysPreset = Readonly<{
|
|
600
|
+
instrument: KeysFamily;
|
|
601
|
+
/** The preset's overrides over the family defaults. */
|
|
602
|
+
keys: Readonly<Record<string, number | string>>;
|
|
603
|
+
doc: string;
|
|
604
|
+
/** Styles it suits (shown in /help and the agent prompt). */
|
|
605
|
+
styles: string;
|
|
606
|
+
}> &
|
|
607
|
+
KeysPresetFx;
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Named starting points. Loading one stores `keys: { preset }` (replacing
|
|
611
|
+
* any overrides, the synth rule); its overrides are read at render, so a
|
|
612
|
+
* later `keys hardness 0.4` tweaks the preset.
|
|
613
|
+
*/
|
|
614
|
+
export const KEYS_PRESETS: Readonly<Record<string, KeysPreset>> = Object.freeze(
|
|
615
|
+
{
|
|
616
|
+
grand: {
|
|
617
|
+
instrument: "grand",
|
|
618
|
+
keys: {},
|
|
619
|
+
doc: "concert grand, bright and long, three-string unisons",
|
|
620
|
+
styles: "Chopin, Beethoven, Brahms, Debussy, jazz, Glass, hyperpop",
|
|
621
|
+
},
|
|
622
|
+
ballad: {
|
|
623
|
+
instrument: "grand",
|
|
624
|
+
keys: { hardness: 0.3, after: 0.5, decay: 1.3 },
|
|
625
|
+
reverb: { mix: 0.25, size: 0.7 },
|
|
626
|
+
doc: "darker grand, soft hammer, more aftersound, in a room",
|
|
627
|
+
styles: "dark ballads, Mitski, Sakamoto, Brahms intermezzi",
|
|
628
|
+
},
|
|
629
|
+
upright: {
|
|
630
|
+
instrument: "upright",
|
|
631
|
+
keys: {},
|
|
632
|
+
doc: "boxy, more inharmonic, shorter",
|
|
633
|
+
styles: "soul, Motown, indie, Beach House",
|
|
634
|
+
},
|
|
635
|
+
felt: {
|
|
636
|
+
instrument: "felt",
|
|
637
|
+
keys: {},
|
|
638
|
+
reverb: { mix: 0.2, size: 0.5 },
|
|
639
|
+
doc: "felt strip down, muted and intimate, audible mechanics",
|
|
640
|
+
styles: "Sakamoto, Eno, Nils Frahm, The Field, Four Tet",
|
|
641
|
+
},
|
|
642
|
+
lofi: {
|
|
643
|
+
instrument: "felt",
|
|
644
|
+
keys: { vib: 0.3, vibmod: 0.05 },
|
|
645
|
+
filter: { cutoff: 3500, resonance: 0.1 },
|
|
646
|
+
fx: { crush: { bits: 10 } },
|
|
647
|
+
doc: "felt piano with tape wow, a dark filter and a little crush",
|
|
648
|
+
styles: "lo-fi hip hop, bedroom pop, chillhop",
|
|
649
|
+
},
|
|
650
|
+
honkytonk: {
|
|
651
|
+
instrument: "honkytonk",
|
|
652
|
+
keys: {},
|
|
653
|
+
doc: "16-cent unisons, bright saloon upright",
|
|
654
|
+
styles: "ragtime, saloon, A. G. Cook",
|
|
655
|
+
},
|
|
656
|
+
prepared: {
|
|
657
|
+
instrument: "prepared",
|
|
658
|
+
keys: {},
|
|
659
|
+
doc: "bolts, rubber and screws on 60% of keys (seeded per key)",
|
|
660
|
+
styles: "Cage, Aphex-style, Oneohtrix Point Never",
|
|
661
|
+
},
|
|
662
|
+
// keys-electric (0.6.1)
|
|
663
|
+
epiano: {
|
|
664
|
+
instrument: "epiano",
|
|
665
|
+
keys: {},
|
|
666
|
+
doc: "Rhodes-style stage tine piano: bell and bark with velocity",
|
|
667
|
+
styles: "jazz, soul, Flying Lotus, Radiohead",
|
|
668
|
+
},
|
|
669
|
+
suitcase: {
|
|
670
|
+
instrument: "epiano",
|
|
671
|
+
keys: { vibe: 0.7, vibehz: 4, bark: 0.3 },
|
|
672
|
+
doc: "tine piano with the suitcase stereo vibrato, left and right in turn",
|
|
673
|
+
styles: "70s soul, Stevie Wonder ballads, neo-soul",
|
|
674
|
+
},
|
|
675
|
+
dyno: {
|
|
676
|
+
instrument: "epiano",
|
|
677
|
+
keys: { bell: 0.85, tone: 9000, hardness: 0.7 },
|
|
678
|
+
doc: "bright, bell-heavy tine piano",
|
|
679
|
+
styles: "80s ballads, city pop",
|
|
680
|
+
},
|
|
681
|
+
wurli: {
|
|
682
|
+
instrument: "wurli",
|
|
683
|
+
keys: {},
|
|
684
|
+
doc: "Wurlitzer-style reed piano, nasal and growly",
|
|
685
|
+
styles: "soul, funk, Radiohead, Supertramp",
|
|
686
|
+
},
|
|
687
|
+
clav: {
|
|
688
|
+
instrument: "clav",
|
|
689
|
+
keys: {},
|
|
690
|
+
doc: "clavinet, both pickups",
|
|
691
|
+
styles: "funk, disco, reggae",
|
|
692
|
+
},
|
|
693
|
+
funkclav: {
|
|
694
|
+
instrument: "clav",
|
|
695
|
+
keys: { pickup: "out", mute: 0.3 },
|
|
696
|
+
doc: "clavinet with both pickups out of phase and the mute slider up",
|
|
697
|
+
styles: "Stevie Wonder funk, Bill Withers",
|
|
698
|
+
},
|
|
699
|
+
// organ (f061-organ)
|
|
700
|
+
tonewheel: {
|
|
701
|
+
instrument: "tonewheel",
|
|
702
|
+
keys: {},
|
|
703
|
+
doc: "tonewheel organ 888000000, scanner C3, slow rotary",
|
|
704
|
+
styles: "jazz, soul, gospel comping, Beach House",
|
|
705
|
+
},
|
|
706
|
+
gospel: {
|
|
707
|
+
instrument: "tonewheel",
|
|
708
|
+
keys: {
|
|
709
|
+
drawbars: "888800008",
|
|
710
|
+
perc: "3rd",
|
|
711
|
+
rotary: "fast",
|
|
712
|
+
drive: 0.35,
|
|
713
|
+
},
|
|
714
|
+
doc: "888800008, 3rd percussion (mutes the 1'), fast rotary, driven",
|
|
715
|
+
styles: "gospel, Motown, rock",
|
|
716
|
+
},
|
|
717
|
+
jazzorgan: {
|
|
718
|
+
instrument: "tonewheel",
|
|
719
|
+
keys: {
|
|
720
|
+
drawbars: "888000000",
|
|
721
|
+
perc: "3rd",
|
|
722
|
+
percvol: "soft",
|
|
723
|
+
scanner: "c3",
|
|
724
|
+
},
|
|
725
|
+
doc: "888000000 with soft fast 3rd percussion, slow rotary",
|
|
726
|
+
styles: "Jimmy Smith jazz, soul jazz",
|
|
727
|
+
},
|
|
728
|
+
combo: {
|
|
729
|
+
instrument: "combo",
|
|
730
|
+
keys: {},
|
|
731
|
+
doc: "Farfisa-style reed registers 08800 with vibrato",
|
|
732
|
+
styles: "Beach House, 60s garage, Glass, Reich",
|
|
733
|
+
},
|
|
734
|
+
vox: {
|
|
735
|
+
instrument: "combo",
|
|
736
|
+
keys: { registers: "08880", voice: "bright", vib: 5.5, vibmod: 0.1 },
|
|
737
|
+
doc: "Vox-style bright saw registers 08880",
|
|
738
|
+
styles: "60s, Doors-ish, shoegaze drones",
|
|
739
|
+
},
|
|
740
|
+
pipe: {
|
|
741
|
+
instrument: "pipe",
|
|
742
|
+
keys: { stops: "plenum" },
|
|
743
|
+
reverb: { mix: 0.35, size: 0.9 },
|
|
744
|
+
doc: "plenum: principal chorus, mixture and 16' in a church",
|
|
745
|
+
styles: "Bach fugue, Bach cantata",
|
|
746
|
+
},
|
|
747
|
+
flutes: {
|
|
748
|
+
instrument: "pipe",
|
|
749
|
+
keys: { stops: "flutes", trem: 0.4, chiff: 0.5 },
|
|
750
|
+
reverb: { mix: 0.3, size: 0.8 },
|
|
751
|
+
doc: "gedackt 8' and flute 4' with tremulant",
|
|
752
|
+
styles: "Bach chorale preludes, Eno, ambient",
|
|
753
|
+
},
|
|
754
|
+
cornet: {
|
|
755
|
+
instrument: "pipe",
|
|
756
|
+
keys: { stops: "cornet" },
|
|
757
|
+
reverb: { mix: 0.3, size: 0.8 },
|
|
758
|
+
doc: "the cornet solo combination (8' 4' 2⅔' 2' 1⅗')",
|
|
759
|
+
styles: "Bach, Buxtehude, French classical",
|
|
760
|
+
},
|
|
761
|
+
reeds: {
|
|
762
|
+
instrument: "pipe",
|
|
763
|
+
keys: { stops: "reeds" },
|
|
764
|
+
reverb: { mix: 0.3, size: 0.85 },
|
|
765
|
+
doc: "trumpet and oboe with nazard, tierce and larigot",
|
|
766
|
+
styles: "Messiaen",
|
|
767
|
+
},
|
|
768
|
+
celeste: {
|
|
769
|
+
instrument: "pipe",
|
|
770
|
+
keys: { stops: "strings", wind: 0.4 },
|
|
771
|
+
reverb: { mix: 0.35, size: 0.9 },
|
|
772
|
+
doc: "gamba and celeste: slow beating strings",
|
|
773
|
+
styles: "Messiaen, ambient, La Monte Young drones",
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
/** Organ words that name a keys preset (aliases of the presets). */
|
|
779
|
+
export const ORGAN_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
|
780
|
+
hammond: "tonewheel",
|
|
781
|
+
b3: "tonewheel",
|
|
782
|
+
farfisa: "combo",
|
|
783
|
+
church: "pipe",
|
|
784
|
+
pipeorgan: "pipe",
|
|
785
|
+
churchorgan: "pipe",
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
export function isKeysPreset(name: string): boolean {
|
|
789
|
+
return Object.prototype.hasOwnProperty.call(KEYS_PRESETS, name);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function isKeysParam(name: string): boolean {
|
|
793
|
+
return Object.prototype.hasOwnProperty.call(KEYS_PARAMS, name);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** Spec name for a parameter typed as its name or a Strudel alias. */
|
|
797
|
+
export function keysParamName(name: string): string | undefined {
|
|
798
|
+
const lower = name.toLowerCase();
|
|
799
|
+
if (isKeysParam(lower)) return lower;
|
|
800
|
+
if (isOrganText(lower)) return lower;
|
|
801
|
+
if (lower === "reg" || lower === "registration" || lower === "stop")
|
|
802
|
+
return "stops";
|
|
803
|
+
if (lower === "drawbar") return "drawbars";
|
|
804
|
+
if (lower === "aftersound") return "after";
|
|
805
|
+
if (lower === "prepared") return "prep";
|
|
806
|
+
for (const [key, spec] of Object.entries(KEYS_PARAMS))
|
|
807
|
+
if (spec.strudel?.some((alias) => alias.toLowerCase() === lower))
|
|
808
|
+
return key;
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Keys parameters with a `keys-<param>` lane (read at note onsets). */
|
|
813
|
+
export const KEYS_LANE_PARAMS: readonly Readonly<{
|
|
814
|
+
param: string;
|
|
815
|
+
spec: NumberParam;
|
|
816
|
+
}>[] = Object.freeze(
|
|
817
|
+
Object.entries(KEYS_PARAMS)
|
|
818
|
+
.filter(
|
|
819
|
+
(entry): entry is [string, NumberParam] =>
|
|
820
|
+
entry[1].kind === "number" && entry[1].automate === true,
|
|
821
|
+
)
|
|
822
|
+
.map(([param, spec]) => Object.freeze({ param, spec }))
|
|
823
|
+
.concat(
|
|
824
|
+
// f061-organ: the rotary speed as a lane (0 stop, 1 slow, 2 fast);
|
|
825
|
+
// the rotors glide between speeds with their own inertia.
|
|
826
|
+
Object.freeze({
|
|
827
|
+
param: "rotary",
|
|
828
|
+
spec: Object.freeze({
|
|
829
|
+
kind: "number",
|
|
830
|
+
min: 0,
|
|
831
|
+
max: 2,
|
|
832
|
+
default: 1,
|
|
833
|
+
step: 1,
|
|
834
|
+
integer: true,
|
|
835
|
+
doc: "rotary speed: 0 stop, 1 slow, 2 fast",
|
|
836
|
+
}) as NumberParam,
|
|
837
|
+
}),
|
|
838
|
+
),
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
/** The `rotary` enum as its lane number (0 stop, 1 slow, 2 fast). */
|
|
842
|
+
export function rotarySpeed(value: unknown): number {
|
|
843
|
+
return value === "fast" ? 2 : value === "stop" ? 0 : 1;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Validates `Track.keys`: overrides in `KEYS_PARAMS` order, then `preset`.
|
|
848
|
+
* `{}` stays `{}` (its presence turns the modelled piano on); null and
|
|
849
|
+
* undefined are absent.
|
|
850
|
+
*/
|
|
851
|
+
export function normalizeKeys(input: unknown): TrackKeys | undefined {
|
|
852
|
+
if (input === undefined || input === null) return undefined;
|
|
853
|
+
if (!isRecord(input))
|
|
854
|
+
throw new FxValidationError("track keys must be an object or null");
|
|
855
|
+
const params: Record<string, unknown> = {};
|
|
856
|
+
const text: Partial<Record<OrganTextParam, string>> = {};
|
|
857
|
+
let preset: string | undefined;
|
|
858
|
+
for (const [key, value] of Object.entries(input)) {
|
|
859
|
+
if (value === undefined || value === null) continue;
|
|
860
|
+
if (key === "preset") {
|
|
861
|
+
if (typeof value !== "string" || !isKeysPreset(value))
|
|
862
|
+
throw new FxValidationError(
|
|
863
|
+
`keys preset must be one of ${Object.keys(KEYS_PRESETS).join(", ")}`,
|
|
864
|
+
);
|
|
865
|
+
preset = value;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (isOrganText(key)) {
|
|
869
|
+
text[key] = normalizeOrganText(key, value);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (!isKeysParam(key)) {
|
|
873
|
+
const name = keysParamName(key);
|
|
874
|
+
throw new FxValidationError(
|
|
875
|
+
`keys has no parameter "${key}"${name ? ` (did you mean "${name}"?)` : ""}`,
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
params[key] = value;
|
|
879
|
+
}
|
|
880
|
+
const values = normalizeParams(KEYS_PARAMS, params, "keys", false);
|
|
881
|
+
return Object.freeze({
|
|
882
|
+
...(values as Record<string, number | string>),
|
|
883
|
+
...Object.fromEntries(
|
|
884
|
+
Object.keys(ORGAN_TEXT)
|
|
885
|
+
.filter((key) => text[key as OrganTextParam] !== undefined)
|
|
886
|
+
.map((key) => [key, text[key as OrganTextParam]!]),
|
|
887
|
+
),
|
|
888
|
+
...(preset ? { preset } : {}),
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Every keys parameter for a track: the grand defaults, then the family's,
|
|
894
|
+
* then the stored preset's, then the stored overrides.
|
|
895
|
+
*/
|
|
896
|
+
export function resolvedKeys(
|
|
897
|
+
instrument: string,
|
|
898
|
+
keys: TrackKeys | undefined,
|
|
899
|
+
): Readonly<Record<string, number | string>> {
|
|
900
|
+
const out: Record<string, number | string> = {};
|
|
901
|
+
for (const [key, spec] of Object.entries(KEYS_PARAMS))
|
|
902
|
+
out[key] = spec.default as number | string;
|
|
903
|
+
if (isPianoFamily(instrument))
|
|
904
|
+
Object.assign(out, PIANO_FAMILY_DEFAULTS[instrument]);
|
|
905
|
+
if (isElectricFamily(instrument))
|
|
906
|
+
Object.assign(out, ELECTRIC_FAMILY_DEFAULTS[instrument]);
|
|
907
|
+
if (isOrganFamily(instrument))
|
|
908
|
+
Object.assign(out, ORGAN_FAMILY_DEFAULTS[instrument]);
|
|
909
|
+
const preset = keys?.preset ? KEYS_PRESETS[keys.preset] : undefined;
|
|
910
|
+
if (preset) Object.assign(out, preset.keys);
|
|
911
|
+
for (const [key, value] of Object.entries(keys ?? {}))
|
|
912
|
+
if (key !== "preset") out[key] = value;
|
|
913
|
+
return out;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** Other names for the electric keys presets (keys-electric, 0.6.1). */
|
|
917
|
+
const KEYS_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
|
918
|
+
rhodes: "epiano",
|
|
919
|
+
wurlitzer: "wurli",
|
|
920
|
+
clavinet: "clav",
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* What a new write of a keys word stores: `piano` and `grand` become the
|
|
925
|
+
* modelled grand, a preset name its instrument and overrides. Undefined
|
|
926
|
+
* for any other word.
|
|
927
|
+
*/
|
|
928
|
+
export function pianoWrite(word: string):
|
|
929
|
+
| (Readonly<{
|
|
930
|
+
instrument: KeysFamily;
|
|
931
|
+
keys: TrackKeys;
|
|
932
|
+
preset?: string;
|
|
933
|
+
}> &
|
|
934
|
+
KeysPresetFx)
|
|
935
|
+
| undefined {
|
|
936
|
+
const lower = word.toLowerCase();
|
|
937
|
+
const name =
|
|
938
|
+
lower === "piano" || lower === "uprightpiano"
|
|
939
|
+
? lower === "piano"
|
|
940
|
+
? "grand"
|
|
941
|
+
: "upright"
|
|
942
|
+
: lower === "feltpiano"
|
|
943
|
+
? "felt"
|
|
944
|
+
: (KEYS_ALIASES[lower] ?? ORGAN_ALIASES[lower] ?? lower);
|
|
945
|
+
if (!isKeysPreset(name)) return undefined;
|
|
946
|
+
const preset = KEYS_PRESETS[name]!;
|
|
947
|
+
return Object.freeze({
|
|
948
|
+
instrument: preset.instrument,
|
|
949
|
+
keys: Object.freeze({ preset: name }),
|
|
950
|
+
preset: name,
|
|
951
|
+
...(preset.filter ? { filter: preset.filter } : {}),
|
|
952
|
+
...(preset.fx ? { fx: preset.fx } : {}),
|
|
953
|
+
...(preset.reverb ? { reverb: preset.reverb } : {}),
|
|
954
|
+
});
|
|
955
|
+
}
|