@hraness/dawg 0.3.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 +122 -0
- package/DAWG.md +605 -39
- package/README.md +4 -4
- package/core/chords.ts +1724 -0
- package/core/diff.ts +13 -1
- package/core/euclid.ts +670 -0
- package/core/fx.ts +1065 -0
- package/core/kits.ts +320 -0
- package/core/params.ts +111 -0
- package/core/rhythm.ts +287 -0
- package/core/score.ts +735 -18
- package/core/sdk/eval-child.ts +34 -22
- package/core/sdk/eval.ts +4 -1
- package/core/sdk/print.ts +304 -22
- package/core/sdk/sync-chords.ts +54 -0
- package/core/sdk/v1.ts +3226 -21
- package/core/synth.ts +1001 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +38 -3
- package/src/agent/brief.ts +29 -4
- package/src/agent/chord-tools.ts +357 -0
- package/src/agent/drum-tools.ts +135 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/pack-tools.ts +369 -0
- package/src/agent/planner.ts +17 -2
- package/src/agent/preview-tool.ts +270 -0
- package/src/agent/rhythm-tools.ts +145 -0
- package/src/agent/tools.ts +308 -14
- package/src/agent/xcb-agent.ts +8 -2
- package/src/audio/audition.ts +93 -0
- package/src/audio/cache.ts +160 -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 +27 -4
- package/src/audio/kits.ts +200 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/preview.ts +470 -0
- package/src/audio/random.ts +15 -0
- package/src/audio/sampler.ts +157 -29
- package/src/audio/samples.ts +386 -44
- 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 +280 -278
- package/src/audio/wavetable-maker.ts +717 -0
- package/src/audio/wavetable.ts +624 -0
- package/src/commands/drums.ts +299 -0
- package/src/commands/edit.ts +26 -4
- package/src/commands/fx.ts +360 -0
- package/src/commands/help.ts +298 -10
- package/src/commands/music.ts +10 -8
- package/src/commands/pack.ts +423 -0
- package/src/commands/rhythm.ts +230 -0
- package/src/commands/sample.ts +162 -2
- package/src/commands/synth.ts +229 -0
- package/src/commands/wavetable.ts +370 -0
- package/src/main.ts +1119 -30
- package/src/media/cli.ts +15 -2
- package/src/media/tools.ts +106 -1
- package/src/media/wavetable.ts +202 -0
- package/src/render.ts +22 -1
- package/src/session/naming.ts +3 -1
- package/src/tui/audition.ts +501 -0
- package/src/tui/euclid.ts +472 -0
- package/src/tui/menu.ts +1286 -244
- package/src/tui/play-chords.ts +538 -0
- package/src/tui/play-session.ts +364 -16
- package/src/tui/sketch.ts +108 -0
- package/src/web/search.ts +4 -1
- package/tui/app.ts +161 -39
- package/tui/grammar.ts +281 -0
- package/tui/highway.ts +7 -1
- package/tui/play-strip.ts +59 -14
package/src/commands/help.ts
CHANGED
|
@@ -35,7 +35,7 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
|
|
|
35
35
|
{ command: "extend <count> bars", summary: "lengthen the loop" },
|
|
36
36
|
{
|
|
37
37
|
command: "instrument <name>",
|
|
38
|
-
summary: "sine piano pluck bass saw square triangle kit",
|
|
38
|
+
summary: "sine piano pluck bass saw square triangle wavetable kit",
|
|
39
39
|
},
|
|
40
40
|
{ command: "volume <0..1>", summary: "track level" },
|
|
41
41
|
{ command: "pan <-1..1>", summary: "left … right" },
|
|
@@ -44,12 +44,28 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
|
|
|
44
44
|
{ command: "solo", summary: "only this track" },
|
|
45
45
|
{ command: "unsolo", summary: "every track again" },
|
|
46
46
|
{ command: "clear", summary: "remove this track's notes" },
|
|
47
|
+
{
|
|
48
|
+
command: "wt <table> | wt <0..1> | wt list",
|
|
49
|
+
summary: "wavetable synth · wt basic · wt wt_digital:2 · wt 0.5",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
command: "wtenv|wtattack|wtdecay|wtrate|wtdepth|warp <n>",
|
|
53
|
+
summary: "scan the table · warpmode bendp",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
command: "fx <effect> <param> <value> | on | off | preset <name>",
|
|
57
|
+
summary: "effects · fx delay mix 0.3 · fx reverb on · fx lists them",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
command: "synth <param> <value> | preset <name>",
|
|
61
|
+
summary: "synth voice · synth lpf 1200 · synth lists every param",
|
|
62
|
+
},
|
|
47
63
|
{ command: "filter <hz> [res]", summary: "low-pass · filter off" },
|
|
48
64
|
{ command: "delay <beats> [fb] [mix]", summary: "ping-pong · delay off" },
|
|
49
65
|
{ command: "reverb <mix> [size]", summary: "room · reverb off" },
|
|
50
66
|
{
|
|
51
67
|
command: "automate <lane> at <beat> <value>",
|
|
52
|
-
summary: "volume pan filter resonance delay-feedback delay-mix",
|
|
68
|
+
summary: "volume pan filter resonance delay-feedback delay-mix wt",
|
|
53
69
|
},
|
|
54
70
|
{
|
|
55
71
|
command: "automate <lane> points <b:v>...",
|
|
@@ -62,6 +78,10 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
|
|
|
62
78
|
{ command: "clear [<lane>] automation", summary: "drop a lane's points" },
|
|
63
79
|
{ command: "track name <text>", summary: "rename this track" },
|
|
64
80
|
{ command: "meter <1..16>", summary: "beats per bar" },
|
|
81
|
+
{
|
|
82
|
+
command: "key <tonic> <mode> | none",
|
|
83
|
+
summary: "song key · key A minor · key F# dorian",
|
|
84
|
+
},
|
|
65
85
|
{
|
|
66
86
|
command: "hit <voice> at <beat>",
|
|
67
87
|
summary: "kit tracks · hit kick at 0",
|
|
@@ -71,6 +91,15 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
|
|
|
71
91
|
summary: "pattern kick every 1",
|
|
72
92
|
},
|
|
73
93
|
{ command: "clear <voice>", summary: "remove one drum voice" },
|
|
94
|
+
{
|
|
95
|
+
command: "euclid <voice> <pulses> [<steps>] [rotate <n>]",
|
|
96
|
+
summary: "generated rhythm · euclid hat 7 16 rotate 2",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
command: "euclid <voice> <field> <value> | off | freeze",
|
|
100
|
+
summary: "repeats pace accent prob swing …",
|
|
101
|
+
},
|
|
102
|
+
{ command: "grid <voice> <x.X.>", summary: "explicit steps · X accent" },
|
|
74
103
|
{ command: "undo", summary: "step back · Ctrl-Z" },
|
|
75
104
|
{ command: "redo", summary: "step forward · Ctrl-Y" },
|
|
76
105
|
],
|
|
@@ -114,15 +143,42 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
|
|
|
114
143
|
},
|
|
115
144
|
{ command: "/auth [--check]", summary: "provider and audio status" },
|
|
116
145
|
{ command: "/play [on|off]", summary: "keyboard play mode · Ctrl-P" },
|
|
146
|
+
{
|
|
147
|
+
command: "/pattern [name]",
|
|
148
|
+
summary: "drum groove picker · moving previews",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
command: "/kit [name]",
|
|
152
|
+
summary: "drum kit picker · synth kits, then samples",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
command: "/pack list|info|use|add",
|
|
156
|
+
summary: "sample packs · /pack use 909/bd",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
command: "/euclid [voice]",
|
|
160
|
+
summary: "T-1 style rhythm editor · Rhythm in /menu",
|
|
161
|
+
},
|
|
117
162
|
{
|
|
118
163
|
command: "/menu [section]",
|
|
119
|
-
summary: "
|
|
164
|
+
summary: "every setting by hand · Ctrl-K",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
command: "/try <sound command>",
|
|
168
|
+
summary: "hear it on a loop first · a A/B · enter keep",
|
|
120
169
|
},
|
|
121
170
|
{
|
|
122
171
|
command: "/click on|off|<volume>",
|
|
123
172
|
summary: "metronome · /count-in 0-2 · /grid 1/16",
|
|
124
173
|
},
|
|
125
|
-
{
|
|
174
|
+
{
|
|
175
|
+
command: "/chords auto|manual|off",
|
|
176
|
+
summary: "play-mode chords · /chords for settings",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
command: "/help [topic]",
|
|
180
|
+
summary: "start here · /help all for everything",
|
|
181
|
+
},
|
|
126
182
|
],
|
|
127
183
|
},
|
|
128
184
|
{
|
|
@@ -134,21 +190,146 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
|
|
|
134
190
|
{ command: "Ctrl-Q", summary: "toggle queue mode" },
|
|
135
191
|
{ command: "Ctrl-Z / Ctrl-Y", summary: "undo / redo" },
|
|
136
192
|
{ command: "Ctrl-O", summary: "transcript" },
|
|
137
|
-
{ command: "Esc", summary: "cancel the agent ·
|
|
193
|
+
{ command: "Esc", summary: "cancel the agent · back one level" },
|
|
194
|
+
{ command: "?", summary: "keys for the screen you are on" },
|
|
138
195
|
{ command: "Space", summary: "play/pause on an empty prompt" },
|
|
139
196
|
{
|
|
140
197
|
command: "Ctrl-P",
|
|
141
|
-
summary: "play mode
|
|
198
|
+
summary: "play mode · Z/X octave · R record · Q chords",
|
|
142
199
|
},
|
|
143
200
|
{
|
|
144
201
|
command: "Ctrl-K",
|
|
145
|
-
summary: "menu
|
|
202
|
+
summary: "menu · ←→ adjust · / filter · x reset",
|
|
146
203
|
},
|
|
147
204
|
{ command: "Ctrl-C", summary: "exit" },
|
|
148
205
|
],
|
|
149
206
|
},
|
|
150
207
|
];
|
|
151
208
|
|
|
209
|
+
/**
|
|
210
|
+
* `/help` with no topic: a short, task-first guide. Each row is something to
|
|
211
|
+
* type or press, then what it does. The full reference is `/help all` (or one
|
|
212
|
+
* group: `/help music`).
|
|
213
|
+
*/
|
|
214
|
+
export const HELP_GUIDE: readonly HelpSection[] = [
|
|
215
|
+
{
|
|
216
|
+
group: "start here" as HelpGroup,
|
|
217
|
+
entries: [
|
|
218
|
+
{ command: "type a request", summary: "“add a walking bass in A minor”" },
|
|
219
|
+
{ command: "ctrl-p", summary: "play notes on the computer keyboard" },
|
|
220
|
+
{
|
|
221
|
+
command: "ctrl-k",
|
|
222
|
+
summary: "menu: every sound, effect, rhythm, chord",
|
|
223
|
+
},
|
|
224
|
+
{ command: "?", summary: "keys for the screen you are on" },
|
|
225
|
+
{
|
|
226
|
+
command: "ctrl-z · ctrl-y",
|
|
227
|
+
summary: "undo · redo (every change is one)",
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
group: "play notes" as HelpGroup,
|
|
233
|
+
entries: [
|
|
234
|
+
{
|
|
235
|
+
command: "ctrl-p, a s d f …",
|
|
236
|
+
summary: "piano keys · z x octave · esc leave",
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
command: "r, then space",
|
|
240
|
+
summary: "record over the loop (one undo a bar)",
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
group: "make drums" as HelpGroup,
|
|
246
|
+
entries: [
|
|
247
|
+
{ command: "/pattern", summary: "pick a groove · moving previews it" },
|
|
248
|
+
{ command: "/kit", summary: "pick a drum kit" },
|
|
249
|
+
{ command: "/euclid", summary: "rhythm editor: pulses, steps, rotation" },
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
group: "shape the sound" as HelpGroup,
|
|
254
|
+
entries: [
|
|
255
|
+
{
|
|
256
|
+
command: "ctrl-k › Sound",
|
|
257
|
+
summary: "instrument, envelope, filter, wavetable",
|
|
258
|
+
},
|
|
259
|
+
{ command: "ctrl-k › Effects", summary: "delay, reverb, distortion …" },
|
|
260
|
+
{ command: "fx delay mix 0.3", summary: "the same from the prompt" },
|
|
261
|
+
{ command: "/try fx reverb mix 0.6", summary: "hear it before keeping" },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
group: "chords" as HelpGroup,
|
|
266
|
+
entries: [
|
|
267
|
+
{ command: "key A minor", summary: "set the song key" },
|
|
268
|
+
{
|
|
269
|
+
command: "ctrl-p, then q",
|
|
270
|
+
summary: "chord mode: one key plays a chord",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
command: "1–4 · 5–8 · n",
|
|
274
|
+
summary: "chord type · extension · next chord",
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
group: "more" as HelpGroup,
|
|
280
|
+
entries: [
|
|
281
|
+
{ command: "/help all", summary: "every command and key" },
|
|
282
|
+
{ command: "/help music", summary: "or session, window, keys" },
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
/** Topics `/help <topic>` takes, besides `all`. */
|
|
288
|
+
export const HELP_TOPICS = ["music", "session", "window", "keys"] as const;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Rows for `/help [topic]`: the guide with no topic, the full reference for
|
|
292
|
+
* `all`, one group for its name; undefined for an unknown topic.
|
|
293
|
+
*/
|
|
294
|
+
export function helpTopicLines(
|
|
295
|
+
topic: string | undefined,
|
|
296
|
+
width = 80,
|
|
297
|
+
): string[] | undefined {
|
|
298
|
+
const name = topic?.trim().toLowerCase().replace(/^\//, "");
|
|
299
|
+
if (!name) return sectionLines(HELP_GUIDE, width, 20);
|
|
300
|
+
if (name === "all" || name === "commands" || name === "reference")
|
|
301
|
+
return helpLines(width);
|
|
302
|
+
const group = HELP_SECTIONS.find((section) => section.group === name);
|
|
303
|
+
return group ? sectionLines([group], width) : undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function sectionLines(
|
|
307
|
+
sections: readonly HelpSection[],
|
|
308
|
+
width: number,
|
|
309
|
+
fixedColumn?: number,
|
|
310
|
+
): string[] {
|
|
311
|
+
const lines: string[] = [];
|
|
312
|
+
const column =
|
|
313
|
+
fixedColumn ??
|
|
314
|
+
Math.min(
|
|
315
|
+
40,
|
|
316
|
+
Math.max(
|
|
317
|
+
...sections.flatMap((s) => s.entries.map((e) => e.command.length)),
|
|
318
|
+
) + 2,
|
|
319
|
+
);
|
|
320
|
+
for (const section of sections) {
|
|
321
|
+
if (lines.length > 0) lines.push("");
|
|
322
|
+
lines.push(`── ${section.group}`);
|
|
323
|
+
for (const entry of section.entries) {
|
|
324
|
+
const pad = Math.max(1, column - entry.command.length);
|
|
325
|
+
lines.push(
|
|
326
|
+
`${entry.command}${" ".repeat(pad)}${entry.summary}`.slice(0, width),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return lines;
|
|
331
|
+
}
|
|
332
|
+
|
|
152
333
|
/** Overlay rows: a heading per group, then `command summary` lines. */
|
|
153
334
|
export function helpLines(width = 80): string[] {
|
|
154
335
|
const lines: string[] = [];
|
|
@@ -187,6 +368,9 @@ export function helpText(): string {
|
|
|
187
368
|
const USAGE: Readonly<Record<string, string>> = {
|
|
188
369
|
click: "/click on|off|<volume> · /click 50%",
|
|
189
370
|
"count-in": "/count-in 0|1|2",
|
|
371
|
+
chords:
|
|
372
|
+
"/chords auto|manual|off · voicing <n> · spread · bass · perform · rate · octaves · sevenths · preset · style",
|
|
373
|
+
key: "key <tonic> <mode> | none · key A minor",
|
|
190
374
|
grid: "/grid 1/4|1/8|1/8T|1/16|1/16T|1/32",
|
|
191
375
|
tempo: "tempo takes 20…300 · tempo 120",
|
|
192
376
|
bpm: "tempo takes 20…300 · tempo 120",
|
|
@@ -201,10 +385,13 @@ const USAGE: Readonly<Record<string, string>> = {
|
|
|
201
385
|
bars: "bars takes 1…256 · bars 8",
|
|
202
386
|
extend: "extend <count> bars · extend 4 bars",
|
|
203
387
|
instrument:
|
|
204
|
-
"instrument <name> · sine piano pluck bass saw square triangle kit",
|
|
388
|
+
"instrument <name> · sine piano pluck bass saw square triangle wavetable kit · synth: sawtooth supersaw pulse white pink z_square…",
|
|
205
389
|
volume: "volume takes 0…1 · volume 0.8",
|
|
206
390
|
vol: "volume takes 0…1 · volume 0.8",
|
|
207
391
|
pan: "pan takes -1…1 · pan -0.5",
|
|
392
|
+
wt: "wt <table> | wt <0..1> | wt list · wt basic · wt wt_digital:2",
|
|
393
|
+
wavetable: "wt <table> | wt <0..1> | wt list · wt basic · wt wt_digital:2",
|
|
394
|
+
warpmode: "warpmode none|asym|bendp|bendm|bendmp|sync|quant",
|
|
208
395
|
filter: "filter <hz> [res] · filter 800 0.3 · filter off",
|
|
209
396
|
delay: "delay <beats> [fb] [mix] · delay 0.75 0.4 0.3 · delay off",
|
|
210
397
|
reverb: "reverb <mix> [size] · reverb 0.3 0.6 · reverb off",
|
|
@@ -222,7 +409,8 @@ const USAGE: Readonly<Record<string, string>> = {
|
|
|
222
409
|
status: "/status",
|
|
223
410
|
export: "/export <file> · /export loop.track.json",
|
|
224
411
|
import: "/import <file> · /import loop.track.json",
|
|
225
|
-
sample:
|
|
412
|
+
sample:
|
|
413
|
+
"/sample <path> [as <voice>] · /sample set <voice> <control> <value>… · /sample set brk fit on clip 1",
|
|
226
414
|
samples: "/sample · lists the focused track's voices",
|
|
227
415
|
view: "/view focus | all",
|
|
228
416
|
transcript: "/transcript",
|
|
@@ -233,15 +421,115 @@ const USAGE: Readonly<Record<string, string>> = {
|
|
|
233
421
|
login: "/login [gateway | openrouter | codex | claude]",
|
|
234
422
|
logout: "/logout [provider]",
|
|
235
423
|
auth: "/auth [--check]",
|
|
236
|
-
help: "/help",
|
|
424
|
+
help: "/help [topic] · /help all · /help music|session|window|keys",
|
|
425
|
+
fx: "fx <effect> <param> <value> | on | off | preset <name> · fx delay mix 0.3",
|
|
426
|
+
synth: "synth <param> <value> | preset <name> · synth lpf 1200",
|
|
427
|
+
pack: "/pack list | info <name> | use <pack>/<sound> | add <url>",
|
|
428
|
+
kit: "/kit [name] · /kit syn909",
|
|
429
|
+
euclid: "/euclid [voice] · euclid hat 7 16",
|
|
430
|
+
menu: "/menu [sound|effects|rhythm|chords|mix|project]",
|
|
431
|
+
try: "/try <sound command> · /try fx reverb mix 0.6",
|
|
432
|
+
play: "/play [on|off] · Ctrl-P",
|
|
433
|
+
meter: "meter <1..16> · meter 3",
|
|
434
|
+
undo: "undo · Ctrl-Z",
|
|
435
|
+
redo: "redo · Ctrl-Y",
|
|
237
436
|
};
|
|
238
437
|
|
|
438
|
+
/** Verbs a typo can be matched against, slash or bare as they are typed. */
|
|
439
|
+
const KNOWN_VERBS: readonly string[] = [
|
|
440
|
+
...new Set(
|
|
441
|
+
[
|
|
442
|
+
...HELP_SECTIONS.flatMap((section) =>
|
|
443
|
+
section.group === "keys"
|
|
444
|
+
? []
|
|
445
|
+
: section.entries.map((entry) => entry.command.split(/[\s|[]/)[0]!),
|
|
446
|
+
),
|
|
447
|
+
...Object.values(USAGE).map((usage) => usage.split(/[\s|[]/)[0]!),
|
|
448
|
+
].filter((verb) => /^\/?[a-z][\w-]*$/i.test(verb)),
|
|
449
|
+
),
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
function editDistance(a: string, b: string): number {
|
|
453
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
454
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
455
|
+
let previous = row[0]!;
|
|
456
|
+
row[0] = i;
|
|
457
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
458
|
+
const current = row[j]!;
|
|
459
|
+
row[j] = Math.min(
|
|
460
|
+
row[j]! + 1,
|
|
461
|
+
row[j - 1]! + 1,
|
|
462
|
+
previous + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
463
|
+
);
|
|
464
|
+
previous = current;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return row[b.length]!;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The known command nearest to the first word of `command` (`/clik` →
|
|
472
|
+
* `/click`, `/fx` → `fx`), or undefined when nothing is close.
|
|
473
|
+
*/
|
|
474
|
+
export function nearestCommand(command: string): string | undefined {
|
|
475
|
+
const word = command.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
476
|
+
const bare = word.replace(/^\//, "");
|
|
477
|
+
if (!bare) return undefined;
|
|
478
|
+
let best: { verb: string; distance: number; score: number } | undefined;
|
|
479
|
+
for (const verb of KNOWN_VERBS) {
|
|
480
|
+
const distance = editDistance(bare, verb.replace(/^\//, ""));
|
|
481
|
+
if (distance === 0 && verb === word) continue;
|
|
482
|
+
// Ties go to the form typed: `/patern` → `/pattern`, not `pattern`.
|
|
483
|
+
const sameForm = verb.startsWith("/") === word.startsWith("/");
|
|
484
|
+
const score = distance * 2 + (sameForm ? 0 : 1);
|
|
485
|
+
if (!best || score < best.score) best = { verb, distance, score };
|
|
486
|
+
}
|
|
487
|
+
const limit = bare.length <= 4 ? 1 : 2;
|
|
488
|
+
return best && best.distance <= limit ? best.verb : undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* `tempoo 90` → `tempo 90`: the first word one edit from a bare command
|
|
493
|
+
* whose arguments then parse (`parses` decides). Slash words and inputs
|
|
494
|
+
* whose rest is not that command's arguments (prose) return undefined.
|
|
495
|
+
*/
|
|
496
|
+
export function typoFix(
|
|
497
|
+
command: string,
|
|
498
|
+
parses: (candidate: string) => boolean,
|
|
499
|
+
): string | undefined {
|
|
500
|
+
const text = command.trim();
|
|
501
|
+
if (!text || text.startsWith("/")) return undefined;
|
|
502
|
+
const [first, ...rest] = text.split(/\s+/);
|
|
503
|
+
const word = first!.toLowerCase();
|
|
504
|
+
for (const verb of KNOWN_VERBS) {
|
|
505
|
+
if (verb.startsWith("/") || verb === word) continue;
|
|
506
|
+
if (editDistance(word, verb) !== 1) continue;
|
|
507
|
+
const candidate = [verb, ...rest].join(" ");
|
|
508
|
+
if (parses(candidate)) return candidate;
|
|
509
|
+
}
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* True for a sentence that merely starts with a command verb (`add a walking
|
|
515
|
+
* bass in A minor`, `pan the hats left`): three or more words and no number
|
|
516
|
+
* or note name with an octave. Those are requests for the agent, not
|
|
517
|
+
* malformed commands. Slash words are never prose.
|
|
518
|
+
*/
|
|
519
|
+
export function looksLikeProse(command: string): boolean {
|
|
520
|
+
const text = command.trim();
|
|
521
|
+
if (text.startsWith("/")) return false;
|
|
522
|
+
const words = text.split(/\s+/);
|
|
523
|
+
return words.length >= 3 && !/\d/.test(text);
|
|
524
|
+
}
|
|
525
|
+
|
|
239
526
|
/**
|
|
240
527
|
* A usage hint when `command` starts with a known verb but did not parse
|
|
241
528
|
* (`pan 3`, `volume 2`, `add H4 at 0`, `/export` with no file); undefined for
|
|
242
529
|
* free text that should go to the agent.
|
|
243
530
|
*/
|
|
244
531
|
export function usageHint(command: string): string | undefined {
|
|
532
|
+
if (looksLikeProse(command)) return undefined;
|
|
245
533
|
const verb = command.trim().toLowerCase().replace(/^\//, "").split(/\s+/)[0];
|
|
246
534
|
return verb ? USAGE[verb] : undefined;
|
|
247
535
|
}
|
package/src/commands/music.ts
CHANGED
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
createScore,
|
|
27
27
|
updateTrack,
|
|
28
28
|
setTrackAutomation,
|
|
29
|
-
|
|
29
|
+
automationPoints,
|
|
30
|
+
automationRange,
|
|
31
|
+
isAutomationParameter,
|
|
30
32
|
type AutomationParameter,
|
|
31
33
|
type NoteInput,
|
|
32
34
|
type TrackScore,
|
|
@@ -96,8 +98,11 @@ const LANE_ALIASES: Readonly<Record<string, EffectAutomationParameter>> =
|
|
|
96
98
|
export function parseEffectLane(
|
|
97
99
|
name: string,
|
|
98
100
|
): EffectAutomationParameter | undefined {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
if (Object.prototype.hasOwnProperty.call(LANE_ALIASES, name))
|
|
102
|
+
return LANE_ALIASES[name];
|
|
103
|
+
// `fx` lanes by name: `distort-drive`, `autofilter-cutoff`, `reverb-mix`.
|
|
104
|
+
return isAutomationParameter(name) && name !== "volume" && name !== "pan"
|
|
105
|
+
? name
|
|
101
106
|
: undefined;
|
|
102
107
|
}
|
|
103
108
|
|
|
@@ -248,7 +253,7 @@ export function parseMusicCommand(prompt: string): MusicCommand | undefined {
|
|
|
248
253
|
const beat = Number(automate[2]);
|
|
249
254
|
const value = Number(automate[3]);
|
|
250
255
|
if (!parameter || !validBeat(beat)) return undefined;
|
|
251
|
-
const { min, max } =
|
|
256
|
+
const { min, max } = automationRange(parameter);
|
|
252
257
|
if (!Number.isFinite(value) || value < min || value > max) return undefined;
|
|
253
258
|
return { type: "effect-automation", parameter, beat, value };
|
|
254
259
|
}
|
|
@@ -332,10 +337,7 @@ export function applyMusicCommand(
|
|
|
332
337
|
if (command.type === "effect-automation") {
|
|
333
338
|
const tick = Math.round(command.beat * score.ticksPerBeat);
|
|
334
339
|
const merged = new Map(
|
|
335
|
-
(track
|
|
336
|
-
point.tick,
|
|
337
|
-
point,
|
|
338
|
-
]),
|
|
340
|
+
automationPoints(track, parameter).map((point) => [point.tick, point]),
|
|
339
341
|
);
|
|
340
342
|
merged.set(tick, { tick, value: command.value });
|
|
341
343
|
points = [...merged.values()].sort((a, b) => a.tick - b.tick);
|