@hraness/dawg 0.4.1 → 0.6.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 +99 -0
- package/DAWG.md +551 -33
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +41 -12
- package/core/expression.ts +1241 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/resonators.ts +574 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +717 -62
- package/core/sdk/eval-child.ts +40 -3
- package/core/sdk/print.ts +614 -27
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2824 -45
- package/core/sections.ts +2072 -0
- package/core/strings.ts +845 -0
- package/core/synth.ts +11 -1
- package/core/tempo.ts +1318 -0
- package/core/tuning.ts +1180 -0
- package/guides/audition.md +26 -0
- package/guides/automation.md +26 -0
- package/guides/chords.md +28 -0
- package/guides/effects.md +29 -0
- package/guides/faders.md +29 -0
- package/guides/files.md +28 -0
- package/guides/getting-started.md +26 -0
- package/guides/index.ts +75 -0
- package/guides/keys.md +27 -0
- package/guides/media.md +27 -0
- package/guides/mix.md +23 -0
- package/guides/music.md +14 -0
- package/guides/notes.md +26 -0
- package/guides/performance.md +26 -0
- package/guides/play.md +24 -0
- package/guides/project.md +13 -0
- package/guides/providers.md +25 -0
- package/guides/rhythm.md +26 -0
- package/guides/sessions.md +21 -0
- package/guides/sound.md +15 -0
- package/guides/sounds.md +29 -0
- package/guides/tempo.md +27 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +9 -0
- package/src/agent/brief.ts +77 -3
- package/src/agent/chord-tools.ts +9 -1
- package/src/agent/expression-tools.ts +336 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +68 -6
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +27 -4
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +574 -16
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +9 -0
- package/src/audio/arrange.ts +489 -0
- package/src/audio/audition.ts +16 -3
- package/src/audio/click.ts +113 -1
- package/src/audio/clock.ts +71 -5
- package/src/audio/dsp/bank.ts +233 -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/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/bus.ts +5 -4
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/dynamics.ts +2 -1
- package/src/audio/effects/filter.ts +5 -3
- package/src/audio/effects/modulation.ts +5 -1
- 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/effects/space.ts +92 -2
- package/src/audio/engine.ts +100 -21
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +263 -17
- package/src/audio/loudness.ts +596 -0
- package/src/audio/master.ts +660 -0
- package/src/audio/measure-worker.ts +45 -0
- package/src/audio/measure.ts +110 -0
- package/src/audio/player.ts +11 -6
- package/src/audio/preview.ts +76 -14
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +269 -34
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +147 -0
- package/src/audio/wav.ts +377 -30
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +941 -0
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +355 -27
- package/src/commands/keys.ts +353 -0
- package/src/commands/master.ts +361 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +967 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +919 -41
- package/src/project/check.ts +13 -0
- package/src/render.ts +139 -8
- package/src/session/daemon.ts +18 -8
- package/src/session/naming.ts +8 -1
- package/src/session/rebase.ts +21 -0
- package/src/tui/arrange-menu.ts +390 -0
- package/src/tui/audition.ts +38 -5
- package/src/tui/fader.ts +409 -0
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +967 -23
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +3 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +459 -43
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +63 -1
- package/tui/guide.ts +351 -0
- package/tui/highway.ts +87 -4
- package/tui/hits.ts +68 -0
- package/tui/input.ts +7 -0
- package/tui/keys.ts +72 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instrument words for 0.6: the one place a typed, menu, agent or SDK
|
|
3
|
+
* instrument word turns into what a track stores. Pure and import-free;
|
|
4
|
+
* `core/sdk/v1.ts` carries an exact copy of the section below (run
|
|
5
|
+
* `bun core/sdk/sync-instruments.ts`).
|
|
6
|
+
*
|
|
7
|
+
* Lanes add rows to `INSTRUMENT_WORDS` (one block each, appended). A legacy
|
|
8
|
+
* word always resolves to itself, so stored and typed `piano`, `marimba`,
|
|
9
|
+
* `sitar` keep today's voice; a row can never claim one. With no rows,
|
|
10
|
+
* every word resolves exactly as it did before 0.6.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---- instrument words (copied into core/sdk/v1.ts) ----
|
|
14
|
+
|
|
15
|
+
/** What an instrument word stores on a track. */
|
|
16
|
+
type InstrumentWord = Readonly<{
|
|
17
|
+
/** The `Track.instrument` value. */
|
|
18
|
+
instrument: string;
|
|
19
|
+
/** The optional Track field the engine reads (created with defaults). */
|
|
20
|
+
field?: string;
|
|
21
|
+
/** A preset of that engine to apply. */
|
|
22
|
+
preset?: string;
|
|
23
|
+
/** An insert-effect preset to apply with it (rig aliases). */
|
|
24
|
+
fx?: string;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
/** A word and what it means. */
|
|
28
|
+
type InstrumentWordRow = Readonly<{
|
|
29
|
+
word: string;
|
|
30
|
+
/**
|
|
31
|
+
* Borrow the voice (instrument, field, preset) of this other word's row
|
|
32
|
+
* when it exists; `instrument` is the fallback when it does not.
|
|
33
|
+
*/
|
|
34
|
+
voice?: string;
|
|
35
|
+
}> &
|
|
36
|
+
InstrumentWord;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Words that keep their pre-0.6 meaning forever: they resolve to
|
|
40
|
+
* themselves, whatever rows the lanes add.
|
|
41
|
+
*/
|
|
42
|
+
export const LEGACY_WORDS: readonly string[] = Object.freeze([
|
|
43
|
+
"piano",
|
|
44
|
+
"pluck",
|
|
45
|
+
"bass",
|
|
46
|
+
"saw",
|
|
47
|
+
"square",
|
|
48
|
+
"triangle",
|
|
49
|
+
"marimba",
|
|
50
|
+
"wind",
|
|
51
|
+
"cello",
|
|
52
|
+
"contrabass",
|
|
53
|
+
"ebass",
|
|
54
|
+
"sitar",
|
|
55
|
+
"organ",
|
|
56
|
+
"strings",
|
|
57
|
+
"bell",
|
|
58
|
+
"keys",
|
|
59
|
+
"lead",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/** 0.6 instrument words; each lane appends its own block. */
|
|
63
|
+
export const INSTRUMENT_WORDS: readonly InstrumentWordRow[] = Object.freeze([
|
|
64
|
+
// strings (f06-strings): plucked presets of the string engine. Legacy
|
|
65
|
+
// sitar/ebass keep today's voice (`string preset sitar` reaches the
|
|
66
|
+
// engine), jangle is the rig alias (the guitar lane maps its 12string to the
|
|
67
|
+
// preset; `string jangle` reaches it) and
|
|
68
|
+
// upright is the keys lane's piano (doublebass reaches the preset).
|
|
69
|
+
{ word: "nylon", instrument: "string", field: "string", preset: "nylon" },
|
|
70
|
+
{ word: "steel", instrument: "string", field: "string", preset: "steel" },
|
|
71
|
+
{
|
|
72
|
+
word: "electric",
|
|
73
|
+
instrument: "string",
|
|
74
|
+
field: "string",
|
|
75
|
+
preset: "electric",
|
|
76
|
+
},
|
|
77
|
+
{ word: "slap", instrument: "string", field: "string", preset: "slap" },
|
|
78
|
+
{ word: "motown", instrument: "string", field: "string", preset: "motown" },
|
|
79
|
+
{ word: "tanpura", instrument: "string", field: "string", preset: "tanpura" },
|
|
80
|
+
{
|
|
81
|
+
word: "harpsichord",
|
|
82
|
+
instrument: "string",
|
|
83
|
+
field: "string",
|
|
84
|
+
preset: "harpsichord",
|
|
85
|
+
},
|
|
86
|
+
{ word: "lute", instrument: "string", field: "string", preset: "lute" },
|
|
87
|
+
{ word: "oud", instrument: "string", field: "string", preset: "oud" },
|
|
88
|
+
{ word: "setar", instrument: "string", field: "string", preset: "setar" },
|
|
89
|
+
{ word: "tar", instrument: "string", field: "string", preset: "tar" },
|
|
90
|
+
{ word: "santur", instrument: "string", field: "string", preset: "santur" },
|
|
91
|
+
{
|
|
92
|
+
word: "dulcimer",
|
|
93
|
+
instrument: "string",
|
|
94
|
+
field: "string",
|
|
95
|
+
preset: "dulcimer",
|
|
96
|
+
},
|
|
97
|
+
{ word: "koto", instrument: "string", field: "string", preset: "koto" },
|
|
98
|
+
{ word: "harp", instrument: "string", field: "string", preset: "harp" },
|
|
99
|
+
{ word: "banjo", instrument: "string", field: "string", preset: "banjo" },
|
|
100
|
+
{ word: "tres", instrument: "string", field: "string", preset: "tres" },
|
|
101
|
+
{
|
|
102
|
+
word: "requinto",
|
|
103
|
+
instrument: "string",
|
|
104
|
+
field: "string",
|
|
105
|
+
preset: "requinto",
|
|
106
|
+
},
|
|
107
|
+
{ word: "acoustic", instrument: "string", field: "string", preset: "steel" },
|
|
108
|
+
{ word: "classical", instrument: "string", field: "string", preset: "nylon" },
|
|
109
|
+
{
|
|
110
|
+
word: "bassguitar",
|
|
111
|
+
instrument: "string",
|
|
112
|
+
field: "string",
|
|
113
|
+
preset: "ebass",
|
|
114
|
+
},
|
|
115
|
+
{ word: "fender", instrument: "string", field: "string", preset: "ebass" },
|
|
116
|
+
{
|
|
117
|
+
word: "doublebass",
|
|
118
|
+
instrument: "string",
|
|
119
|
+
field: "string",
|
|
120
|
+
preset: "upright",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
word: "cembalo",
|
|
124
|
+
instrument: "string",
|
|
125
|
+
field: "string",
|
|
126
|
+
preset: "harpsichord",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
word: "hammered",
|
|
130
|
+
instrument: "string",
|
|
131
|
+
field: "string",
|
|
132
|
+
preset: "dulcimer",
|
|
133
|
+
},
|
|
134
|
+
{ word: "sehtar", instrument: "string", field: "string", preset: "setar" },
|
|
135
|
+
// f06-rig: guitar track aliases, a guitar voice plus a whole rig. The
|
|
136
|
+
// voice is the strings lane's `electric` row (jangle: its 12-string
|
|
137
|
+
// `jangle` preset); the pluck only while that row is absent. Never `lead`
|
|
138
|
+
// or `bass`.
|
|
139
|
+
{
|
|
140
|
+
word: "jangle",
|
|
141
|
+
instrument: "string",
|
|
142
|
+
field: "string",
|
|
143
|
+
preset: "jangle",
|
|
144
|
+
fx: "jangle",
|
|
145
|
+
},
|
|
146
|
+
{ word: "punk", instrument: "pluck", voice: "electric", fx: "punk" },
|
|
147
|
+
{ word: "funk", instrument: "pluck", voice: "electric", fx: "funk" },
|
|
148
|
+
{ word: "ragged", instrument: "pluck", voice: "electric", fx: "ragged" },
|
|
149
|
+
{ word: "gtr-lead", instrument: "pluck", voice: "electric", fx: "lead" },
|
|
150
|
+
{ word: "gtr-metal", instrument: "pluck", voice: "electric", fx: "metal" },
|
|
151
|
+
{ word: "bachata", instrument: "pluck", voice: "electric", fx: "bachata" },
|
|
152
|
+
// granular (f06-granular): the instrument and its texture presets. Each
|
|
153
|
+
// starts from a built-in synth source, so nothing downloads.
|
|
154
|
+
{
|
|
155
|
+
word: "granular",
|
|
156
|
+
instrument: "granular",
|
|
157
|
+
field: "granular",
|
|
158
|
+
preset: "cloud",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
word: "grains",
|
|
162
|
+
instrument: "granular",
|
|
163
|
+
field: "granular",
|
|
164
|
+
preset: "cloud",
|
|
165
|
+
},
|
|
166
|
+
{ word: "cloud", instrument: "granular", field: "granular", preset: "cloud" },
|
|
167
|
+
{
|
|
168
|
+
word: "sparkle",
|
|
169
|
+
instrument: "granular",
|
|
170
|
+
field: "granular",
|
|
171
|
+
preset: "sparkle",
|
|
172
|
+
},
|
|
173
|
+
{ word: "swarm", instrument: "granular", field: "granular", preset: "swarm" },
|
|
174
|
+
{
|
|
175
|
+
word: "microloop",
|
|
176
|
+
instrument: "granular",
|
|
177
|
+
field: "granular",
|
|
178
|
+
preset: "microloop",
|
|
179
|
+
},
|
|
180
|
+
// keys (f06-piano): modelled pianos. `piano` stays legacy here; the typed
|
|
181
|
+
// surfaces store a new `piano` as `grand` (core/keys.ts `pianoWrite`).
|
|
182
|
+
{ word: "grand", instrument: "grand", field: "keys", preset: "grand" },
|
|
183
|
+
{ word: "ballad", instrument: "grand", field: "keys", preset: "ballad" },
|
|
184
|
+
{ word: "upright", instrument: "upright", field: "keys", preset: "upright" },
|
|
185
|
+
{ word: "felt", instrument: "felt", field: "keys", preset: "felt" },
|
|
186
|
+
{ word: "lofi", instrument: "felt", field: "keys", preset: "lofi" },
|
|
187
|
+
{
|
|
188
|
+
word: "honkytonk",
|
|
189
|
+
instrument: "honkytonk",
|
|
190
|
+
field: "keys",
|
|
191
|
+
preset: "honkytonk",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
word: "prepared",
|
|
195
|
+
instrument: "prepared",
|
|
196
|
+
field: "keys",
|
|
197
|
+
preset: "prepared",
|
|
198
|
+
},
|
|
199
|
+
// f06-modal: mallets and bells (core/resonators.ts). `marimba` is legacy;
|
|
200
|
+
// `modal` alone gives the modal marimba.
|
|
201
|
+
{ word: "modal", instrument: "modal", field: "modal", preset: "marimba" },
|
|
202
|
+
{ word: "vibes", instrument: "modal", field: "modal", preset: "vibes" },
|
|
203
|
+
{ word: "vibraphone", instrument: "modal", field: "modal", preset: "vibes" },
|
|
204
|
+
{
|
|
205
|
+
word: "xylophone",
|
|
206
|
+
instrument: "modal",
|
|
207
|
+
field: "modal",
|
|
208
|
+
preset: "xylophone",
|
|
209
|
+
},
|
|
210
|
+
{ word: "glock", instrument: "modal", field: "modal", preset: "glock" },
|
|
211
|
+
{
|
|
212
|
+
word: "glockenspiel",
|
|
213
|
+
instrument: "modal",
|
|
214
|
+
field: "modal",
|
|
215
|
+
preset: "glock",
|
|
216
|
+
},
|
|
217
|
+
{ word: "celesta", instrument: "modal", field: "modal", preset: "celesta" },
|
|
218
|
+
{ word: "chimes", instrument: "modal", field: "modal", preset: "chimes" },
|
|
219
|
+
{ word: "tubular", instrument: "modal", field: "modal", preset: "chimes" },
|
|
220
|
+
{ word: "kalimba", instrument: "modal", field: "modal", preset: "kalimba" },
|
|
221
|
+
{
|
|
222
|
+
word: "thumbpiano",
|
|
223
|
+
instrument: "modal",
|
|
224
|
+
field: "modal",
|
|
225
|
+
preset: "kalimba",
|
|
226
|
+
},
|
|
227
|
+
{ word: "mbira", instrument: "modal", field: "modal", preset: "mbira" },
|
|
228
|
+
{ word: "steelpan", instrument: "modal", field: "modal", preset: "steelpan" },
|
|
229
|
+
{ word: "bowl", instrument: "modal", field: "modal", preset: "bowl" },
|
|
230
|
+
{ word: "gong", instrument: "modal", field: "modal", preset: "gong" },
|
|
231
|
+
{ word: "gongageng", instrument: "modal", field: "modal", preset: "gong" },
|
|
232
|
+
{ word: "timpani", instrument: "modal", field: "modal", preset: "timpani" },
|
|
233
|
+
{
|
|
234
|
+
word: "steeldrum",
|
|
235
|
+
instrument: "modal",
|
|
236
|
+
field: "modal",
|
|
237
|
+
preset: "steelpan",
|
|
238
|
+
},
|
|
239
|
+
{ word: "singingbowl", instrument: "modal", field: "modal", preset: "bowl" },
|
|
240
|
+
{
|
|
241
|
+
word: "kettledrum",
|
|
242
|
+
instrument: "modal",
|
|
243
|
+
field: "modal",
|
|
244
|
+
preset: "timpani",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
word: "tubularbells",
|
|
248
|
+
instrument: "modal",
|
|
249
|
+
field: "modal",
|
|
250
|
+
preset: "chimes",
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* What an instrument word means: a legacy word is itself, a row word is its
|
|
256
|
+
* row, anything else is undefined (callers keep the word as typed).
|
|
257
|
+
*/
|
|
258
|
+
export function resolveInstrumentWord(
|
|
259
|
+
word: string,
|
|
260
|
+
): InstrumentWord | undefined {
|
|
261
|
+
if (LEGACY_WORDS.includes(word)) return Object.freeze({ instrument: word });
|
|
262
|
+
const row = INSTRUMENT_WORDS.find((entry) => entry.word === word);
|
|
263
|
+
if (!row) return undefined;
|
|
264
|
+
const { word: _word, voice, ...meaning } = row;
|
|
265
|
+
const borrowed =
|
|
266
|
+
voice === undefined
|
|
267
|
+
? undefined
|
|
268
|
+
: INSTRUMENT_WORDS.find((entry) => entry.word === voice && !entry.voice);
|
|
269
|
+
if (!borrowed) return Object.freeze(meaning);
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
instrument: borrowed.instrument,
|
|
272
|
+
...(borrowed.field === undefined ? {} : { field: borrowed.field }),
|
|
273
|
+
...(borrowed.preset === undefined ? {} : { preset: borrowed.preset }),
|
|
274
|
+
...(meaning.fx === undefined ? {} : { fx: meaning.fx }),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The `Track.instrument` value a word stores (the word itself if unknown). */
|
|
279
|
+
export function instrumentForWord(word: string): string {
|
|
280
|
+
return resolveInstrumentWord(word)?.instrument ?? word;
|
|
281
|
+
}
|
package/core/keys.ts
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
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
|
+
/** Body EQ voicings (soundboard and case colour). */
|
|
39
|
+
export const PIANO_BODIES = Object.freeze([
|
|
40
|
+
"grand",
|
|
41
|
+
"upright",
|
|
42
|
+
"felt",
|
|
43
|
+
"honkytonk",
|
|
44
|
+
"prepared",
|
|
45
|
+
] as const);
|
|
46
|
+
|
|
47
|
+
export type PianoBody = (typeof PIANO_BODIES)[number];
|
|
48
|
+
|
|
49
|
+
const unit = (
|
|
50
|
+
value: number,
|
|
51
|
+
doc: string,
|
|
52
|
+
automate = false,
|
|
53
|
+
step = 0.05,
|
|
54
|
+
): NumberParam => ({
|
|
55
|
+
kind: "number",
|
|
56
|
+
min: 0,
|
|
57
|
+
max: 1,
|
|
58
|
+
default: value,
|
|
59
|
+
step,
|
|
60
|
+
...(automate ? { automate } : {}),
|
|
61
|
+
doc,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Every keys parameter, in storage and print order. Defaults are the grand's;
|
|
66
|
+
* a family's own defaults (`PIANO_FAMILY_DEFAULTS`) sit between these and
|
|
67
|
+
* the track's overrides. `automate` parameters have a `keys-<param>` lane,
|
|
68
|
+
* read at each note's onset.
|
|
69
|
+
*/
|
|
70
|
+
export const KEYS_PARAMS: Readonly<Record<string, ParamSpec>> = Object.freeze({
|
|
71
|
+
hardness: unit(
|
|
72
|
+
0.5,
|
|
73
|
+
"hammer felt hardness: brightness at a given velocity",
|
|
74
|
+
true,
|
|
75
|
+
),
|
|
76
|
+
touch: unit(1, "velocity sensitivity (0 plays every note at 0.8)", true),
|
|
77
|
+
inharm: {
|
|
78
|
+
kind: "number",
|
|
79
|
+
min: 0,
|
|
80
|
+
max: 4,
|
|
81
|
+
default: 1,
|
|
82
|
+
step: 0.1,
|
|
83
|
+
unit: "x",
|
|
84
|
+
doc: "inharmonicity multiplier (0 harmonic, 1 grand, 2.5 upright)",
|
|
85
|
+
},
|
|
86
|
+
unison: {
|
|
87
|
+
kind: "number",
|
|
88
|
+
min: 0,
|
|
89
|
+
max: 30,
|
|
90
|
+
default: 0.7,
|
|
91
|
+
step: 0.5,
|
|
92
|
+
unit: "c",
|
|
93
|
+
doc: "detune spread of the unison strings in cents",
|
|
94
|
+
},
|
|
95
|
+
decay: {
|
|
96
|
+
kind: "number",
|
|
97
|
+
min: 0.1,
|
|
98
|
+
max: 4,
|
|
99
|
+
default: 1,
|
|
100
|
+
step: 0.1,
|
|
101
|
+
unit: "x",
|
|
102
|
+
automate: true,
|
|
103
|
+
doc: "sustain time multiplier",
|
|
104
|
+
},
|
|
105
|
+
release: {
|
|
106
|
+
kind: "number",
|
|
107
|
+
min: 0.1,
|
|
108
|
+
max: 4,
|
|
109
|
+
default: 1,
|
|
110
|
+
step: 0.1,
|
|
111
|
+
unit: "x",
|
|
112
|
+
automate: true,
|
|
113
|
+
doc: "damper time multiplier (how fast a released key stops)",
|
|
114
|
+
},
|
|
115
|
+
strike: {
|
|
116
|
+
kind: "number",
|
|
117
|
+
min: 0.04,
|
|
118
|
+
max: 0.3,
|
|
119
|
+
default: 0.12,
|
|
120
|
+
step: 0.01,
|
|
121
|
+
doc: "hammer position along the string",
|
|
122
|
+
},
|
|
123
|
+
after: unit(0.3, "aftersound share (the slow second stage of the decay)"),
|
|
124
|
+
knock: unit(0.5, "soundboard knock and hammer thump", true),
|
|
125
|
+
noise: unit(0.25, "key-off and damper mechanics", true),
|
|
126
|
+
felt: unit(0, "felt strip between hammers and strings", true),
|
|
127
|
+
prep: unit(0, "share of keys carrying a preparation (seeded per key)"),
|
|
128
|
+
width: unit(0.6, "keyboard stereo spread, bass left and treble right"),
|
|
129
|
+
stretch: unit(
|
|
130
|
+
1,
|
|
131
|
+
"octave stretch from the strings' own inharmonicity; 0 keeps tuning exact",
|
|
132
|
+
),
|
|
133
|
+
body: {
|
|
134
|
+
kind: "enum",
|
|
135
|
+
values: PIANO_BODIES,
|
|
136
|
+
default: "grand",
|
|
137
|
+
optional: true,
|
|
138
|
+
doc: "body EQ voicing (default: the family's own)",
|
|
139
|
+
},
|
|
140
|
+
vib: {
|
|
141
|
+
kind: "number",
|
|
142
|
+
min: 0,
|
|
143
|
+
max: 64,
|
|
144
|
+
default: 0,
|
|
145
|
+
step: 0.25,
|
|
146
|
+
unit: "Hz",
|
|
147
|
+
doc: "pitch wobble rate (tape wow); 0 is off; note vibrato replaces it",
|
|
148
|
+
strudel: ["vib", "vibrato", "v"],
|
|
149
|
+
},
|
|
150
|
+
vibmod: {
|
|
151
|
+
kind: "number",
|
|
152
|
+
min: 0,
|
|
153
|
+
max: 24,
|
|
154
|
+
default: 0.5,
|
|
155
|
+
step: 0.05,
|
|
156
|
+
unit: "st",
|
|
157
|
+
doc: "pitch wobble depth in semitones",
|
|
158
|
+
strudel: ["vibmod", "vmod"],
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/** The parameters shown first in the menu and `keys` listing. */
|
|
163
|
+
export const KEYS_SIMPLE = Object.freeze([
|
|
164
|
+
"hardness",
|
|
165
|
+
"decay",
|
|
166
|
+
"release",
|
|
167
|
+
"felt",
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
export type TrackKeys = Readonly<
|
|
171
|
+
Record<string, number | string> & { preset?: string }
|
|
172
|
+
>;
|
|
173
|
+
|
|
174
|
+
/** Each family's own defaults over the grand's (`KEYS_PARAMS`). */
|
|
175
|
+
export const PIANO_FAMILY_DEFAULTS: Readonly<
|
|
176
|
+
Record<PianoFamily, Readonly<Record<string, number | string>>>
|
|
177
|
+
> = Object.freeze({
|
|
178
|
+
grand: Object.freeze({ body: "grand" }),
|
|
179
|
+
upright: Object.freeze({ inharm: 2.5, decay: 0.6, body: "upright" }),
|
|
180
|
+
felt: Object.freeze({
|
|
181
|
+
felt: 1,
|
|
182
|
+
hardness: 0.3,
|
|
183
|
+
noise: 0.6,
|
|
184
|
+
body: "felt",
|
|
185
|
+
}),
|
|
186
|
+
honkytonk: Object.freeze({ unison: 16, hardness: 0.7, body: "honkytonk" }),
|
|
187
|
+
prepared: Object.freeze({ prep: 0.6, body: "prepared" }),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/** Effects a preset sets with the voice (applied over the effect defaults). */
|
|
191
|
+
export type KeysPresetFx = Readonly<{
|
|
192
|
+
/** The track filter (`Track.filter`). */
|
|
193
|
+
filter?: Readonly<{ cutoff: number; resonance: number; type?: "lpf" }>;
|
|
194
|
+
/** Chain effects (`Track.fx`), merged over the track's own. */
|
|
195
|
+
fx?: Readonly<Record<string, Readonly<Record<string, number | string>>>>;
|
|
196
|
+
reverb?: Readonly<{ mix: number; size: number }>;
|
|
197
|
+
}>;
|
|
198
|
+
|
|
199
|
+
export type KeysPreset = Readonly<{
|
|
200
|
+
instrument: PianoFamily;
|
|
201
|
+
/** The preset's overrides over the family defaults. */
|
|
202
|
+
keys: Readonly<Record<string, number | string>>;
|
|
203
|
+
doc: string;
|
|
204
|
+
/** Styles it suits (shown in /help and the agent prompt). */
|
|
205
|
+
styles: string;
|
|
206
|
+
}> &
|
|
207
|
+
KeysPresetFx;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Named starting points. Loading one stores `keys: { preset }` (replacing
|
|
211
|
+
* any overrides, the synth rule); its overrides are read at render, so a
|
|
212
|
+
* later `keys hardness 0.4` tweaks the preset.
|
|
213
|
+
*/
|
|
214
|
+
export const KEYS_PRESETS: Readonly<Record<string, KeysPreset>> = Object.freeze(
|
|
215
|
+
{
|
|
216
|
+
grand: {
|
|
217
|
+
instrument: "grand",
|
|
218
|
+
keys: {},
|
|
219
|
+
doc: "concert grand, bright and long, three-string unisons",
|
|
220
|
+
styles: "Chopin, Beethoven, Brahms, Debussy, jazz, Glass, hyperpop",
|
|
221
|
+
},
|
|
222
|
+
ballad: {
|
|
223
|
+
instrument: "grand",
|
|
224
|
+
keys: { hardness: 0.3, after: 0.5, decay: 1.3 },
|
|
225
|
+
reverb: { mix: 0.25, size: 0.7 },
|
|
226
|
+
doc: "darker grand, soft hammer, more aftersound, in a room",
|
|
227
|
+
styles: "dark ballads, Mitski, Sakamoto, Brahms intermezzi",
|
|
228
|
+
},
|
|
229
|
+
upright: {
|
|
230
|
+
instrument: "upright",
|
|
231
|
+
keys: {},
|
|
232
|
+
doc: "boxy, more inharmonic, shorter",
|
|
233
|
+
styles: "soul, Motown, indie, Beach House",
|
|
234
|
+
},
|
|
235
|
+
felt: {
|
|
236
|
+
instrument: "felt",
|
|
237
|
+
keys: {},
|
|
238
|
+
reverb: { mix: 0.2, size: 0.5 },
|
|
239
|
+
doc: "felt strip down, muted and intimate, audible mechanics",
|
|
240
|
+
styles: "Sakamoto, Eno, Nils Frahm, The Field, Four Tet",
|
|
241
|
+
},
|
|
242
|
+
lofi: {
|
|
243
|
+
instrument: "felt",
|
|
244
|
+
keys: { vib: 0.3, vibmod: 0.05 },
|
|
245
|
+
filter: { cutoff: 3500, resonance: 0.1 },
|
|
246
|
+
fx: { crush: { bits: 10 } },
|
|
247
|
+
doc: "felt piano with tape wow, a dark filter and a little crush",
|
|
248
|
+
styles: "lo-fi hip hop, bedroom pop, chillhop",
|
|
249
|
+
},
|
|
250
|
+
honkytonk: {
|
|
251
|
+
instrument: "honkytonk",
|
|
252
|
+
keys: {},
|
|
253
|
+
doc: "16-cent unisons, bright saloon upright",
|
|
254
|
+
styles: "ragtime, saloon, A. G. Cook",
|
|
255
|
+
},
|
|
256
|
+
prepared: {
|
|
257
|
+
instrument: "prepared",
|
|
258
|
+
keys: {},
|
|
259
|
+
doc: "bolts, rubber and screws on 60% of keys (seeded per key)",
|
|
260
|
+
styles: "Cage, Aphex-style, Oneohtrix Point Never",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
export function isKeysPreset(name: string): boolean {
|
|
266
|
+
return Object.prototype.hasOwnProperty.call(KEYS_PRESETS, name);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function isKeysParam(name: string): boolean {
|
|
270
|
+
return Object.prototype.hasOwnProperty.call(KEYS_PARAMS, name);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Spec name for a parameter typed as its name or a Strudel alias. */
|
|
274
|
+
export function keysParamName(name: string): string | undefined {
|
|
275
|
+
const lower = name.toLowerCase();
|
|
276
|
+
if (isKeysParam(lower)) return lower;
|
|
277
|
+
if (lower === "aftersound") return "after";
|
|
278
|
+
if (lower === "prepared") return "prep";
|
|
279
|
+
for (const [key, spec] of Object.entries(KEYS_PARAMS))
|
|
280
|
+
if (spec.strudel?.some((alias) => alias.toLowerCase() === lower))
|
|
281
|
+
return key;
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Keys parameters with a `keys-<param>` lane (read at note onsets). */
|
|
286
|
+
export const KEYS_LANE_PARAMS: readonly Readonly<{
|
|
287
|
+
param: string;
|
|
288
|
+
spec: NumberParam;
|
|
289
|
+
}>[] = Object.freeze(
|
|
290
|
+
Object.entries(KEYS_PARAMS)
|
|
291
|
+
.filter(
|
|
292
|
+
(entry): entry is [string, NumberParam] =>
|
|
293
|
+
entry[1].kind === "number" && entry[1].automate === true,
|
|
294
|
+
)
|
|
295
|
+
.map(([param, spec]) => Object.freeze({ param, spec })),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Validates `Track.keys`: overrides in `KEYS_PARAMS` order, then `preset`.
|
|
300
|
+
* `{}` stays `{}` (its presence turns the modelled piano on); null and
|
|
301
|
+
* undefined are absent.
|
|
302
|
+
*/
|
|
303
|
+
export function normalizeKeys(input: unknown): TrackKeys | undefined {
|
|
304
|
+
if (input === undefined || input === null) return undefined;
|
|
305
|
+
if (!isRecord(input))
|
|
306
|
+
throw new FxValidationError("track keys must be an object or null");
|
|
307
|
+
const params: Record<string, unknown> = {};
|
|
308
|
+
let preset: string | undefined;
|
|
309
|
+
for (const [key, value] of Object.entries(input)) {
|
|
310
|
+
if (value === undefined || value === null) continue;
|
|
311
|
+
if (key === "preset") {
|
|
312
|
+
if (typeof value !== "string" || !isKeysPreset(value))
|
|
313
|
+
throw new FxValidationError(
|
|
314
|
+
`keys preset must be one of ${Object.keys(KEYS_PRESETS).join(", ")}`,
|
|
315
|
+
);
|
|
316
|
+
preset = value;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (!isKeysParam(key)) {
|
|
320
|
+
const name = keysParamName(key);
|
|
321
|
+
throw new FxValidationError(
|
|
322
|
+
`keys has no parameter "${key}"${name ? ` (did you mean "${name}"?)` : ""}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
params[key] = value;
|
|
326
|
+
}
|
|
327
|
+
const values = normalizeParams(KEYS_PARAMS, params, "keys", false);
|
|
328
|
+
return Object.freeze({
|
|
329
|
+
...(values as Record<string, number | string>),
|
|
330
|
+
...(preset ? { preset } : {}),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Every keys parameter for a track: the grand defaults, then the family's,
|
|
336
|
+
* then the stored preset's, then the stored overrides.
|
|
337
|
+
*/
|
|
338
|
+
export function resolvedKeys(
|
|
339
|
+
instrument: string,
|
|
340
|
+
keys: TrackKeys | undefined,
|
|
341
|
+
): Readonly<Record<string, number | string>> {
|
|
342
|
+
const out: Record<string, number | string> = {};
|
|
343
|
+
for (const [key, spec] of Object.entries(KEYS_PARAMS))
|
|
344
|
+
out[key] = spec.default as number | string;
|
|
345
|
+
if (isPianoFamily(instrument))
|
|
346
|
+
Object.assign(out, PIANO_FAMILY_DEFAULTS[instrument]);
|
|
347
|
+
const preset = keys?.preset ? KEYS_PRESETS[keys.preset] : undefined;
|
|
348
|
+
if (preset) Object.assign(out, preset.keys);
|
|
349
|
+
for (const [key, value] of Object.entries(keys ?? {}))
|
|
350
|
+
if (key !== "preset") out[key] = value;
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* What a new write of a keys word stores: `piano` and `grand` become the
|
|
356
|
+
* modelled grand, a preset name its instrument and overrides. Undefined
|
|
357
|
+
* for any other word.
|
|
358
|
+
*/
|
|
359
|
+
export function pianoWrite(word: string):
|
|
360
|
+
| (Readonly<{
|
|
361
|
+
instrument: PianoFamily;
|
|
362
|
+
keys: TrackKeys;
|
|
363
|
+
preset?: string;
|
|
364
|
+
}> &
|
|
365
|
+
KeysPresetFx)
|
|
366
|
+
| undefined {
|
|
367
|
+
const lower = word.toLowerCase();
|
|
368
|
+
const name =
|
|
369
|
+
lower === "piano" || lower === "uprightpiano"
|
|
370
|
+
? lower === "piano"
|
|
371
|
+
? "grand"
|
|
372
|
+
: "upright"
|
|
373
|
+
: lower === "feltpiano"
|
|
374
|
+
? "felt"
|
|
375
|
+
: lower;
|
|
376
|
+
if (!isKeysPreset(name)) return undefined;
|
|
377
|
+
const preset = KEYS_PRESETS[name]!;
|
|
378
|
+
return Object.freeze({
|
|
379
|
+
instrument: preset.instrument,
|
|
380
|
+
keys: Object.freeze({ preset: name }),
|
|
381
|
+
preset: name,
|
|
382
|
+
...(preset.filter ? { filter: preset.filter } : {}),
|
|
383
|
+
...(preset.fx ? { fx: preset.fx } : {}),
|
|
384
|
+
...(preset.reverb ? { reverb: preset.reverb } : {}),
|
|
385
|
+
});
|
|
386
|
+
}
|