@hraness/dawg 0.4.0 → 0.5.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 (116) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/DAWG.md +300 -35
  3. package/README.md +4 -4
  4. package/core/chords.ts +288 -7
  5. package/core/diff.ts +37 -12
  6. package/core/expression.ts +1241 -0
  7. package/core/loop.ts +23 -0
  8. package/core/master.ts +455 -0
  9. package/core/midi.ts +452 -0
  10. package/core/rhythm.ts +7 -2
  11. package/core/score.ts +536 -56
  12. package/core/sdk/eval-child.ts +38 -3
  13. package/core/sdk/print.ts +445 -14
  14. package/core/sdk/v1.ts +1709 -23
  15. package/core/sections.ts +2046 -0
  16. package/core/synth.ts +11 -1
  17. package/core/tempo.ts +1318 -0
  18. package/core/tuning.ts +1180 -0
  19. package/guides/audition.md +26 -0
  20. package/guides/automation.md +26 -0
  21. package/guides/chords.md +28 -0
  22. package/guides/effects.md +27 -0
  23. package/guides/faders.md +29 -0
  24. package/guides/files.md +28 -0
  25. package/guides/getting-started.md +26 -0
  26. package/guides/index.ts +75 -0
  27. package/guides/keys.md +27 -0
  28. package/guides/media.md +27 -0
  29. package/guides/mix.md +23 -0
  30. package/guides/music.md +14 -0
  31. package/guides/notes.md +26 -0
  32. package/guides/performance.md +26 -0
  33. package/guides/play.md +24 -0
  34. package/guides/project.md +13 -0
  35. package/guides/providers.md +25 -0
  36. package/guides/rhythm.md +26 -0
  37. package/guides/sessions.md +21 -0
  38. package/guides/sound.md +15 -0
  39. package/guides/sounds.md +25 -0
  40. package/guides/tempo.md +26 -0
  41. package/guides/tracks.md +25 -0
  42. package/guides/web-search.md +21 -0
  43. package/package.json +3 -1
  44. package/src/agent/agent.ts +2 -0
  45. package/src/agent/brief.ts +77 -3
  46. package/src/agent/chord-tools.ts +9 -1
  47. package/src/agent/expression-tools.ts +336 -0
  48. package/src/agent/master-tools.ts +299 -0
  49. package/src/agent/models.ts +4 -4
  50. package/src/agent/ops.ts +29 -4
  51. package/src/agent/planner.ts +45 -2
  52. package/src/agent/preview-tool.ts +21 -3
  53. package/src/agent/section-tools.ts +411 -0
  54. package/src/agent/time-tools.ts +290 -0
  55. package/src/agent/tools.ts +36 -2
  56. package/src/agent/tuning-tools.ts +301 -0
  57. package/src/agent/xcb-agent.ts +2 -0
  58. package/src/audio/arrange.ts +471 -0
  59. package/src/audio/audition.ts +16 -3
  60. package/src/audio/click.ts +113 -1
  61. package/src/audio/clock.ts +71 -5
  62. package/src/audio/effects/bus.ts +5 -4
  63. package/src/audio/effects/chain.ts +12 -2
  64. package/src/audio/effects/common.ts +30 -1
  65. package/src/audio/effects/convolution.ts +43 -5
  66. package/src/audio/effects/dynamics.ts +2 -1
  67. package/src/audio/effects/filter.ts +5 -3
  68. package/src/audio/effects/modulation.ts +5 -1
  69. package/src/audio/effects/space.ts +197 -50
  70. package/src/audio/engine.ts +175 -20
  71. package/src/audio/live.ts +33 -6
  72. package/src/audio/loudness.ts +551 -0
  73. package/src/audio/master.ts +660 -0
  74. package/src/audio/measure-worker.ts +45 -0
  75. package/src/audio/measure.ts +110 -0
  76. package/src/audio/player.ts +11 -6
  77. package/src/audio/preview.ts +87 -16
  78. package/src/audio/render-worker.ts +6 -1
  79. package/src/audio/renderer.ts +7 -1
  80. package/src/audio/sampler.ts +108 -19
  81. package/src/audio/synth/voice.ts +60 -13
  82. package/src/audio/synth/zzfx.ts +10 -4
  83. package/src/audio/warp.ts +86 -0
  84. package/src/audio/wav.ts +448 -59
  85. package/src/commands/arrange.ts +949 -0
  86. package/src/commands/expression.ts +934 -0
  87. package/src/commands/help.ts +281 -24
  88. package/src/commands/master.ts +361 -0
  89. package/src/commands/music.ts +6 -1
  90. package/src/commands/synth.ts +11 -1
  91. package/src/commands/time.ts +964 -0
  92. package/src/commands/tuning.ts +490 -0
  93. package/src/main.ts +826 -47
  94. package/src/render.ts +109 -7
  95. package/src/session/daemon.ts +18 -8
  96. package/src/session/naming.ts +8 -1
  97. package/src/session/rebase.ts +21 -0
  98. package/src/tui/arrange-menu.ts +390 -0
  99. package/src/tui/audition.ts +58 -5
  100. package/src/tui/euclid.ts +50 -6
  101. package/src/tui/fader.ts +409 -0
  102. package/src/tui/menu-time.ts +401 -0
  103. package/src/tui/menu.ts +656 -16
  104. package/src/tui/performance-menu.ts +235 -0
  105. package/src/tui/play-chords.ts +95 -1
  106. package/src/tui/play-mode.ts +150 -6
  107. package/src/tui/play-session.ts +414 -41
  108. package/tui/app.ts +262 -13
  109. package/tui/arrange-strip.ts +174 -0
  110. package/tui/drawer.ts +478 -0
  111. package/tui/grammar.ts +79 -12
  112. package/tui/guide.ts +351 -0
  113. package/tui/highway.ts +87 -4
  114. package/tui/hits.ts +68 -0
  115. package/tui/input.ts +7 -0
  116. package/tui/keys.ts +72 -0
@@ -4,6 +4,7 @@
4
4
  * app commands take a slash (bare aliases keep working but are listed once,
5
5
  * in their canonical form).
6
6
  */
7
+ import { EXPRESSION_USAGE } from "./expression.ts";
7
8
 
8
9
  export type HelpGroup = "music" | "session" | "window" | "keys";
9
10
 
@@ -60,6 +61,38 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
60
61
  command: "synth <param> <value> | preset <name>",
61
62
  summary: "synth voice · synth lpf 1200 · synth lists every param",
62
63
  },
64
+ {
65
+ command: "art <articulation>|off [target]",
66
+ summary:
67
+ "staccato legato accent tenuto marcato ghost · art staccato bars 1-2",
68
+ },
69
+ {
70
+ command: "glide <ms>|0 [legato|mono|poly] | glide <ms> <target>",
71
+ summary:
72
+ "portamento in ms · glide 60 mono · glide 0 off · SDK 0.06 (s)",
73
+ },
74
+ {
75
+ command: "bend <cents>|scoop|fall|doit|<at:cents>... [target]",
76
+ summary: "pitch curve over each note · bend +200 bar 3",
77
+ },
78
+ {
79
+ command: "vibrato <rate> <depth> [<delay>] [target]",
80
+ summary: "per-note vibrato · vibrato 5.5 30 0.2",
81
+ },
82
+ {
83
+ command: "pedal <beat>-<beat>... | bars | down|half|up <beat> | off",
84
+ summary: "sustain pedal · pedal 0-3.5 4-7.5 · pedal bars",
85
+ },
86
+ {
87
+ command: "velcurve linear|soft|hard|fixed [<v>]",
88
+ summary: "how velocity maps to level · velcurve fixed 0.6 (0..1)",
89
+ },
90
+ {
91
+ command: "humanize <ms> [<vel%> [<len%>]] [seed <n>|<target>] | off",
92
+ summary:
93
+ "seeded feel at render · humanize 10 8 5 · humanize 20 bars 2-3",
94
+ },
95
+ { command: "expression", summary: "this track's performance settings" },
63
96
  { command: "filter <hz> [res]", summary: "low-pass · filter off" },
64
97
  { command: "delay <beats> [fb] [mix]", summary: "ping-pong · delay off" },
65
98
  { command: "reverb <mix> [size]", summary: "room · reverb off" },
@@ -76,12 +109,82 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
76
109
  summary: "drop one point",
77
110
  },
78
111
  { command: "clear [<lane>] automation", summary: "drop a lane's points" },
112
+ {
113
+ command: "master <unit> on|off | preset <name> | <param> <value>",
114
+ summary:
115
+ "song master · eq glue tape width limiter · master glue ratio 4",
116
+ },
117
+ {
118
+ command: "master <target> | target <lufs> | measure | off",
119
+ summary:
120
+ "loudness · master streaming · master target -9 · master measure",
121
+ },
79
122
  { command: "track name <text>", summary: "rename this track" },
80
123
  { command: "meter <1..16>", summary: "beats per bar" },
124
+ {
125
+ command: "tempo <bpm> at <beat>|bar <n> [ramp|exp]",
126
+ summary: "tempo change · tempo 90 at bar 9 ramp · tempo clear",
127
+ },
128
+ {
129
+ command: "rit|accel [<n> bars] [to <bpm>] [at bar <n>]",
130
+ summary: "gradual · rit 4 bars to 80 · a tempo · tempo primo",
131
+ },
132
+ {
133
+ command: "fermata [at <beat>|at bar <n>|at end] [<extra beats>]",
134
+ summary: "hold a beat · fermata at 31 2 · fermata clear",
135
+ },
136
+ {
137
+ command: "meter <n>/<d> [at bar <n>]",
138
+ summary: "meter change · meter 7/8 at bar 5 · meter clear",
139
+ },
140
+ {
141
+ command: "track rate|phase|cycle <n> | off",
142
+ summary: "polytempo · track rate 3/2 · track cycle 3",
143
+ },
144
+ {
145
+ command: "track phasing <beats> [over <beats>|hold <n>]",
146
+ summary: "Reich phasing · track phasing 3 hold 8 · track time off",
147
+ },
81
148
  {
82
149
  command: "key <tonic> <mode> | none",
83
150
  summary: "song key · key A minor · key F# dorian",
84
151
  },
152
+ {
153
+ command: "scale [<tonic>] <name> | list",
154
+ summary: "song scale · scale D hijaz · scale yaman · scale list",
155
+ },
156
+ {
157
+ command: "tuning <name> | edo <n> | scl <file> | off",
158
+ summary: "song tuning · tuning 19-edo · tuning just · tuning list",
159
+ },
160
+ {
161
+ command: "tuning ref <hz> | root <note> | map linear|nearest",
162
+ summary: "A4 reference · degree-0 key · keys per step",
163
+ },
164
+ {
165
+ command: "tuning track <…> | track off",
166
+ summary: "this track's tuning · off follows the song",
167
+ },
168
+ {
169
+ command: "cents <id> <±c>",
170
+ summary: "detune one note · cents n3 -14 · add E4-14c at 0",
171
+ },
172
+ {
173
+ command: "section <name> <a>-<b> | add | dup | move | rename | delete",
174
+ summary: "song sections · section chorus 9-16 · section lists them",
175
+ },
176
+ {
177
+ command: "section loop | jump | mute | vary <name>",
178
+ summary: "section loop chorus · section mute verse drums",
179
+ },
180
+ {
181
+ command: "form <section…> | off | bake",
182
+ summary: "song form · form intro verse chorus*2 outro",
183
+ },
184
+ {
185
+ command: "build | drop | fill [<section> | <a>-<b>]",
186
+ summary: "riser, roll, sweep · pre-drop cut and impact · drum fill",
187
+ },
85
188
  {
86
189
  command: "hit <voice> at <beat>",
87
190
  summary: "kit tracks · hit kick at 0",
@@ -112,7 +215,10 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
112
215
  { command: "/rename <name>|--auto", summary: "name this session" },
113
216
  { command: "/fork [<name>]", summary: "copy into a new session" },
114
217
  { command: "/status", summary: "name · revision · digest · storage" },
115
- { command: "/export <file>", summary: "write track.loop/v1 JSON" },
218
+ {
219
+ command: "/export <file>",
220
+ summary: "write track.loop/v1 JSON, or MIDI for .mid",
221
+ },
116
222
  { command: "/import <file>", summary: "replace the score from a file" },
117
223
  {
118
224
  command: "/sample [<path> [as <voice>]]",
@@ -143,6 +249,10 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
143
249
  },
144
250
  { command: "/auth [--check]", summary: "provider and audio status" },
145
251
  { command: "/play [on|off]", summary: "keyboard play mode · Ctrl-P" },
252
+ {
253
+ command: "/play degrees|chromatic",
254
+ summary: "home row plays the key's scale degrees (any tuning) · i",
255
+ },
146
256
  {
147
257
  command: "/pattern [name]",
148
258
  summary: "drum groove picker · moving previews",
@@ -179,6 +289,10 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
179
289
  command: "/help [topic]",
180
290
  summary: "start here · /help all for everything",
181
291
  },
292
+ {
293
+ command: "/guide [topic]",
294
+ summary: "short how-to guides · F1",
295
+ },
182
296
  ],
183
297
  },
184
298
  {
@@ -219,13 +333,9 @@ export const HELP_GUIDE: readonly HelpSection[] = [
219
333
  { command: "ctrl-p", summary: "play notes on the computer keyboard" },
220
334
  {
221
335
  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)",
336
+ summary: "menu: sound, effects, performance, mix, master, arrange …",
228
337
  },
338
+ { command: "? · ctrl-z · ctrl-y", summary: "keys here · undo · redo" },
229
339
  ],
230
340
  },
231
341
  {
@@ -244,8 +354,10 @@ export const HELP_GUIDE: readonly HelpSection[] = [
244
354
  {
245
355
  group: "make drums" as HelpGroup,
246
356
  entries: [
247
- { command: "/pattern", summary: "pick a groove · moving previews it" },
248
- { command: "/kit", summary: "pick a drum kit" },
357
+ {
358
+ command: "/pattern · /kit",
359
+ summary: "pick a groove · pick a drum kit",
360
+ },
249
361
  { command: "/euclid", summary: "rhythm editor: pulses, steps, rotation" },
250
362
  ],
251
363
  },
@@ -257,35 +369,68 @@ export const HELP_GUIDE: readonly HelpSection[] = [
257
369
  summary: "instrument, envelope, filter, wavetable",
258
370
  },
259
371
  { command: "ctrl-k › Effects", summary: "delay, reverb, distortion …" },
260
- { command: "fx delay mix 0.3", summary: "the same from the prompt" },
372
+ {
373
+ command: "master streaming",
374
+ summary: "finish: -14 LUFS, limiter · master measure",
375
+ },
261
376
  { command: "/try fx reverb mix 0.6", summary: "hear it before keeping" },
262
377
  ],
263
378
  },
379
+ {
380
+ group: "shape the performance" as HelpGroup,
381
+ entries: [
382
+ {
383
+ command: "art staccato bar 2",
384
+ summary: "humanize 8 5 · ctrl-k › Sound › performance",
385
+ },
386
+ ],
387
+ },
264
388
  {
265
389
  group: "chords" as HelpGroup,
266
390
  entries: [
267
391
  { command: "key A minor", summary: "set the song key" },
268
392
  {
269
393
  command: "ctrl-p, then q",
270
- summary: "chord mode: one key plays a chord",
394
+ summary: "chord mode · 1–4 type · 5–8 extension · n next",
271
395
  },
396
+ ],
397
+ },
398
+ {
399
+ group: "song structure" as HelpGroup,
400
+ entries: [
272
401
  {
273
- command: "1–4 · 5–8 · n",
274
- summary: "chord type · extension · next chord",
402
+ command: "section verse 1-8",
403
+ summary: "name bars · form verse chorus*2 orders them",
404
+ },
405
+ {
406
+ command: "build · drop · fill",
407
+ summary: "transitions · ctrl-k › Arrange · /help arrange",
275
408
  },
276
409
  ],
277
410
  },
278
411
  {
279
412
  group: "more" as HelpGroup,
280
413
  entries: [
281
- { command: "/help all", summary: "every command and key" },
282
- { command: "/help music", summary: "or session, window, keys" },
414
+ {
415
+ command: "rit 4 bars to 80",
416
+ summary: "tempo, fermatas, meter · tuning pelog · scale yaman",
417
+ },
418
+ {
419
+ command: "/help all",
420
+ summary: "every command and key · /guide feature guides · F1",
421
+ },
283
422
  ],
284
423
  },
285
424
  ];
286
425
 
287
426
  /** Topics `/help <topic>` takes, besides `all`. */
288
- export const HELP_TOPICS = ["music", "session", "window", "keys"] as const;
427
+ export const HELP_TOPICS = [
428
+ "music",
429
+ "session",
430
+ "window",
431
+ "keys",
432
+ "arrange",
433
+ ] as const;
289
434
 
290
435
  /**
291
436
  * Rows for `/help [topic]`: the guide with no topic, the full reference for
@@ -299,10 +444,47 @@ export function helpTopicLines(
299
444
  if (!name) return sectionLines(HELP_GUIDE, width, 20);
300
445
  if (name === "all" || name === "commands" || name === "reference")
301
446
  return helpLines(width);
447
+ if (name === "arrange" || name === "arrangement" || name === "sections")
448
+ return sectionLines([arrangeSection()], width);
302
449
  const group = HELP_SECTIONS.find((section) => section.group === name);
303
450
  return group ? sectionLines([group], width) : undefined;
304
451
  }
305
452
 
453
+ /** `/help arrange`: every arranging command with its full usage. */
454
+ function arrangeSection(): HelpSection {
455
+ const verbs = ["section", "sections", "form", "build", "drop", "fill"];
456
+ return {
457
+ group: "arrange" as HelpGroup,
458
+ entries: [
459
+ // Usage strings run long: wrapped onto continuation rows.
460
+ ...verbs.flatMap((verb) =>
461
+ wrapWords(USAGE[verb] ?? "", 46).map((summary, index) => ({
462
+ command: index === 0 ? verb : "",
463
+ summary,
464
+ })),
465
+ ),
466
+ { command: "/menu arrange", summary: "the Arrange menu (ctrl-k)" },
467
+ {
468
+ command: "dawg render --section <name>",
469
+ summary: "export one section",
470
+ },
471
+ ],
472
+ };
473
+ }
474
+
475
+ function wrapWords(text: string, width: number): string[] {
476
+ const rows: string[] = [];
477
+ let row = "";
478
+ for (const word of text.split(" ")) {
479
+ if (row && row.length + 1 + word.length > width) {
480
+ rows.push(row);
481
+ row = word;
482
+ } else row = row ? `${row} ${word}` : word;
483
+ }
484
+ if (row) rows.push(row);
485
+ return rows;
486
+ }
487
+
306
488
  function sectionLines(
307
489
  sections: readonly HelpSection[],
308
490
  width: number,
@@ -365,15 +547,31 @@ export function helpText(): string {
365
547
  }
366
548
 
367
549
  /** Usage for a known verb, shown instead of sending a near-miss to the agent. */
368
- const USAGE: Readonly<Record<string, string>> = {
550
+ export const USAGE: Readonly<Record<string, string>> = {
369
551
  click: "/click on|off|<volume> · /click 50%",
370
552
  "count-in": "/count-in 0|1|2",
371
553
  chords:
372
554
  "/chords auto|manual|off · voicing <n> · spread · bass · perform · rate · octaves · sevenths · preset · style",
373
555
  key: "key <tonic> <mode> | none · key A minor",
556
+ scale: "scale [<tonic>] <name> | list · scale D hijaz",
557
+ tuning:
558
+ "tuning <name> | edo <n> | ratios … | cents … | scl <file> [kbm <file>] | ref <hz> | root <note> | map linear|nearest | track … | off · tuning 19-edo",
559
+ tune: "tuning <name> | edo <n> | scl <file> | off · tuning list",
560
+ cents: "cents <id> <±cents> · cents n3 -14",
374
561
  grid: "/grid 1/4|1/8|1/8T|1/16|1/16T|1/32",
375
- tempo: "tempo takes 20…300 · tempo 120",
562
+ tempo:
563
+ "tempo takes 20…300 · tempo 120 · tempo 90 at bar 9 [ramp|exp] · tempo remove bar 9 · tempo clear · tempo map",
376
564
  bpm: "tempo takes 20…300 · tempo 120",
565
+ rit: "rit [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · rit 4 bars to 80",
566
+ ritardando:
567
+ "rit [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · rit 4 bars to 80",
568
+ rall: "rit [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · rall 2 bars",
569
+ accel:
570
+ "accel [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · accel 8 bars to 174 at bar 9",
571
+ accelerando:
572
+ "accel [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · accel 8 bars to 174",
573
+ fermata:
574
+ "fermata [at <beat>|at bar <n>|at end] [<extra beats>] · fermata at 31 2 · fermata remove 31 · fermata clear",
377
575
  add: "add <note> at <beat> [for <beats>] · add C4 at 0",
378
576
  put: "add <note> at <beat> [for <beats>] · add C4 at 0",
379
577
  remove: "remove <id> · ids show in the transcript",
@@ -400,7 +598,8 @@ const USAGE: Readonly<Record<string, string>> = {
400
598
  hit: "hit <voice> at <beat> · hit kick at 0",
401
599
  pattern: "pattern <voice> <beats...> | every <step> · pattern kick every 1",
402
600
  clear: "clear · clear <voice> · clear [<lane>] automation",
403
- track: "/track <name> · /track drums",
601
+ track:
602
+ "/track <name> · /track drums · track rate <0.125..8>|<a>/<b>|off · track phase <beats> · track cycle <beats> · track phasing <beats> [over <beats>]",
404
603
  tracks: "/tracks",
405
604
  sessions: "/sessions",
406
605
  resume: "/resume [<n>|<name>|<id>]",
@@ -422,15 +621,37 @@ const USAGE: Readonly<Record<string, string>> = {
422
621
  logout: "/logout [provider]",
423
622
  auth: "/auth [--check]",
424
623
  help: "/help [topic] · /help all · /help music|session|window|keys",
624
+ guide: "/guide [topic] · /guide chords · F1",
425
625
  fx: "fx <effect> <param> <value> | on | off | preset <name> · fx delay mix 0.3",
426
626
  synth: "synth <param> <value> | preset <name> · synth lpf 1200",
427
627
  pack: "/pack list | info <name> | use <pack>/<sound> | add <url>",
428
628
  kit: "/kit [name] · /kit syn909",
429
629
  euclid: "/euclid [voice] · euclid hat 7 16",
430
- menu: "/menu [sound|effects|rhythm|chords|mix|project]",
630
+ menu: "/menu [sound|effects|rhythm|chords|mix|master|project|tuning]",
631
+ master:
632
+ "master <unit> on|off|preset <name>|<param> <value> · master streaming|club|loud · master target -14 · master measure · master off",
431
633
  try: "/try <sound command> · /try fx reverb mix 0.6",
432
- play: "/play [on|off] · Ctrl-P",
433
- meter: "meter <1..16> · meter 3",
634
+ play: "/play [on|off|degrees|chromatic] · Ctrl-P · i toggles degrees",
635
+ meter:
636
+ "meter <1..16> · meter 3 · meter 7/8 [at bar <n>] · meter remove bar <n> · meter clear",
637
+ art: EXPRESSION_USAGE.art,
638
+ articulation: EXPRESSION_USAGE.art,
639
+ bend: EXPRESSION_USAGE.bend,
640
+ vibrato: EXPRESSION_USAGE.vibrato,
641
+ glide: EXPRESSION_USAGE.glide,
642
+ portamento: EXPRESSION_USAGE.glide,
643
+ pedal: EXPRESSION_USAGE.pedal,
644
+ sustain: EXPRESSION_USAGE.pedal,
645
+ velcurve: EXPRESSION_USAGE.velcurve,
646
+ humanize: EXPRESSION_USAGE.humanize,
647
+ section:
648
+ "section [mark] <name> <a>-<b> | add [<name>] [<n>] | dup | move <name> to <bar> | rename <name> to <new> | delete | unmark | mute | vary | reset | loop <name>|off | jump <name> · section chorus 9-16",
649
+ sections: "section · lists sections, the form and the loop",
650
+ form: "form <section…> | off | bake · form verse verse chorus verse · form verse chorus*2",
651
+ build:
652
+ "build [into <section> | <section> | <a>-<b>] [<n> bars] [riser] [roll] [sweep] [uplifter] · build into chorus",
653
+ drop: "drop [<section> | at <bar>] [cut <beats>] [no impact] · drop chorus",
654
+ fill: "fill [<section> | at <bar>] [toms|roll|kick] [<n> beats] [no crash] · fill chorus (the beats before it)",
434
655
  undo: "undo · Ctrl-Z",
435
656
  redo: "redo · Ctrl-Y",
436
657
  };
@@ -530,6 +751,42 @@ export function looksLikeProse(command: string): boolean {
530
751
  */
531
752
  export function usageHint(command: string): string | undefined {
532
753
  if (looksLikeProse(command)) return undefined;
533
- const verb = command.trim().toLowerCase().replace(/^\//, "").split(/\s+/)[0];
534
- return verb ? USAGE[verb] : undefined;
754
+ const words = command.trim().toLowerCase().replace(/^\//, "").split(/\s+/);
755
+ const verb = words[0];
756
+ if (!verb) return undefined;
757
+ // `build tension`, `drop bass`, `form a hook`: everyday words, so only
758
+ // arguments that look like the grammar earn a usage hint; the rest is
759
+ // a request for the agent.
760
+ if (
761
+ ARRANGE_VERBS.has(verb) &&
762
+ !command.trim().startsWith("/") &&
763
+ !words.slice(1).some((word) => /\d/.test(word) || ARRANGE_WORDS.has(word))
764
+ )
765
+ return undefined;
766
+ return USAGE[verb];
535
767
  }
768
+
769
+ const ARRANGE_VERBS: ReadonlySet<string> = new Set([
770
+ "build",
771
+ "drop",
772
+ "fill",
773
+ "form",
774
+ ]);
775
+ const ARRANGE_WORDS: ReadonlySet<string> = new Set([
776
+ "at",
777
+ "into",
778
+ "cut",
779
+ "no",
780
+ "impact",
781
+ "crash",
782
+ "riser",
783
+ "roll",
784
+ "sweep",
785
+ "uplifter",
786
+ "toms",
787
+ "kick",
788
+ "beats",
789
+ "bars",
790
+ "bake",
791
+ "off",
792
+ ]);