@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/DAWG.md +605 -39
  3. package/README.md +4 -4
  4. package/core/chords.ts +1724 -0
  5. package/core/diff.ts +13 -1
  6. package/core/euclid.ts +670 -0
  7. package/core/fx.ts +1065 -0
  8. package/core/kits.ts +320 -0
  9. package/core/params.ts +111 -0
  10. package/core/rhythm.ts +287 -0
  11. package/core/score.ts +735 -18
  12. package/core/sdk/eval-child.ts +34 -22
  13. package/core/sdk/eval.ts +4 -1
  14. package/core/sdk/print.ts +304 -22
  15. package/core/sdk/sync-chords.ts +54 -0
  16. package/core/sdk/v1.ts +3226 -21
  17. package/core/synth.ts +1001 -0
  18. package/package.json +1 -1
  19. package/src/agent/agent.ts +38 -3
  20. package/src/agent/brief.ts +29 -4
  21. package/src/agent/chord-tools.ts +357 -0
  22. package/src/agent/drum-tools.ts +135 -0
  23. package/src/agent/models.ts +4 -4
  24. package/src/agent/pack-tools.ts +369 -0
  25. package/src/agent/planner.ts +17 -2
  26. package/src/agent/preview-tool.ts +270 -0
  27. package/src/agent/rhythm-tools.ts +145 -0
  28. package/src/agent/tools.ts +308 -14
  29. package/src/agent/xcb-agent.ts +8 -2
  30. package/src/audio/audition.ts +93 -0
  31. package/src/audio/cache.ts +160 -0
  32. package/src/audio/effects/bus.ts +147 -0
  33. package/src/audio/effects/chain.ts +99 -0
  34. package/src/audio/effects/common.ts +251 -0
  35. package/src/audio/effects/convolution.ts +301 -0
  36. package/src/audio/effects/drive.ts +142 -0
  37. package/src/audio/effects/duck.ts +122 -0
  38. package/src/audio/effects/dynamics.ts +121 -0
  39. package/src/audio/effects/filter.ts +319 -0
  40. package/src/audio/effects/modulation.ts +174 -0
  41. package/src/audio/effects/space.ts +337 -0
  42. package/src/audio/engine.ts +27 -4
  43. package/src/audio/kits.ts +200 -0
  44. package/src/audio/packs.ts +1787 -0
  45. package/src/audio/preview.ts +470 -0
  46. package/src/audio/random.ts +15 -0
  47. package/src/audio/sampler.ts +157 -29
  48. package/src/audio/samples.ts +386 -44
  49. package/src/audio/synth/oscillators.ts +268 -0
  50. package/src/audio/synth/voice.ts +555 -0
  51. package/src/audio/synth/zzfx.ts +137 -0
  52. package/src/audio/wav.ts +280 -278
  53. package/src/audio/wavetable-maker.ts +717 -0
  54. package/src/audio/wavetable.ts +624 -0
  55. package/src/commands/drums.ts +299 -0
  56. package/src/commands/edit.ts +26 -4
  57. package/src/commands/fx.ts +360 -0
  58. package/src/commands/help.ts +298 -10
  59. package/src/commands/music.ts +10 -8
  60. package/src/commands/pack.ts +423 -0
  61. package/src/commands/rhythm.ts +230 -0
  62. package/src/commands/sample.ts +162 -2
  63. package/src/commands/synth.ts +229 -0
  64. package/src/commands/wavetable.ts +370 -0
  65. package/src/main.ts +1119 -30
  66. package/src/media/cli.ts +15 -2
  67. package/src/media/tools.ts +106 -1
  68. package/src/media/wavetable.ts +202 -0
  69. package/src/render.ts +22 -1
  70. package/src/session/naming.ts +3 -1
  71. package/src/tui/audition.ts +501 -0
  72. package/src/tui/euclid.ts +472 -0
  73. package/src/tui/menu.ts +1286 -244
  74. package/src/tui/play-chords.ts +538 -0
  75. package/src/tui/play-session.ts +364 -16
  76. package/src/tui/sketch.ts +108 -0
  77. package/src/web/search.ts +4 -1
  78. package/tui/app.ts +161 -39
  79. package/tui/grammar.ts +281 -0
  80. package/tui/highway.ts +7 -1
  81. package/tui/play-strip.ts +59 -14
@@ -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: "edit by hand · Ctrl-K · track effects automation …",
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
- { command: "/help", summary: "this list · ?" },
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 · close an overlay" },
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: A–' W–P keys · Z/X octave · R rec · M click",
198
+ summary: "play mode · Z/X octave · R record · Q chords",
142
199
  },
143
200
  {
144
201
  command: "Ctrl-K",
145
- summary: "menu: ↑↓/jk move · enter · ←→/+- nudge · / filter",
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: "/sample <path> [as <voice>] · /sample kick.wav as kick",
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
  }
@@ -26,7 +26,9 @@ import {
26
26
  createScore,
27
27
  updateTrack,
28
28
  setTrackAutomation,
29
- AUTOMATION_LANES,
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
- return Object.prototype.hasOwnProperty.call(LANE_ALIASES, name)
100
- ? LANE_ALIASES[name]
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 } = AUTOMATION_LANES[parameter];
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[AUTOMATION_LANES[parameter].field] ?? []).map((point) => [
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);