@hraness/dawg 0.6.0 → 0.7.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 (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -1,12 +1,29 @@
1
1
  /**
2
2
  * The command reference, in one place: `/help` renders it in the overlay,
3
- * `dawg --help` prints it, and the docs mirror it. Music words are bare;
4
- * app commands take a slash (bare aliases keep working but are listed once,
5
- * in their canonical form).
3
+ * `dawg --help` prints it, and the docs mirror it. Groups are the ten topic
4
+ * ids (src/lang/glossary.ts), the same ids `/guide` and `/menu` take.
5
+ * A topic may have sub-groups (`sound · performance`) so no group runs
6
+ * past about fifteen rows. Commands are written bare, since the slash is
7
+ * optional; only the free-text window verbs (`/rename`, `/fork`,
8
+ * `/resume`), `/auth`, `/bpm` (a sample's tempo; bare `bpm` is song tempo)
9
+ * and `/play` (play mode; bare `play` is the transport) keep it.
6
10
  */
11
+ import { GUITAR_TUNING_NAMES } from "../../core/chords.ts";
12
+ import { KEYS, type KeyScreen } from "../../tui/grammar.ts";
13
+ import { displayWidth, truncate } from "../../tui/text.ts";
14
+ import {
15
+ resolveTopic,
16
+ topicMiss,
17
+ TOPIC_SUMMARY,
18
+ TOPICS,
19
+ type TopicId,
20
+ } from "../lang/glossary.ts";
7
21
  import { EXPRESSION_USAGE } from "./expression.ts";
22
+ import { editDistance } from "./nearest.ts";
23
+ import { VOCAL_VERBS } from "./vocal.ts";
8
24
 
9
- export type HelpGroup = "music" | "session" | "window" | "keys";
25
+ /** A topic id, or a heading of the start page. */
26
+ export type HelpGroup = string;
10
27
 
11
28
  export type HelpEntry = Readonly<{
12
29
  /** Canonical form, e.g. `pan <-1..1>`. */
@@ -22,29 +39,16 @@ export type HelpSection = Readonly<{
22
39
 
23
40
  export const HELP_SECTIONS: readonly HelpSection[] = [
24
41
  {
25
- group: "music",
42
+ group: "sound",
26
43
  entries: [
27
- { command: "play", summary: "start the transport" },
28
- { command: "pause", summary: "stop the transport" },
29
- { command: "tempo <bpm>", summary: "20–300 BPM" },
30
- { command: "add <note> at <beat> [for <beats>]", summary: "add C4 at 0" },
31
- { command: "remove <id>", summary: "delete a note" },
32
- { command: "move <id> to <beat>", summary: "shift a note" },
33
- { command: "length <id> <beats>", summary: "resize a note" },
34
- { command: "velocity <id> <0..1>", summary: "note loudness" },
35
- { command: "bars <count>", summary: "loop length, 1–256" },
36
- { command: "extend <count> bars", summary: "lengthen the loop" },
37
44
  {
38
45
  command: "instrument <name>",
39
- summary: "sine piano pluck bass saw square triangle wavetable kit",
46
+ summary: "sine piano pluck bass sawtooth square triangle wavetable kit",
47
+ },
48
+ {
49
+ command: "synth <param> <value> | preset <name>",
50
+ summary: "the synth · synth lpf 1200 · synth alone lists every param",
40
51
  },
41
- { command: "volume <0..1>", summary: "track level" },
42
- { command: "pan <-1..1>", summary: "left … right" },
43
- { command: "mute", summary: "silence this track" },
44
- { command: "unmute", summary: "hear it again" },
45
- { command: "solo", summary: "only this track" },
46
- { command: "unsolo", summary: "every track again" },
47
- { command: "clear", summary: "remove this track's notes" },
48
52
  {
49
53
  command: "wt <table> | wt <0..1> | wt list",
50
54
  summary: "wavetable synth · wt basic · wt wt_digital:2 · wt 0.5",
@@ -55,51 +59,68 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
55
59
  },
56
60
  {
57
61
  command:
58
- "grain <preset> | <param> <value> | on [voice V] | src synth:<name>|voice V | reset | off",
62
+ "grain <preset> | <param> <value> | on [voice V] | src synth:<name>|voice V",
59
63
  summary:
60
64
  "granular cloud · grain cloud · grain scan 0.2 · track swarm-2 · grain presets",
61
65
  },
62
66
  {
63
- command: "fx <effect> <param> <value> | on | off | preset <name>",
64
- summary: "effects · fx delay mix 0.3 · fx reverb on · fx lists them",
67
+ command: "piano [<preset>] | grand | upright | felt | honkytonk",
68
+ summary: "modeled piano · piano ballad · piano lofi · piano prepared",
65
69
  },
66
70
  {
67
- command: "synth <param> <value> | preset <name>",
68
- summary: "synth voice · synth lpf 1200 · synth lists every param",
71
+ command: "keys <param> <value> | preset <name> | reset",
72
+ summary: "piano params · keys hardness 0.3 decay 1.5 · keys lists them",
69
73
  },
70
74
  {
71
- command: "string <preset> | <param> <value> | presets | reset | off",
75
+ command: "epiano|wurli|clav [preset <name>] | <param> <value>",
72
76
  summary:
73
- "plucked strings · string sitar · string buzz 0.8 · string ring 6",
77
+ "electric keys · suitcase dyno funkclav · epiano vibe 0.6 · clav pickup bridge",
74
78
  },
75
79
  {
76
- command: "rig <preset> | reset",
80
+ command:
81
+ "tonewheel [<drawbars>] | combo [<tabs>] [<register>] | pipe [<stops>]",
77
82
  summary:
78
- "guitar rig · rig crunch · rig metal · rig funk · rig lists them",
83
+ "organs · tonewheel 888800008 perc 3rd · gospel · combo 08880 flute · pipe plenum · keys perc 3rd",
79
84
  },
80
85
  {
81
- command: "track jangle|punk|funk|ragged|gtr-lead|gtr-metal|bachata",
82
- summary: "new guitar track with that rig",
86
+ command: "rotary slow|fast|stop [at <beat>]",
87
+ summary:
88
+ "organ rotary speaker speed · rotary fast at 16 writes the keys-rotary lane",
83
89
  },
84
90
  {
85
- command: "stomp|head|cab <type> | <param> <value>",
91
+ command: "string <preset> | <param> <value> | presets | reset | off",
86
92
  summary:
87
- "pedal, amp, cabinet · stomp rat · head gain 7 gate -55 · cab 4x12",
93
+ "plucked strings · string sitar · string buzz 0.8 · string ring 6",
88
94
  },
89
95
  {
90
- command: "piano [<preset>] | grand | upright | felt | honkytonk",
91
- summary: "modelled piano · piano ballad · piano lofi · piano prepared",
96
+ command: "bowed [<preset>] | <param> <value> | presets",
97
+ summary:
98
+ "bowed strings · bowed violin · bowed cellos · bowed pressure 0.7 · bowed sord 1",
92
99
  },
93
100
  {
94
- command: "keys <param> <value> | preset <name> | reset",
95
- summary: "piano params · keys hardness 0.3 decay 1.5 · keys lists them",
101
+ command:
102
+ "guitar tune <name|notes> | capo | hand | ring | position | reset",
103
+ summary: "guitar fretting · guitar tune dadgad · guitar capo 2",
96
104
  },
97
105
  {
98
106
  command:
99
107
  "modal <preset> | <param> <value> | mallet <name> | reset | off",
108
+ summary: "mallets · modal vibes · modal hardness 0.8 · modal presets",
109
+ },
110
+ {
111
+ command: "wind <preset> | <param> <value> | mute <name> | reset | off",
100
112
  summary:
101
- "mallets and bells · modal vibes · modal hardness 0.8 · modal presets",
113
+ "winds and brass · wind flute · wind sax · wind players 4 · wind presets",
102
114
  },
115
+ {
116
+ command: "/try <sound command>",
117
+ summary: "hear it on a loop first · a A/B · enter keep",
118
+ },
119
+ ],
120
+ },
121
+ {
122
+ group: "sound · performance",
123
+ entries: [
103
124
  {
104
125
  command: "art <articulation>|off [target]",
105
126
  summary:
@@ -122,6 +143,11 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
122
143
  command: "pedal <beat>-<beat>... | bars | down|half|up <beat> | off",
123
144
  summary: "sustain pedal · pedal 0-3.5 4-7.5 · pedal bars",
124
145
  },
146
+ {
147
+ command: "pedal soft|sost <beat>-<beat>... | bars | off",
148
+ summary:
149
+ "piano una corda and sostenuto · pedal soft 0-8 · pedal sost 0-4",
150
+ },
125
151
  {
126
152
  command: "velcurve linear|soft|hard|fixed [<v>]",
127
153
  summary: "how velocity maps to level · velcurve fixed 0.6 (0..1)",
@@ -132,361 +158,511 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
132
158
  "seeded feel at render · humanize 10 8 5 · humanize 20 bars 2-3",
133
159
  },
134
160
  { command: "expression", summary: "this track's performance settings" },
135
- { command: "filter <hz> [res]", summary: "low-pass · filter off" },
136
- { command: "delay <beats> [fb] [mix]", summary: "ping-pong · delay off" },
137
- { command: "reverb <mix> [size]", summary: "room · reverb off" },
161
+ ],
162
+ },
163
+ {
164
+ group: "sound · samples",
165
+ entries: [
138
166
  {
139
- command: "automate <lane> at <beat> <value>",
140
- summary: "volume pan filter resonance delay-feedback delay-mix wt",
167
+ command: "/sample [<path> [as <sample>]]",
168
+ summary: "add a sample · alone lists the samples",
141
169
  },
142
170
  {
143
- command: "automate <lane> points <b:v>...",
144
- summary: "several points · automate pan points 0:-1 4:1",
171
+ command: "/bpm <n> [<sample>]",
172
+ summary: "a sample's own tempo (the slash matters: bare bpm is tempo)",
145
173
  },
146
174
  {
147
- command: "automate <lane> remove <beat>",
148
- summary: "drop one point",
175
+ command: "fitmode [repitch|beats|tones|auto] [<sample>]",
176
+ summary: "how it fits · alone suggests one from the sound",
149
177
  },
150
- { command: "clear [<lane>] automation", summary: "drop a lane's points" },
151
178
  {
152
- command: "master <unit> on|off | preset <name> | <param> <value>",
153
- summary:
154
- "song master · eq glue tape width limiter · master glue ratio 4",
179
+ command: "len <beats> [<sample>]",
180
+ summary: "the sample lasts n beats of the song",
155
181
  },
156
182
  {
157
- command: "master <target> | target <lufs> | measure | off",
183
+ command: "shift <semitones> [formant keep|follow|<n>] [<sample>]",
158
184
  summary:
159
- "loudness · master streaming · master target -9 · master measure",
185
+ "a sample's pitch without changing its length · shift 0 clears",
160
186
  },
161
- { command: "track name <text>", summary: "rename this track" },
162
- { command: "meter <1..16>", summary: "beats per bar" },
163
187
  {
164
- command: "tempo <bpm> at <beat>|bar <n> [ramp|exp]",
165
- summary: "tempo change · tempo 90 at bar 9 ramp · tempo clear",
188
+ command: "fade [in|out] <seconds> [<sample>]",
189
+ summary: "a sample's fade in and out (Strudel fadeInTime/fadeTime)",
166
190
  },
167
191
  {
168
- command: "rit|accel [<n> bars] [to <bpm>] [at bar <n>]",
169
- summary: "gradual · rit 4 bars to 80 · a tempo · tempo primo",
192
+ command:
193
+ "resample <track>|orbit <n>|master [section <name>|bars a-b] [grain]",
194
+ summary: "render to a pinned WAV on a new sampler (or granular) track",
170
195
  },
196
+ ],
197
+ },
198
+ {
199
+ group: "voice",
200
+ entries: [
171
201
  {
172
- command: "fermata [at <beat>|at bar <n>|at end] [<extra beats>]",
173
- summary: "hold a beat · fermata at 31 2 · fermata clear",
202
+ command: "sing [preset] [param value]",
203
+ summary:
204
+ "built-in singing voice: aah ooh choir chorale airy glass lament soprano basso, throat: drone khoomei sygyt kargyraa",
174
205
  },
175
206
  {
176
- command: "meter <n>/<d> [at bar <n>]",
177
- summary: "meter change · meter 7/8 at bar 5 · meter clear",
207
+ command: "sing vowels <v> …",
208
+ summary:
209
+ "vowels for the track's notes in order, cycled · sing vowels a e i o",
178
210
  },
179
211
  {
180
- command: "track rate|phase|cycle <n> | off",
181
- summary: "polytempo · track rate 3/2 · track cycle 3",
212
+ command: "note vowel <v|a>u> [target]",
213
+ summary: "sung vowel for selected notes · note vowel o bar 2",
182
214
  },
183
215
  {
184
- command: "track phasing <beats> [over <beats>|hold <n>]",
185
- summary: "Reich phasing · track phasing 3 hold 8 · track time off",
216
+ command: "lyrics [bar] sun-lit morn-ing",
217
+ summary: "syllables onto the notes (- splits, _ holds, ~ skips)",
186
218
  },
187
219
  {
188
- command: "key <tonic> <mode> | none",
189
- summary: "song key · key A minor · key F# dorian",
220
+ command: "autotune [preset] [field value …] | off | presets",
221
+ summary:
222
+ "pitch correction · autotune hard · autotune gentle · autotune to notes melody · autotune key D bayati",
190
223
  },
191
224
  {
192
- command: "scale [<tonic>] <name> | list",
193
- summary: "song scale · scale D hijaz · scale yaman · scale list",
225
+ command:
226
+ "/formant <-12..12> [mix] | deep | giant | bright | tiny | on | off",
227
+ summary:
228
+ "formant shift at constant pitch · /formant -4 deeper · /formant 3 smaller",
194
229
  },
195
230
  {
196
- command: "tuning <name> | edo <n> | scl <file> | off",
197
- summary: "song tuning · tuning 19-edo · tuning just · tuning list",
231
+ command:
232
+ "/vowel <v> [<to> [<morph>]] | ee | to <v>|off | morph <0..1> | off",
233
+ summary: "vowel filter · /vowel a · /vowel a o 0.5 morphs a toward o",
198
234
  },
199
235
  {
200
- command: "tuning ref <hz> | root <note> | map linear|nearest",
201
- summary: "A4 reference · degree-0 key · keys per step",
236
+ command:
237
+ "vocoder [preset] | src <track> | <param> <value|reset> | off | presets",
238
+ summary:
239
+ "vocode the focused voice onto a synth (classic robot talkbox choir glass whisper smear lofi) · help vocoder lists params",
202
240
  },
241
+ ],
242
+ },
243
+ {
244
+ group: "voice · clips",
245
+ entries: [
246
+ { command: "vocal", summary: "voice tools: lists every verb" },
203
247
  {
204
- command: "tuning track <…> | track off",
205
- summary: "this track's tuning · off follows the song",
248
+ command:
249
+ "clip [id] gain -3 | gain by -3 | fade .01 .2 | fade in .01 | split 7",
250
+ summary: "edit an audio clip (dB, seconds, 1-based bars)",
206
251
  },
207
252
  {
208
- command: "cents <id> <±c>",
209
- summary: "detune one note · cents n3 -14 · add E4-14c at 0",
253
+ command:
254
+ "clip [id] trim offset 1 dur 4|end | rev | repeat 2 [to 32] | mute | rm",
255
+ summary: "trim, reverse, repeat, mute or remove a clip",
210
256
  },
257
+ ...VOCAL_VERBS.map((verb) => ({
258
+ command: `vocal ${verb.usage}`,
259
+ summary: verb.summary,
260
+ })),
261
+ ],
262
+ },
263
+ {
264
+ group: "effects",
265
+ entries: [
211
266
  {
212
- command: "section <name> <a>-<b> | add | dup | move | rename | delete",
213
- summary: "song sections · section chorus 9-16 · section lists them",
267
+ command: "fx <effect> <param> <value> | on | off | preset <name>",
268
+ summary:
269
+ "effects · fx delay mix 0.3 · fx reverb on · fx alone lists them",
214
270
  },
215
271
  {
216
- command: "section loop | jump | mute | vary <name>",
217
- summary: "section loop chorus · section mute verse drums",
272
+ command: "filter <hz> [res] · delay <beats> [fb] [mix] · reverb <mix>",
273
+ summary:
274
+ "shortcuts for fx filter, delay and reverb · filter 1200 · delay 0.5 · reverb 0.3",
218
275
  },
219
276
  {
220
- command: "form <section…> | off | bake",
221
- summary: "song form · form intro verse chorus*2 outro",
277
+ command: "rig <preset> | reset",
278
+ summary:
279
+ "guitar amp and pedals · rig crunch · rig jangle · rig metal · rig alone lists them",
222
280
  },
223
281
  {
224
- command: "build | drop | fill [<section> | <a>-<b>]",
225
- summary: "riser, roll, sweep · pre-drop cut and impact · drum fill",
282
+ command: "stomp|head|cab <type> | <param> <value>",
283
+ summary:
284
+ "pedal, amp, cabinet · stomp rat · head gain 7 gate -55 · cab 4x12",
226
285
  },
227
286
  {
228
- command: "hit <voice> at <beat>",
229
- summary: "kit tracks · hit kick at 0",
287
+ command: "fx wobble|bloom|swell|double [param value]",
288
+ summary:
289
+ "shoegaze · fx wobble depth 30 · fx double · fx reverb ir builtin:reverse",
230
290
  },
291
+ ],
292
+ },
293
+ {
294
+ group: "rhythm",
295
+ entries: [
231
296
  {
232
- command: "pattern <voice> <beats...> | every <step>",
233
- summary: "pattern kick every 1",
297
+ command: "hit <drum> at <beat>",
298
+ summary: "one hit on a kit track · hit kick at 0",
234
299
  },
235
- { command: "clear <voice>", summary: "remove one drum voice" },
236
300
  {
237
- command: "euclid <voice> <pulses> [<steps>] [rotate <n>]",
238
- summary: "generated rhythm · euclid hat 7 16 rotate 2",
301
+ command: "pattern <drum> <beats...> | every <step>",
302
+ summary: "a drum on beats · pattern kick every 1",
239
303
  },
304
+ { command: "clear <drum>", summary: "remove one drum's hits" },
240
305
  {
241
- command: "euclid <voice> <field> <value> | off | freeze",
242
- summary: "repeats pace accent prob swing …",
306
+ command: "euclid <drum> <pulses> [<steps>] [rotate <n>]",
307
+ summary: "generated rhythm · euclid hat 7 16 rotate 2",
243
308
  },
244
- { command: "grid <voice> <x.X.>", summary: "explicit steps · X accent" },
245
- { command: "undo", summary: "step back · Ctrl-Z" },
246
- { command: "redo", summary: "step forward · Ctrl-Y" },
247
- ],
248
- },
249
- {
250
- group: "session",
251
- entries: [
252
- { command: "/sessions", summary: "list sessions in this workspace" },
253
- { command: "/resume [<n>|<name>|<id>]", summary: "switch session" },
254
- { command: "/rename <name>|--auto", summary: "name this session" },
255
- { command: "/fork [<name>]", summary: "copy into a new session" },
256
- { command: "/status", summary: "name · revision · digest · storage" },
257
309
  {
258
- command: "/export <file>",
259
- summary: "write track.loop/v1 JSON, or MIDI for .mid",
310
+ command: "euclid <drum> <field> <value> | off | freeze",
311
+ summary: "repeats pace accent prob swing …",
260
312
  },
261
- { command: "/import <file>", summary: "replace the score from a file" },
313
+ { command: "grid <drum> <x.X.>", summary: "explicit steps · X accent" },
262
314
  {
263
- command: "/sample [<path> [as <voice>]]",
264
- summary: "add a sample voice · list voices",
315
+ command: "pattern [name]",
316
+ summary: "groove picker · hear each groove move",
265
317
  },
266
318
  {
267
- command: "/bpm <n> [<voice>]",
268
- summary:
269
- "the sample's own tempo (slash needed: bare bpm is song tempo)",
319
+ command: "/kit [name]",
320
+ summary: "kit picker · synth kits, then sample kits",
270
321
  },
271
322
  {
272
- command: "/fitmode [repitch|beats|tones|auto] [<voice>]",
273
- summary: "how it fits · alone suggests one from the sound",
323
+ command: "pack list|info|use|add",
324
+ summary: "sample packs · pack use 909/bd",
274
325
  },
275
326
  {
276
- command: "/len <beats> [<voice>]",
277
- summary: "the sample lasts n beats of the song",
327
+ command: "euclid [drum]",
328
+ summary: "euclid rhythm editor · Ctrl-K › Rhythm",
278
329
  },
279
330
  ],
280
331
  },
281
332
  {
282
- group: "window",
333
+ group: "chords",
283
334
  entries: [
284
335
  {
285
- command: "/track <name>",
286
- summary: "focus a track, creating it if new",
336
+ command: "key <tonic> <mode> | none",
337
+ summary: "song key · key A minor · key F# dorian",
287
338
  },
288
- { command: "/tracks", summary: "list tracks" },
289
- { command: "/view focus|all", summary: "one track or every track" },
290
- { command: "/transcript", summary: "scrollable log · Ctrl-O" },
291
- { command: "/theme default|high-contrast|mono", summary: "colors" },
292
- { command: "/motion on|off", summary: "animation" },
293
- { command: "/model [alias]", summary: "pick a model · cost per prompt" },
294
339
  {
295
- command: "/login [gateway|openrouter|codex|claude]",
296
- summary: "sign in to a provider",
340
+ command: "scale [<tonic>] <name> | list",
341
+ summary: "song scale · scale D hijaz · scale yaman · scale list",
297
342
  },
298
343
  {
299
- command: "/logout [provider]",
300
- summary: "forget keys and the saved choice",
344
+ command: "tuning <name> | edo <n> | scl <file> | off",
345
+ summary: "song tuning · tuning 19-edo · tuning just · tuning list",
301
346
  },
302
- { command: "/auth [--check]", summary: "provider and audio status" },
303
- { command: "/play [on|off]", summary: "keyboard play mode · Ctrl-P" },
304
347
  {
305
- command: "/play degrees|chromatic",
306
- summary: "home row plays the key's scale degrees (any tuning) · i",
348
+ command: "tuning ref <hz> | root <note> | map linear|nearest",
349
+ summary: "A4 reference · degree-0 key · keys per step",
307
350
  },
308
351
  {
309
- command: "/pattern [name]",
310
- summary: "drum groove picker · moving previews",
352
+ command: "tuning track <…> | track off",
353
+ summary: "this track's tuning · off follows the song",
311
354
  },
312
355
  {
313
- command: "/kit [name]",
314
- summary: "drum kit picker · synth kits, then samples",
356
+ command: "cents <id> <±c>",
357
+ summary: "detune one note · cents n3 -14 · add E4-14c at 0",
315
358
  },
316
359
  {
317
- command: "/pack list|info|use|add",
318
- summary: "sample packs · /pack use 909/bd",
360
+ command: "progression <chords> [each 4] [at 0] [bass]",
361
+ summary:
362
+ "sustained voice-led chords · progression i7 IV7 each 8 · progression Am7 D9 bass",
319
363
  },
320
364
  {
321
- command: "/euclid [voice]",
322
- summary: "T-1 style rhythm editor · Rhythm in /menu",
365
+ command: "strum [chords] [pattern] [strokes D-DU-UDU] [speed 22ms]",
366
+ summary:
367
+ "strummed guitar chords · strum G D Em C folk · strum I V vi IV · strum alone strums the track's chords",
323
368
  },
324
369
  {
325
- command: "/menu [section]",
326
- summary: "every setting by hand · Ctrl-K",
370
+ command: "/chords auto|manual|off",
371
+ summary: "chords in play mode · /chords alone shows settings",
327
372
  },
328
373
  {
329
- command: "/try <sound command>",
330
- summary: "hear it on a loop first · a A/B · enter keep",
374
+ command: "play degrees|in-key|chromatic",
375
+ summary:
376
+ "in play mode the home row plays the key's degrees (any tuning)",
331
377
  },
378
+ ],
379
+ },
380
+ {
381
+ group: "mix",
382
+ entries: [
383
+ { command: "volume <0..1>", summary: "track level" },
384
+ { command: "pan <-1..1>", summary: "left … right" },
385
+ { command: "mute", summary: "silence this track" },
386
+ { command: "unmute", summary: "hear it again" },
387
+ { command: "solo", summary: "only this track" },
388
+ { command: "unsolo", summary: "every track again" },
332
389
  {
333
- command: "/click on|off|<volume>",
334
- summary: "metronome · /count-in 0-2 · /grid 1/16",
390
+ command: "automate <lane> at <beat> <value>",
391
+ summary: "volume pan filter resonance delay-feedback delay-mix wt",
335
392
  },
336
393
  {
337
- command: "/chords auto|manual|off",
338
- summary: "play-mode chords · /chords for settings",
394
+ command: "automate <lane> points <b:v>...",
395
+ summary: "several points · automate pan points 0:-1 4:1",
339
396
  },
397
+ { command: "automate <lane> remove <beat>", summary: "drop one point" },
398
+ { command: "clear [<lane>] automation", summary: "drop a lane's points" },
340
399
  {
341
- command: "/help [topic]",
342
- summary: "start here · /help all for everything",
400
+ command: "master <unit> on|off | preset <name> | <param> <value>",
401
+ summary:
402
+ "song master · eq glue tape width limiter · master glue ratio 4",
343
403
  },
344
404
  {
345
- command: "/guide [topic]",
346
- summary: "short how-to guides · F1",
405
+ command: "master <target> | target <lufs> | measure | off",
406
+ summary:
407
+ "loudness · master streaming · master target -9 · master measure",
347
408
  },
348
409
  ],
349
410
  },
350
411
  {
351
- group: "keys",
412
+ group: "arrange",
352
413
  entries: [
353
- { command: "Enter", summary: "submit" },
354
- { command: "Shift-Enter", summary: "newline" },
355
- { command: "Alt-Enter", summary: "queue the request" },
356
- { command: "Ctrl-Q", summary: "toggle queue mode" },
357
- { command: "Ctrl-Z / Ctrl-Y", summary: "undo / redo" },
358
- { command: "Ctrl-O", summary: "transcript" },
359
- { command: "Esc", summary: "cancel the agent · back one level" },
360
- { command: "?", summary: "keys for the screen you are on" },
361
- { command: "Space", summary: "play/pause on an empty prompt" },
414
+ { command: "add <note> at <beat> [for <beats>]", summary: "add C4 at 0" },
415
+ { command: "remove <id>", summary: "delete a note" },
416
+ { command: "move <id> to <beat>", summary: "shift a note" },
417
+ { command: "length <id> <beats>", summary: "resize a note" },
418
+ { command: "velocity <id> <0..1>", summary: "note loudness" },
419
+ { command: "clear", summary: "remove this track's notes" },
420
+ { command: "track <name>", summary: "focus a track, creating it if new" },
421
+ { command: "tracks", summary: "list tracks" },
362
422
  {
363
- command: "Ctrl-P",
364
- summary: "play mode · Z/X octave · R record · Q chords",
423
+ command: "track rm|move <name> [<position>]",
424
+ summary:
425
+ "remove a track (Ctrl-Z undoes) or move it · rm and delete are aliases",
365
426
  },
427
+ { command: "track name <text>", summary: "rename this track" },
366
428
  {
367
- command: "Ctrl-K",
368
- summary: "menu · ←→ adjust · / filter · x reset",
429
+ command: "track rate|phase|loop <n> | off",
430
+ summary: "polytempo · track rate 3/2 · track loop 3",
369
431
  },
370
- { command: "Ctrl-C", summary: "exit" },
371
- ],
372
- },
373
- ];
374
-
375
- /**
376
- * `/help` with no topic: a short, task-first guide. Each row is something to
377
- * type or press, then what it does. The full reference is `/help all` (or one
378
- * group: `/help music`).
379
- */
380
- export const HELP_GUIDE: readonly HelpSection[] = [
381
- {
382
- group: "start here" as HelpGroup,
383
- entries: [
384
- { command: "type a request", summary: "“add a walking bass in A minor”" },
385
- { command: "ctrl-p", summary: "play notes on the computer keyboard" },
386
432
  {
387
- command: "ctrl-k",
388
- summary: "menu: sound, effects, performance, mix, master, arrange …",
433
+ command: "track phasing <beats> [over <beats>|hold <n>]",
434
+ summary: "Reich phasing · track phasing 3 hold 8 · track time off",
389
435
  },
390
- { command: "? · ctrl-z · ctrl-y", summary: "keys here · undo · redo" },
391
436
  ],
392
437
  },
393
438
  {
394
- group: "play notes" as HelpGroup,
439
+ group: "arrange · song",
395
440
  entries: [
396
441
  {
397
- command: "ctrl-p, a s d f …",
398
- summary: "piano keys · z x octave · esc leave",
442
+ command: "section <name> <a>-<b> | add | dup | move | rename | remove",
443
+ summary:
444
+ "song sections · section chorus 9-16 · section alone lists them",
399
445
  },
400
446
  {
401
- command: "r, then space",
402
- summary: "record over the loop (one undo a bar)",
447
+ command: "section loop | jump | mute | vary <name>",
448
+ summary: "section loop chorus · section mute verse drums",
449
+ },
450
+ {
451
+ command: "form <section…> | off | bake",
452
+ summary: "song form · form intro verse chorus*2 outro",
453
+ },
454
+ {
455
+ command: "build | drop | fill [<section> | <a>-<b>]",
456
+ summary: "riser, roll, sweep · pre-drop cut and impact · drum fill",
457
+ },
458
+ {
459
+ command: "style <id> [bars] [seed]",
460
+ summary: "a whole song in a style · style bebop 16 3 · style again",
403
461
  },
404
- ],
405
- },
406
- {
407
- group: "make drums" as HelpGroup,
408
- entries: [
409
462
  {
410
- command: "/pattern · /kit",
411
- summary: "pick a groove · pick a drum kit",
463
+ command: "style list|search|info · style blend <a> <b> [w]",
464
+ summary: "the style tree · style search maqam",
412
465
  },
413
- { command: "/euclid", summary: "rhythm editor: pulses, steps, rotation" },
466
+ { command: "bars <count>", summary: "loop length, 1–256" },
467
+ { command: "extend <count> bars", summary: "lengthen the loop" },
414
468
  ],
415
469
  },
416
470
  {
417
- group: "shape the sound" as HelpGroup,
471
+ group: "project",
418
472
  entries: [
473
+ { command: "play", summary: "start the transport" },
474
+ { command: "pause", summary: "stop the transport" },
419
475
  {
420
- command: "ctrl-k › Sound",
421
- summary: "instrument, strings, envelope, filter, wavetable",
476
+ command: "/play [on|off]",
477
+ summary: "play mode: the computer keyboard plays notes · ctrl-p",
422
478
  },
423
- { command: "ctrl-k › Effects", summary: "delay, reverb, distortion …" },
479
+ { command: "tempo <bpm>", summary: "20–300 BPM" },
424
480
  {
425
- command: "master streaming",
426
- summary: "finish: -14 LUFS, limiter · master measure",
481
+ command: "tempo <bpm> at <beat>|bar <n> [ramp|exp]",
482
+ summary: "tempo change · tempo 90 at bar 9 ramp · tempo clear",
483
+ },
484
+ {
485
+ command: "rit|accel [<n> bars] [to <bpm>] [at bar <n>]",
486
+ summary: "gradual · rit 4 bars to 80 · a tempo · tempo primo",
427
487
  },
428
- { command: "/try fx reverb mix 0.6", summary: "hear it before keeping" },
488
+ {
489
+ command: "fermata [at <beat>|at bar <n>|at end] [<extra beats>]",
490
+ summary: "hold a beat · fermata at 31 2 · fermata clear",
491
+ },
492
+ { command: "meter <1..16>", summary: "beats per bar" },
493
+ {
494
+ command: "meter <n>/<d> [at bar <n>]",
495
+ summary: "meter change · meter 7/8 at bar 5 · meter clear",
496
+ },
497
+ {
498
+ command: "/click on|off|<volume>",
499
+ summary: "metronome · /count-in 0-2 · grid 1/16",
500
+ },
501
+ {
502
+ command: "export <file> [stems]",
503
+ summary:
504
+ "JSON, MIDI for .mid, WAV for .wav · export song.wav · export mix.wav stems",
505
+ },
506
+ { command: "import <file>", summary: "replace the score from a file" },
507
+ {
508
+ command: "calibration [0|1|latest|off]",
509
+ summary: "sound fixes · 1 chokes hats, levels keys · 0 legacy",
510
+ },
511
+ { command: "undo", summary: "step back · Ctrl-Z" },
512
+ { command: "redo", summary: "step forward · Ctrl-Y" },
513
+ { command: "/status", summary: "name · revision · digest · storage" },
429
514
  ],
430
515
  },
431
516
  {
432
- group: "shape the performance" as HelpGroup,
517
+ group: "project · window",
433
518
  entries: [
519
+ { command: "/view focus|all", summary: "one track or every track" },
520
+ { command: "/transcript", summary: "scrollable log · Ctrl-O" },
521
+ { command: "/theme default|high-contrast|mono", summary: "colors" },
522
+ { command: "/motion on|off", summary: "animation" },
523
+ {
524
+ command: "/menu [section]",
525
+ summary: "every setting by hand · Ctrl-K · /menu <topic>",
526
+ },
434
527
  {
435
- command: "art staccato bar 2",
436
- summary: "humanize 8 5 · ctrl-k › Sound › performance",
528
+ command: "help [topic]",
529
+ summary: "start here · help <topic> · help all for everything",
437
530
  },
531
+ { command: "/guide [topic]", summary: "short how-to guides · F1" },
438
532
  ],
439
533
  },
440
534
  {
441
- group: "chords" as HelpGroup,
535
+ group: "project · session",
442
536
  entries: [
443
- { command: "key A minor", summary: "set the song key" },
444
- {
445
- command: "ctrl-p, then q",
446
- summary: "chord mode · 1–4 type · 5–8 extension · n next",
447
- },
537
+ { command: "/sessions", summary: "list sessions in this workspace" },
538
+ { command: "/resume [<n>|<name>|<id>]", summary: "switch session" },
539
+ { command: "/rename <name>|--auto", summary: "name this session" },
540
+ { command: "/fork [<name>]", summary: "copy into a new session" },
448
541
  ],
449
542
  },
450
543
  {
451
- group: "song structure" as HelpGroup,
544
+ group: "agent",
452
545
  entries: [
453
546
  {
454
- command: "section verse 1-8",
455
- summary: "name bars · form verse chorus*2 orders them",
547
+ command: "/model [alias]",
548
+ summary: "pick the agent's model · /model lists them · /model fast",
456
549
  },
457
550
  {
458
- command: "build · drop · fill",
459
- summary: "transitions · ctrl-k › Arrange · /help arrange",
551
+ command: "/model key [gateway|openrouter|codex|claude]",
552
+ summary: "add an agent key (optional) · finds existing setups first",
553
+ },
554
+ {
555
+ command: "/logout [provider]",
556
+ summary: "forget keys and the saved choice",
557
+ },
558
+ { command: "/auth [--check]", summary: "provider and audio status" },
559
+ {
560
+ command: "/showme on|quiet|off",
561
+ summary:
562
+ "the agent types its commands in your prompt bar as it streams",
460
563
  },
461
564
  ],
462
565
  },
566
+ ];
567
+
568
+ /**
569
+ * `/help` with no topic: start here, then the ten topics. Each row is
570
+ * something to type or press, then what it does. The full reference is
571
+ * `/help all`; one topic is `/help <topic>`.
572
+ */
573
+ export const HELP_GUIDE: readonly HelpSection[] = [
463
574
  {
464
- group: "more" as HelpGroup,
575
+ group: "start here",
465
576
  entries: [
466
577
  {
467
- command: "rit 4 bars to 80",
468
- summary: "tempo, fermatas, meter · tuning pelog · scale yaman",
578
+ command: "type a request",
579
+ summary: "“add a walking bass in A minor” (with an agent)",
580
+ },
581
+ {
582
+ command: "style deep-house",
583
+ summary: "a whole song in a style · style list",
469
584
  },
470
585
  {
471
- command: "/help all",
472
- summary: "every command and key · /guide feature guides · F1",
586
+ command: "space · ctrl-p",
587
+ summary: "play · play mode: the keyboard plays notes",
473
588
  },
589
+ { command: "ctrl-k", summary: "menu: every setting by hand" },
590
+ { command: "? · ctrl-z · ctrl-y", summary: "keys here · undo · redo" },
591
+ ],
592
+ },
593
+ {
594
+ group: "topics · help <topic>",
595
+ entries: [
596
+ ...TOPICS.map((id) => ({ command: id, summary: TOPIC_SUMMARY[id] })),
597
+ { command: "all", summary: "every command · /guide <topic> · F1" },
474
598
  ],
475
599
  },
476
600
  ];
477
601
 
478
- /** Topics `/help <topic>` takes, besides `all`. */
479
- export const HELP_TOPICS = [
480
- "music",
481
- "session",
482
- "window",
483
- "keys",
484
- "arrange",
485
- ] as const;
602
+ /** Topics `/help <topic>` takes, besides `all` (aliases resolve too). */
603
+ export const HELP_TOPICS = TOPICS;
604
+
605
+ /**
606
+ * `/help keys`: every screen's `?` panel, straight from tui/grammar.ts
607
+ * KEYS, so the reference and the panels never disagree.
608
+ */
609
+ export function keysLines(width = 80): string[] {
610
+ const seen = new Set<string>();
611
+ const sections = (Object.keys(KEYS) as KeyScreen[]).flatMap((screen) =>
612
+ KEYS[screen].map((section) => ({
613
+ title:
614
+ section.title === screen || screen === "prompt"
615
+ ? section.title
616
+ : `${screen} · ${section.title}`,
617
+ rows: section.rows,
618
+ })),
619
+ );
620
+ const column =
621
+ Math.min(
622
+ 24,
623
+ Math.max(
624
+ ...sections.flatMap((s) => s.rows.map(([keys]) => displayWidth(keys))),
625
+ ),
626
+ ) + 2;
627
+ const lines = ["── keys"];
628
+ for (const section of sections) {
629
+ const key = JSON.stringify(section.rows);
630
+ if (seen.has(key)) continue;
631
+ seen.add(key);
632
+ lines.push("", truncate(`── ${section.title}`, width));
633
+ for (const [keys, action] of section.rows)
634
+ lines.push(
635
+ truncate(
636
+ `${keys}${" ".repeat(Math.max(1, column - displayWidth(keys)))}${action}`,
637
+ width,
638
+ ),
639
+ );
640
+ }
641
+ return lines;
642
+ }
643
+
644
+ /** The last rows of a topic page: the same id in the other two doors. */
645
+ function doors(id: TopicId, width: number): string[] {
646
+ return ["", truncate(`guide ${id} · menu ${id} · help all`, width)];
647
+ }
648
+
649
+ /** A topic's groups: `sound`, then `sound · performance`, `sound · samples`. */
650
+ export function topicSections(id: TopicId): readonly HelpSection[] {
651
+ return HELP_SECTIONS.filter(
652
+ (section) => section.group === id || section.group.startsWith(`${id} · `),
653
+ );
654
+ }
655
+
656
+ /** One topic's page: its groups, then where else the topic opens. */
657
+ function topicLines(id: TopicId, width: number): string[] {
658
+ if (id === "keys") return [...keysLines(width), ...doors(id, width)];
659
+ return [...sectionLines(topicSections(id), width), ...doors(id, width)];
660
+ }
486
661
 
487
662
  /**
488
- * Rows for `/help [topic]`: the guide with no topic, the full reference for
489
- * `all`, one group for its name; undefined for an unknown topic.
663
+ * Rows for `/help [topic]`: the start page with no topic, the full
664
+ * reference for `all`, a topic for its id or an alias, a command's rows
665
+ * for its verb (`/help vocoder`); undefined for an unknown word.
490
666
  */
491
667
  export function helpTopicLines(
492
668
  topic: string | undefined,
@@ -496,34 +672,91 @@ export function helpTopicLines(
496
672
  if (!name) return sectionLines(HELP_GUIDE, width, 20);
497
673
  if (name === "all" || name === "commands" || name === "reference")
498
674
  return helpLines(width);
499
- if (name === "arrange" || name === "arrangement" || name === "sections")
500
- return sectionLines([arrangeSection()], width);
501
- const group = HELP_SECTIONS.find((section) => section.group === name);
502
- return group ? sectionLines([group], width) : undefined;
675
+ if ((TOPICS as readonly string[]).includes(name))
676
+ return topicLines(name as TopicId, width);
677
+ const spelled = HELP_COMMAND_ALIASES[name];
678
+ if (spelled) return commandTopic(spelled, width, name);
679
+ const alias = resolveTopic(name);
680
+ // A topic alias wins unless a reference row starts with the word:
681
+ // `/help sections` is the arrange topic, `/help scale` the command.
682
+ const command =
683
+ alias && !hasRow(name) ? undefined : commandTopic(name, width);
684
+ if (command)
685
+ return alias
686
+ ? [...command, "", truncate(`see also help ${alias}`, width)]
687
+ : command;
688
+ return alias ? topicLines(alias, width) : undefined;
503
689
  }
504
690
 
505
- /** `/help arrange`: every arranging command with its full usage. */
506
- function arrangeSection(): HelpSection {
507
- const verbs = ["section", "sections", "form", "build", "drop", "fill"];
508
- return {
509
- group: "arrange" as HelpGroup,
510
- entries: [
511
- // Usage strings run long: wrapped onto continuation rows.
512
- ...verbs.flatMap((verb) =>
513
- wrapWords(USAGE[verb] ?? "", 46).map((summary, index) => ({
514
- command: index === 0 ? verb : "",
515
- summary,
516
- })),
517
- ),
518
- { command: "/menu arrange", summary: "the Arrange menu (ctrl-k)" },
519
- {
520
- command: "dawg render --section <name>",
521
- summary: "export one section",
522
- },
523
- ],
524
- };
691
+ /** True when a reference row starts with `name` (`/scale`, `scale …`). */
692
+ function hasRow(name: string): boolean {
693
+ const verb = name.replace(/[^a-z0-9-]/g, "");
694
+ return HELP_SECTIONS.some((section) =>
695
+ section.entries.some((entry) =>
696
+ new RegExp(`^/?${verb}(\\s|$)`, "i").test(entry.command),
697
+ ),
698
+ );
525
699
  }
526
700
 
701
+ /**
702
+ * `/help <command>` (`/help vocoder`, `/help clip`): the command's rows
703
+ * from every group, then its full usage wrapped; undefined when no command
704
+ * has that name.
705
+ */
706
+ function commandTopic(
707
+ name: string,
708
+ width: number,
709
+ usageKey = name,
710
+ ): string[] | undefined {
711
+ const pattern = name
712
+ .replace(/[^a-z0-9 -]/g, "")
713
+ .trim()
714
+ .replace(/ +/g, "\\s+");
715
+ const own = (command: string) =>
716
+ new RegExp(`^/?${pattern}(\\s|$)`, "i").test(command);
717
+ const rows = HELP_SECTIONS.flatMap((section) =>
718
+ section.entries.filter((entry) => own(entry.command)),
719
+ );
720
+ const usage = USAGE[usageKey] ?? USAGE[name];
721
+ if (rows.length === 0 && !usage) return undefined;
722
+ const wrap = Math.max(30, width - 2);
723
+ // A usage line that repeats a row keeps only what it adds (examples).
724
+ const extra = usage
725
+ ? rows.reduce((text, entry) => {
726
+ const bare = (value: string) =>
727
+ value.replace(/^\//, "").replace(/\s*\|\s*/g, "|");
728
+ return bare(text).startsWith(bare(entry.command))
729
+ ? bare(text)
730
+ .slice(bare(entry.command).length)
731
+ .replace(/^\s*·\s*/, "")
732
+ : text;
733
+ }, usage)
734
+ : "";
735
+ // Examples a row summary already shows are not repeated.
736
+ const shown = rows.map((entry) => entry.summary).join(" · ");
737
+ const added = extra
738
+ .split(" · ")
739
+ .filter((part) => part && !shown.split(" · ").includes(part))
740
+ .join(" · ");
741
+ return [
742
+ `── ${name}`,
743
+ ...rows.flatMap((entry) => [
744
+ ...wrapWords(entry.command, wrap),
745
+ ...wrapWords(entry.summary, wrap - 2).map((line) => ` ${line}`),
746
+ ]),
747
+ ...(added ? ["", ...wrapWords(added, wrap)] : []),
748
+ ];
749
+ }
750
+
751
+ /**
752
+ * Typed aliases that /help shows under their one word (design §8.2): the
753
+ * alias opens the page and the title, heading and rows name the canonical
754
+ * command.
755
+ */
756
+ export const HELP_COMMAND_ALIASES: Readonly<Record<string, string>> = {
757
+ login: "model key",
758
+ };
759
+
527
760
  function wrapWords(text: string, width: number): string[] {
528
761
  const rows: string[] = [];
529
762
  let row = "";
@@ -553,18 +786,21 @@ function sectionLines(
553
786
  );
554
787
  for (const section of sections) {
555
788
  if (lines.length > 0) lines.push("");
556
- lines.push(`── ${section.group}`);
789
+ lines.push(truncate(`── ${section.group}`, width));
557
790
  for (const entry of section.entries) {
558
791
  const pad = Math.max(1, column - entry.command.length);
559
792
  lines.push(
560
- `${entry.command}${" ".repeat(pad)}${entry.summary}`.slice(0, width),
793
+ truncate(`${entry.command}${" ".repeat(pad)}${entry.summary}`, width),
561
794
  );
562
795
  }
563
796
  }
564
797
  return lines;
565
798
  }
566
799
 
567
- /** Overlay rows: a heading per group, then `command summary` lines. */
800
+ /**
801
+ * `/help all`: a heading per topic, then `command summary` lines, then
802
+ * the keys of every screen.
803
+ */
568
804
  export function helpLines(width = 80): string[] {
569
805
  const lines: string[] = [];
570
806
  const column = Math.min(
@@ -575,27 +811,25 @@ export function helpLines(width = 80): string[] {
575
811
  );
576
812
  for (const section of HELP_SECTIONS) {
577
813
  if (lines.length > 0) lines.push("");
578
- lines.push(`── ${section.group}`);
814
+ lines.push(truncate(`── ${section.group}`, width));
579
815
  for (const entry of section.entries) {
580
816
  const pad = Math.max(1, column - entry.command.length);
581
817
  lines.push(
582
- `${entry.command}${" ".repeat(pad)}${entry.summary}`.slice(0, width),
818
+ truncate(`${entry.command}${" ".repeat(pad)}${entry.summary}`, width),
583
819
  );
584
820
  }
585
821
  }
586
- return lines;
822
+ return [...lines, "", ...keysLines(width)];
587
823
  }
588
824
 
589
825
  /** The `Commands:` block of `dawg --help`. */
590
826
  export function helpText(): string {
591
- return HELP_SECTIONS.filter((section) => section.group !== "keys")
592
- .map(
593
- (section) =>
594
- `${section.group}:\n${section.entries
595
- .map((entry) => ` ${entry.command}`)
596
- .join("\n")}`,
597
- )
598
- .join("\n");
827
+ return HELP_SECTIONS.map(
828
+ (section) =>
829
+ `${section.group}:\n${section.entries
830
+ .map((entry) => ` ${entry.command}`)
831
+ .join("\n")}`,
832
+ ).join("\n");
599
833
  }
600
834
 
601
835
  /** Usage for a known verb, shown instead of sending a near-miss to the agent. */
@@ -603,17 +837,20 @@ export const USAGE: Readonly<Record<string, string>> = {
603
837
  click: "/click on|off|<volume> · /click 50%",
604
838
  "count-in": "/count-in 0|1|2",
605
839
  chords:
606
- "/chords auto|manual|off · voicing <n> · spread · bass · perform · rate · octaves · sevenths · preset · style",
840
+ "/chords auto|manual|off · voicing <n> · spread · bass · perform · rate · octaves · sevenths · preset · style · strokes · speed",
607
841
  key: "key <tonic> <mode> | none · key A minor",
608
842
  scale: "scale [<tonic>] <name> | list · scale D hijaz",
609
843
  tuning:
610
844
  "tuning <name> | edo <n> | ratios … | cents … | scl <file> [kbm <file>] | ref <hz> | root <note> | map linear|nearest | track … | off · tuning 19-edo",
611
- tune: "tuning <name> | edo <n> | scl <file> | off · tuning list",
845
+ calibration: "calibration [0|1|latest|off] · calibration latest",
846
+ tune: "tuning <name> | edo <n> | scl <file> | off · tuning list · pitch correction is autotune",
847
+ autotune:
848
+ "autotune [hard|robot|warble|trap|pop|natural|gentle|guided|locked] | to scale|chromatic|chord|notes [track] | key <key> | speed hold glide <ms> | relax amount center drift <0..1> | flex <0..100> | vib <Hz> vibmod <st> | voice auto|bass|tenor|alto|soprano | reset | off | presets · autotune hard · autotune natural flex 40",
612
849
  cents: "cents <id> <±cents> · cents n3 -14",
613
- grid: "/grid 1/4|1/8|1/8T|1/16|1/16T|1/32",
850
+ grid: "grid 1/4|1/8|1/8T|1/16|1/16T|1/32",
614
851
  tempo:
615
852
  "tempo takes 20…300 · tempo 120 · tempo 90 at bar 9 [ramp|exp] · tempo remove bar 9 · tempo clear · tempo map",
616
- bpm: "tempo takes 20…300 · tempo 120 · a sample's own tempo: /bpm 174 [<voice>] · /bpm off",
853
+ bpm: "tempo takes 20…300 · tempo 120 · a sample's own tempo: /bpm 174 [<sample>] · /bpm off",
617
854
  rit: "rit [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · rit 4 bars to 80",
618
855
  ritardando:
619
856
  "rit [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · rit 4 bars to 80",
@@ -635,14 +872,14 @@ export const USAGE: Readonly<Record<string, string>> = {
635
872
  bars: "bars takes 1…256 · bars 8",
636
873
  extend: "extend <count> bars · extend 4 bars",
637
874
  instrument:
638
- "instrument <name> · sine piano pluck bass saw square triangle wavetable kit · pianos: grand upright felt honkytonk prepared · synth: sawtooth supersaw pulse white pink z_square…",
875
+ "instrument <name> · sine piano pluck bass sawtooth square triangle wavetable kit · pianos: grand upright felt honkytonk prepared · electric: epiano suitcase dyno wurli clav funkclav · synth: supersaw pulse white pink z_square · voices: vocal aah ooh choir chorale khoomei sygyt kargyraa vocoder…",
639
876
  volume: "volume takes 0…1 · volume 0.8",
640
877
  vol: "volume takes 0…1 · volume 0.8",
641
878
  pan: "pan takes -1…1 · pan -0.5",
642
879
  wt: "wt <table> | wt <0..1> | wt list · wt basic · wt wt_digital:2",
643
880
  wavetable: "wt <table> | wt <0..1> | wt list · wt basic · wt wt_digital:2",
644
881
  grain:
645
- "grain <preset> | <param> <value> | on [voice V] | src synth:<name>|voice V | reset | off | presets · grain cloud · grain pitch 12",
882
+ "grain <preset> | <param> <value> | on [voice V] | src synth:<name>|voice V | reset | off | presets · sync 1/64…1/1 (t triplet, d or . dotted) · grain cloud · grain pitch 12 · grain sync 1/8.",
646
883
  granular:
647
884
  "grain <preset> | <param> <value> | on [voice V] | src synth:<name>|voice V | reset | off | presets · grain cloud",
648
885
  warpmode: "warpmode none|asym|bendp|bendm|bendmp|sync|quant",
@@ -651,58 +888,95 @@ export const USAGE: Readonly<Record<string, string>> = {
651
888
  reverb: "reverb <mix> [size] · reverb 0.3 0.6 · reverb off",
652
889
  automate: "automate <lane> at <beat> <value> · automate volume at 0 0.5",
653
890
  automation: "automate <lane> at <beat> <value> · automate volume at 0 0.5",
654
- hit: "hit <voice> at <beat> · hit kick at 0",
655
- pattern: "pattern <voice> <beats...> | every <step> · pattern kick every 1",
656
- clear: "clear · clear <voice> · clear [<lane>] automation",
891
+ hit: "hit <drum> at <beat> · hit kick at 0",
892
+ pattern: "pattern <drum> <beats...> | every <step> · pattern kick every 1",
893
+ clear: "clear · clear <drum> · clear [<lane>] automation",
657
894
  track:
658
- "/track <name> · /track drums · track rate <0.125..8>|<a>/<b>|off · track phase <beats> · track cycle <beats> · track phasing <beats> [over <beats>]",
659
- tracks: "/tracks",
895
+ "track <name> · track drums · track rm <name> · track move <name> <position> · track rate <0.125..8>|<a>/<b>|off · track phase <beats> · track loop <beats> · track phasing <beats> [over <beats>]",
896
+ tracks: "tracks",
660
897
  sessions: "/sessions",
661
898
  resume: "/resume [<n>|<name>|<id>]",
662
899
  rename: "/rename <name> | --auto",
663
900
  fork: "/fork [<name>]",
664
901
  status: "/status",
665
- export: "/export <file> · /export loop.track.json",
666
- import: "/import <file> · /import loop.track.json",
902
+ export: "export <file> · export loop.track.json",
903
+ import: "import <file> · import loop.track.json",
667
904
  sample:
668
- "/sample <path> [as <voice>] · /sample set <voice> <control> <value>… · /sample set brk fit on clip 1",
669
- samples: "/sample · lists the focused track's voices",
905
+ "/sample <path> [as <sample>] · /sample set <sample> <control> <value>… · /sample set brk fit on clip 1 · /sample set soft vel 0-63 rr a",
906
+ samples: "/sample · lists the focused track's samples",
670
907
  fitmode:
671
- "/fitmode [repitch|beats|tones|auto|off] [<voice>] · /fitmode beats · /fitmode auto brk",
672
- len: "/len <beats> [<voice>] · /len 16 · /len off",
908
+ "fitmode [repitch|beats|tones|auto|off] [<sample>] · fitmode beats · fitmode auto brk",
909
+ len: "len <beats> [<sample>] · len 16 · len off",
910
+ shift:
911
+ "shift <semitones> [formant keep|follow|<n>] [<sample>] · shift 7 formant keep · shift 0",
912
+ fade: "fade [in|out] <seconds> [<sample>] · fade out 0.5 · fade in 0.05 · fade off",
913
+ resample:
914
+ "resample <track>|orbit <n>|master [section <name>|bars a-b] [post] [grain] [as <id>] · resample lead · resample drums bars 1-2 grain · resample master section chorus",
915
+ bounce: "resample <track>|orbit <n>|master [section <name>|bars a-b] [grain]",
673
916
  view: "/view focus | all",
674
917
  transcript: "/transcript",
675
918
  log: "/transcript",
676
919
  theme: "/theme default | high-contrast | mono",
677
920
  motion: "/motion on | off",
678
- model: "/model [alias | vendor/model]",
679
- login: "/login [gateway | openrouter | codex | claude]",
921
+ model:
922
+ "/model [fast | alias | vendor/model] · /model key [gateway | openrouter | codex | claude]",
923
+ models: "/model [fast | alias | vendor/model] · /model key [provider]",
924
+ login: "/model key [gateway | openrouter | codex | claude]",
680
925
  logout: "/logout [provider]",
681
926
  auth: "/auth [--check]",
682
- help: "/help [topic] · /help all · /help music|session|window|keys",
683
- guide: "/guide [topic] · /guide chords · F1",
927
+ help: "help [topic] · help all · help sound|voice|effects|rhythm|chords|mix|arrange|project|keys|agent",
928
+ guide: "/guide [topic] · /guide voice · F1 · the same topics as help",
684
929
  fx: "fx <effect> <param> <value> | on | off | preset <name> · fx delay mix 0.3",
685
930
  synth: "synth <param> <value> | preset <name> · synth lpf 1200",
686
931
  string:
687
932
  "string <preset> | preset <name> | <param> <value> | presets | reset | off · string koto",
688
- rig: "rig clean|crunch|punk|ragged|lead|metal|fuzz|octave|funk|wah|bachata|spring|bassdrive|reese|jangle|alt | reset",
933
+ bowed:
934
+ "bowed [violin|viola|cello|contrabass|fiddle|erhu|kamancheh|violins|violas|cellos|contrabasses|pizz|trem] | <param> <value> | presets · bowed violin · bowed pressure 0.7",
935
+ rig: "rig clean|crunch|punk|ragged|lead|metal|fuzz|octave|funk|wah|bachata|spring|bassdrive|reese|jangle|alt|shoegaze|glide|dreampop|swell|ebow | reset",
936
+ guitar: `guitar tune ${GUITAR_TUNING_NAMES.join("|")} | E A D G B E · capo 0..12 · hand 3..6 · ring 0..1 · position · reset`,
937
+ progression:
938
+ "progression i7 IV7 i7 IV7 [each 8] [at 0] [bass] · numerals in the song key or symbols (Am7 D9) · block chords that hold for each span",
939
+ strum:
940
+ "strum G D Em C [folk|pop|punk|…] [strokes D-DU-UDU] [speed 22ms] [each 4] [at 0] · strum alone strums the track's chords",
689
941
  stomp:
690
942
  "stomp fuzz|face|od|rat|octave | gain <0-10> tone <0-1> level <dB> | off",
691
943
  head: "head clean|chime|crunch|lead|high|solid|bass | gain bass mid treble presence master <0-10> | gate <dB> | off",
692
944
  cab: "cab 1x12|2x12|4x12|1x10|open|8x10|1x15|di | mic <0-1> | off",
693
- keys: "keys <param> <value> | preset <name> | reset | presets · keys hardness 0.3 · keys stretch 0",
945
+ keys: "keys <param> <value> | preset <name> | reset | presets · keys hardness 0.3 · keys stretch 0 · keys sym 0.5 (sympathetic bloom under the sustain pedal)",
694
946
  piano:
695
947
  "piano [grand|ballad|upright|felt|lofi|honkytonk|prepared] · piano ballad",
948
+ epiano:
949
+ "epiano [preset epiano|suitcase|dyno] | bark bell tone vibe vibehz decay release <value> · epiano vibe 0.6 · rhodes",
950
+ wurli:
951
+ "wurli [preset wurli] | bark bell tone trem decay release <value> · wurli trem 0.5",
952
+ clav: "clav [preset clav|funkclav] | pickup neck|bridge|both|out | mute tone decay release <value> · clav pickup bridge",
953
+ tonewheel:
954
+ "tonewheel [<9 drawbar digits>] | hammond | b3 | gospel | jazzorgan · tonewheel 888800008",
955
+ combo: "combo [<5 register digits>] | farfisa | vox · combo 08880",
956
+ pipe: "pipe [plenum|flutes|cornet|reeds|strings|full | <stop> …] | church · pipe principal8,octave4",
957
+ rotary: "rotary slow|fast|stop [at <beat>] · rotary fast · rotary fast at 16",
696
958
  modal:
697
- "modal <preset> | <body> | <param> <value> | mallet <name> | reset | off | presets · modal vibes · modal saron · modal ring 3",
698
- pack: "/pack list | info <name> | use <pack>/<sound> | add <url>",
959
+ "modal <preset> | <body> | <param> <value> | mallet <name> | pair <track> | gamelan | reset | off | presets · modal vibes · modal gangsa · modal ring 3",
960
+ wind: "wind <preset> | <param> <value> | mute <name> | reset | off | presets · wind flute · wind trumpet mute harmon · wind players 4",
961
+ formant:
962
+ "/formant <-12..12> [mix] | deep|giant|bright|tiny | on | off · /formant -4 · /formant 3 0.5",
963
+ vowel:
964
+ "/vowel <v> [<to> [<morph 0..1>]] | ee | to <v>|off | morph <0..1> | mix <0..1> | off · /vowel a o 0.5",
965
+ lyrics:
966
+ 'lyrics [bar] <syllables> | clear [bar] · lyrics "sun-lit morn-ing" (- splits, _ holds, ~ skips)',
967
+ sing: "sing <preset> | <param> <value> | drone <D3> | vowels a e i … | reset | off | presets · sing choir · sing khoomei drone D3 · sing vowel o voices 6",
968
+ vocoder:
969
+ "vocoder [preset] | src <track> | <param> <value|reset> | reset | off | presets · vocoder talkbox · vocoder src vox · vocoder formant +3 · vocoder gate auto · params: tap mode carrier follow root spread bands lo hi width attack release formant unvoiced sens hiss gate enhance depth freeze mix gain seed",
970
+ pack: "pack list | info <name> | use <pack>/<sound> | add <url>",
699
971
  kit: "/kit [name] · /kit syn909",
700
- euclid: "/euclid [voice] · euclid hat 7 16",
701
- menu: "/menu [sound|effects|rhythm|chords|mix|master|project|tuning]",
972
+ euclid: "euclid [drum] · euclid hat 7 16",
973
+ menu: "/menu [sound|voice|effects|rhythm|chords|mix|arrange|project|keys|agent] or any row name · /menu tuning · Ctrl-K",
974
+ style:
975
+ "style [list [id]|search <words>|info <id>|<id> [bars] [seed]|blend <a> <b> [w] [bars] [seed]|again] · style deep-house 16 · style blend bebop bossa-nova 0.3",
702
976
  master:
703
977
  "master <unit> on|off|preset <name>|<param> <value> · master streaming|club|loud · master target -14 · master measure · master off",
704
978
  try: "/try <sound command> · /try fx reverb mix 0.6",
705
- play: "/play [on|off|degrees|chromatic] · Ctrl-P · i toggles degrees",
979
+ play: "/play [on|off|degrees|in-key|chromatic] · ctrl-p play mode · i toggles degrees",
706
980
  meter:
707
981
  "meter <1..16> · meter 3 · meter 7/8 [at bar <n>] · meter remove bar <n> · meter clear",
708
982
  art: EXPRESSION_USAGE.art,
@@ -727,37 +1001,32 @@ export const USAGE: Readonly<Record<string, string>> = {
727
1001
  redo: "redo · Ctrl-Y",
728
1002
  };
729
1003
 
730
- /** Verbs a typo can be matched against, slash or bare as they are typed. */
1004
+ /** Verbs a typo can be matched against, without their slash. */
731
1005
  const KNOWN_VERBS: readonly string[] = [
732
1006
  ...new Set(
733
1007
  [
734
1008
  ...HELP_SECTIONS.flatMap((section) =>
735
- section.group === "keys"
736
- ? []
737
- : section.entries.map((entry) => entry.command.split(/[\s|[]/)[0]!),
1009
+ section.entries.map((entry) => entry.command.split(/[\s|[]/)[0]!),
738
1010
  ),
739
1011
  ...Object.values(USAGE).map((usage) => usage.split(/[\s|[]/)[0]!),
740
- ].filter((verb) => /^\/?[a-z][\w-]*$/i.test(verb)),
1012
+ ]
1013
+ .map((verb) => verb.replace(/^\//, ""))
1014
+ .filter((verb) => /^[a-z][\w-]*$/i.test(verb)),
741
1015
  ),
742
1016
  ];
743
1017
 
744
- function editDistance(a: string, b: string): number {
745
- const row = Array.from({ length: b.length + 1 }, (_, index) => index);
746
- for (let i = 1; i <= a.length; i += 1) {
747
- let previous = row[0]!;
748
- row[0] = i;
749
- for (let j = 1; j <= b.length; j += 1) {
750
- const current = row[j]!;
751
- row[j] = Math.min(
752
- row[j]! + 1,
753
- row[j - 1]! + 1,
754
- previous + (a[i - 1] === b[j - 1] ? 0 : 1),
755
- );
756
- previous = current;
757
- }
758
- }
759
- return row[b.length]!;
760
- }
1018
+ /** Window verbs whose free text would otherwise read as a typo fix. */
1019
+ const SLASH_ONLY: ReadonlySet<string> = new Set([
1020
+ "rename",
1021
+ "fork",
1022
+ "resume",
1023
+ "login",
1024
+ "logout",
1025
+ "auth",
1026
+ "bpm",
1027
+ ]);
1028
+
1029
+ export { editDistance } from "./nearest.ts";
761
1030
 
762
1031
  /**
763
1032
  * The known command nearest to the first word of `command` (`/clik` →
@@ -765,19 +1034,25 @@ function editDistance(a: string, b: string): number {
765
1034
  */
766
1035
  export function nearestCommand(command: string): string | undefined {
767
1036
  const word = command.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
1037
+ const slash = word.startsWith("/");
768
1038
  const bare = word.replace(/^\//, "");
769
1039
  if (!bare) return undefined;
770
1040
  let best: { verb: string; distance: number; score: number } | undefined;
771
1041
  for (const verb of KNOWN_VERBS) {
772
- const distance = editDistance(bare, verb.replace(/^\//, ""));
773
- if (distance === 0 && verb === word) continue;
774
- // Ties go to the form typed: `/patern` → `/pattern`, not `pattern`.
775
- const sameForm = verb.startsWith("/") === word.startsWith("/");
776
- const score = distance * 2 + (sameForm ? 0 : 1);
1042
+ const distance = editDistance(bare, verb);
1043
+ if (distance === 0 && !slash) continue;
1044
+ // Ties go to a dropped letter (`/clik` → `/click`, not `/clip`), the
1045
+ // commonest slip, then a changed one, then an extra one.
1046
+ const shape =
1047
+ verb.length > bare.length ? 0 : verb.length === bare.length ? 1 : 2;
1048
+ const score = distance * 3 + shape;
777
1049
  if (!best || score < best.score) best = { verb, distance, score };
778
1050
  }
779
1051
  const limit = bare.length <= 4 ? 1 : 2;
780
- return best && best.distance <= limit ? best.verb : undefined;
1052
+ if (!best || best.distance > limit) return undefined;
1053
+ // Answer in the form typed (`/clik` → `/click`), except an exact word
1054
+ // that failed with a slash (`/fx delay on` → `fx`): its slash was wrong.
1055
+ return slash && best.distance > 0 ? `/${best.verb}` : best.verb;
781
1056
  }
782
1057
 
783
1058
  /**
@@ -794,7 +1069,7 @@ export function typoFix(
794
1069
  const [first, ...rest] = text.split(/\s+/);
795
1070
  const word = first!.toLowerCase();
796
1071
  for (const verb of KNOWN_VERBS) {
797
- if (verb.startsWith("/") || verb === word) continue;
1072
+ if (SLASH_ONLY.has(verb) || verb === word) continue;
798
1073
  if (editDistance(word, verb) !== 1) continue;
799
1074
  const candidate = [verb, ...rest].join(" ");
800
1075
  if (parses(candidate)) return candidate;
@@ -834,7 +1109,18 @@ export function usageHint(command: string): string | undefined {
834
1109
  !words.slice(1).some((word) => /\d/.test(word) || ARRANGE_WORDS.has(word))
835
1110
  )
836
1111
  return undefined;
837
- return USAGE[verb];
1112
+ const usage = USAGE[verb];
1113
+ return usage === undefined ? undefined : usageForm(usage);
1114
+ }
1115
+
1116
+ /**
1117
+ * Every usage line reads `usage · <cmd> <args>`; a range refusal keeps
1118
+ * its own template (`pan takes -1…1 · pan -0.5`).
1119
+ */
1120
+ export function usageForm(usage: string): string {
1121
+ return / takes /.test(usage) || usage.startsWith("usage · ")
1122
+ ? usage
1123
+ : `usage · ${usage}`;
838
1124
  }
839
1125
 
840
1126
  const ARRANGE_VERBS: ReadonlySet<string> = new Set([
@@ -861,3 +1147,30 @@ const ARRANGE_WORDS: ReadonlySet<string> = new Set([
861
1147
  "bake",
862
1148
  "off",
863
1149
  ]);
1150
+
1151
+ /** The overlay title for `/help [topic]`: an alias shows its topic. */
1152
+ export function helpTitle(topic: string | undefined): string {
1153
+ const name = topic?.trim().toLowerCase().replace(/^\//, "");
1154
+ if (!name) return "help";
1155
+ const spelled = HELP_COMMAND_ALIASES[name];
1156
+ if (spelled) return `help · ${spelled}`;
1157
+ const id = (TOPICS as readonly string[]).includes(name)
1158
+ ? name
1159
+ : USAGE[name] || commandHelpExists(name)
1160
+ ? name
1161
+ : (resolveTopic(name) ?? name);
1162
+ return `help · ${id}`;
1163
+ }
1164
+
1165
+ function commandHelpExists(name: string): boolean {
1166
+ return HELP_SECTIONS.some((section) =>
1167
+ section.entries.some((entry) =>
1168
+ entry.command.replace(/^\//, "").startsWith(`${name} `),
1169
+ ),
1170
+ );
1171
+ }
1172
+
1173
+ /** `no topic X · did you mean Y · /help`, for `/help X` with no page. */
1174
+ export function helpMiss(topic: string): string {
1175
+ return topicMiss(topic, ["all"]);
1176
+ }