@hraness/dawg 0.6.1 → 0.8.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 (277) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DAWG.md +454 -140
  3. package/README.md +30 -26
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +271 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +184 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/ids.ts +52 -0
  11. package/core/instruments.ts +19 -0
  12. package/core/keys.ts +3 -3
  13. package/core/loop.ts +8 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/range.ts +581 -0
  17. package/core/resonators.ts +16 -2
  18. package/core/routing.ts +165 -0
  19. package/core/score.ts +955 -14
  20. package/core/sdk/eval-child.ts +7 -2
  21. package/core/sdk/eval.ts +35 -6
  22. package/core/sdk/print.ts +287 -5
  23. package/core/sdk/sync-lyrics.ts +49 -0
  24. package/core/sdk/v1.ts +2132 -46
  25. package/core/sections.ts +480 -35
  26. package/core/sing.ts +815 -0
  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/guides/agent.md +29 -0
  50. package/guides/arrange.md +31 -0
  51. package/guides/audio.md +31 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -7
  54. package/guides/chords.md +15 -15
  55. package/guides/effects.md +17 -16
  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 +15 -7
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/panes.md +32 -0
  65. package/guides/performance.md +15 -13
  66. package/guides/play.md +21 -13
  67. package/guides/project.md +26 -8
  68. package/guides/providers.md +20 -14
  69. package/guides/resample.md +15 -9
  70. package/guides/rhythm.md +18 -13
  71. package/guides/sessions.md +17 -7
  72. package/guides/show-me.md +31 -0
  73. package/guides/sound.md +25 -9
  74. package/guides/sounds.md +15 -13
  75. package/guides/styles.md +31 -0
  76. package/guides/tape.md +32 -0
  77. package/guides/tempo.md +15 -10
  78. package/guides/tracks.md +15 -10
  79. package/guides/tuning.md +32 -0
  80. package/guides/voice.md +31 -0
  81. package/guides/web-search.md +18 -8
  82. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  83. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  84. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  85. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  86. package/native/prebuilt/manifest.json +21 -0
  87. package/package.json +6 -2
  88. package/src/agent/agent.ts +130 -18
  89. package/src/agent/calibration-tools.ts +53 -0
  90. package/src/agent/clip-tools.ts +453 -0
  91. package/src/agent/command-agent.ts +378 -0
  92. package/src/agent/drum-tools.ts +2 -2
  93. package/src/agent/expression-tools.ts +1 -1
  94. package/src/agent/gateway.ts +246 -60
  95. package/src/agent/models.ts +53 -12
  96. package/src/agent/ops.ts +27 -1
  97. package/src/agent/pack-tools.ts +1 -1
  98. package/src/agent/planner.ts +25 -0
  99. package/src/agent/portable-schema.ts +80 -0
  100. package/src/agent/preview-tool.ts +4 -1
  101. package/src/agent/provider.ts +22 -8
  102. package/src/agent/range-tools.ts +216 -0
  103. package/src/agent/rhythm-tools.ts +1 -1
  104. package/src/agent/section-tools.ts +1 -1
  105. package/src/agent/show-me.ts +497 -0
  106. package/src/agent/steer.ts +15 -0
  107. package/src/agent/style-tools.ts +217 -0
  108. package/src/agent/tool-error.ts +12 -0
  109. package/src/agent/tools.ts +110 -23
  110. package/src/agent/usage.ts +2 -2
  111. package/src/agent/voice-tools.ts +925 -0
  112. package/src/agent/xcb-agent.ts +13 -10
  113. package/src/argv.ts +38 -0
  114. package/src/audio/analysis.ts +253 -0
  115. package/src/audio/arrange.ts +49 -5
  116. package/src/audio/audio-command.ts +218 -0
  117. package/src/audio/autotune-engine.ts +101 -0
  118. package/src/audio/autotune.ts +640 -0
  119. package/src/audio/clips.ts +240 -0
  120. package/src/audio/devices.ts +264 -0
  121. package/src/audio/doctor.ts +157 -0
  122. package/src/audio/dsp/bandbank.ts +138 -0
  123. package/src/audio/dsp/envelope.ts +10 -0
  124. package/src/audio/dsp/follow.ts +120 -0
  125. package/src/audio/dsp/formant.ts +427 -0
  126. package/src/audio/dsp/glottal.ts +243 -0
  127. package/src/audio/dsp/interp.ts +7 -2
  128. package/src/audio/dsp/lpc.ts +50 -0
  129. package/src/audio/dsp/periodicity.ts +59 -0
  130. package/src/audio/dsp/pitch.ts +995 -0
  131. package/src/audio/dsp/psola.ts +199 -0
  132. package/src/audio/effects/chain.ts +3 -1
  133. package/src/audio/effects/common.ts +43 -0
  134. package/src/audio/effects/convolution.ts +7 -4
  135. package/src/audio/effects/filter.ts +48 -69
  136. package/src/audio/effects/formant.ts +263 -0
  137. package/src/audio/engine.ts +360 -40
  138. package/src/audio/fit.ts +35 -3
  139. package/src/audio/instrument-check.ts +59 -43
  140. package/src/audio/instruments.ts +4 -0
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +8 -1
  143. package/src/audio/keys/engine.ts +13 -1
  144. package/src/audio/keys/piano.ts +22 -2
  145. package/src/audio/kits.ts +135 -6
  146. package/src/audio/live.ts +114 -20
  147. package/src/audio/native.ts +615 -0
  148. package/src/audio/preview.ts +30 -2
  149. package/src/audio/render-worker.ts +2 -0
  150. package/src/audio/renderer.ts +2 -0
  151. package/src/audio/resample.ts +2 -1
  152. package/src/audio/sampler.ts +85 -4
  153. package/src/audio/samples.ts +20 -2
  154. package/src/audio/sing/analysis.ts +193 -0
  155. package/src/audio/sing/engine.ts +949 -0
  156. package/src/audio/strings/bow.ts +48 -5
  157. package/src/audio/strings/engine.ts +8 -2
  158. package/src/audio/synth/oscillators.ts +31 -21
  159. package/src/audio/synth/voice.ts +34 -1
  160. package/src/audio/vocoder/bank.ts +314 -0
  161. package/src/audio/vocoder/carrier.ts +165 -0
  162. package/src/audio/vocoder/control.ts +68 -0
  163. package/src/audio/vocoder/detect.ts +50 -0
  164. package/src/audio/vocoder/index.ts +304 -0
  165. package/src/audio/vocoder/talkbox.ts +143 -0
  166. package/src/audio/wav.ts +500 -77
  167. package/src/audio/winds/engine.ts +5 -1
  168. package/src/audio/winds/trim.ts +28 -4
  169. package/src/audio/winds/trims1.ts +297 -0
  170. package/src/audio/winds/voice.ts +15 -2
  171. package/src/auth/cli.ts +38 -36
  172. package/src/auth/credentials.ts +30 -1
  173. package/src/auth/login.ts +15 -9
  174. package/src/auth/tui.ts +19 -10
  175. package/src/commands/arrange.ts +82 -32
  176. package/src/commands/autotune.ts +421 -0
  177. package/src/commands/calibration.ts +74 -0
  178. package/src/commands/clips.ts +887 -0
  179. package/src/commands/drums.ts +3 -2
  180. package/src/commands/edit.ts +11 -4
  181. package/src/commands/expression.ts +1 -1
  182. package/src/commands/formant.ts +221 -0
  183. package/src/commands/fx.ts +101 -32
  184. package/src/commands/grammar.ts +560 -0
  185. package/src/commands/help.ts +774 -366
  186. package/src/commands/history.ts +139 -10
  187. package/src/commands/keys.ts +25 -8
  188. package/src/commands/modal.ts +1 -1
  189. package/src/commands/music.ts +1 -1
  190. package/src/commands/nearest.ts +53 -0
  191. package/src/commands/pack.ts +9 -2
  192. package/src/commands/param-range.ts +56 -0
  193. package/src/commands/parses.ts +135 -0
  194. package/src/commands/progression.ts +170 -0
  195. package/src/commands/range.ts +763 -0
  196. package/src/commands/resample.ts +13 -10
  197. package/src/commands/rhythm.ts +3 -0
  198. package/src/commands/rig.ts +3 -24
  199. package/src/commands/sample.ts +11 -1
  200. package/src/commands/sing.ts +474 -0
  201. package/src/commands/strum.ts +13 -1
  202. package/src/commands/style.ts +415 -0
  203. package/src/commands/time.ts +6 -3
  204. package/src/commands/tuning.ts +3 -3
  205. package/src/commands/vocal-pitch.ts +617 -0
  206. package/src/commands/vocal.ts +147 -0
  207. package/src/commands/vocoder.ts +627 -0
  208. package/src/commands/wind.ts +2 -2
  209. package/src/fs/durable.ts +50 -0
  210. package/src/identity/actor.ts +127 -0
  211. package/src/lang/glossary.ts +511 -0
  212. package/src/launch-args.ts +267 -0
  213. package/src/main.ts +2508 -308
  214. package/src/media/cli.ts +20 -3
  215. package/src/media/import.ts +3 -1
  216. package/src/project/check.ts +21 -2
  217. package/src/project/clip-pins.ts +72 -0
  218. package/src/project/init.ts +23 -8
  219. package/src/project/sync.ts +418 -86
  220. package/src/render.ts +20 -1
  221. package/src/session/client.ts +107 -3
  222. package/src/session/clipboard.ts +79 -0
  223. package/src/session/daemon.ts +199 -5
  224. package/src/session/live-host.ts +232 -0
  225. package/src/session/meta.ts +14 -0
  226. package/src/session/origin.ts +174 -0
  227. package/src/session/port.ts +109 -14
  228. package/src/session/presence.ts +34 -4
  229. package/src/session/protocol.ts +365 -7
  230. package/src/session/rebase.ts +63 -12
  231. package/src/session/receipt.ts +265 -0
  232. package/src/session/shared-live.ts +131 -0
  233. package/src/session/store.ts +126 -45
  234. package/src/tui/arrange-menu.ts +225 -23
  235. package/src/tui/audition.ts +1 -1
  236. package/src/tui/euclid.ts +113 -27
  237. package/src/tui/fader.ts +282 -42
  238. package/src/tui/granular-menu.ts +2 -4
  239. package/src/tui/knob-fields.ts +107 -0
  240. package/src/tui/knob-map.ts +198 -0
  241. package/src/tui/menu-clips.ts +297 -0
  242. package/src/tui/menu-time.ts +20 -13
  243. package/src/tui/menu-voice.ts +405 -0
  244. package/src/tui/menu.ts +987 -179
  245. package/src/tui/modal-menu.ts +6 -6
  246. package/src/tui/performance-menu.ts +5 -2
  247. package/src/tui/play-chords.ts +4 -2
  248. package/src/tui/play-mode.ts +15 -1
  249. package/src/tui/play-session.ts +243 -28
  250. package/src/tui/sing-menu.ts +278 -0
  251. package/src/tui/style-menu.ts +104 -0
  252. package/src/tui/tape-mode.ts +580 -0
  253. package/src/tui/tape-view.ts +263 -0
  254. package/src/tui/vocoder-menu.ts +244 -0
  255. package/src/tui/wind-menu.ts +3 -3
  256. package/src/version.ts +8 -0
  257. package/src/web/fetch.ts +115 -29
  258. package/tui/activity.ts +180 -9
  259. package/tui/app.ts +491 -84
  260. package/tui/clip-row.ts +132 -0
  261. package/tui/delight.ts +193 -0
  262. package/tui/drawer.ts +233 -27
  263. package/tui/frame-gate.ts +76 -0
  264. package/tui/grammar.ts +143 -65
  265. package/tui/guide.ts +50 -5
  266. package/tui/highway.ts +309 -30
  267. package/tui/hints.ts +197 -0
  268. package/tui/hits.ts +7 -1
  269. package/tui/input.ts +60 -9
  270. package/tui/keys.ts +1 -1
  271. package/tui/knobs.ts +268 -0
  272. package/tui/play-strip.ts +80 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +151 -18
  275. package/tui/tape.ts +439 -0
  276. package/tui/text.ts +35 -2
  277. package/tui/theme.ts +46 -1
@@ -17,6 +17,14 @@
17
17
  * every newer event. Events whose rewind was compacted away, and events that
18
18
  * did not change the composition (transport), take no part.
19
19
  */
20
+ import { DiffError, diffScores } from "../../core/diff.ts";
21
+ import {
22
+ scoreFromJSON,
23
+ type ScoreOperation,
24
+ type TrackScore,
25
+ } from "../../core/score.ts";
26
+ import { rebaseOperations } from "../session/rebase.ts";
27
+ import { musicalReceipt } from "../session/receipt.ts";
20
28
  import {
21
29
  isIdentityRewind,
22
30
  rewindComposition,
@@ -27,6 +35,9 @@ export type HistoryEvent = Readonly<{
27
35
  revision: number;
28
36
  kind: string;
29
37
  rewind?: Rewind | undefined;
38
+ payload?: unknown;
39
+ /** Who wrote it; per-pane undo keys on `clientId` (design §12.6). */
40
+ actor?: Readonly<{ clientId: string }> | undefined;
30
41
  }>;
31
42
 
32
43
  export type HistoryTarget = Readonly<{
@@ -39,31 +50,149 @@ export type HistoryTarget = Readonly<{
39
50
  export const UNDO_KIND = "score.undo";
40
51
  export const REDO_KIND = "score.redo";
41
52
 
42
- export function historyTarget(
43
- composition: unknown,
53
+ /**
54
+ * Replays the log into undo and redo stacks of event indexes, over the
55
+ * events `include` admits. An undo or redo event pops its stack only when
56
+ * it stepped that stack's top (its payload names the revision); otherwise
57
+ * it is an ordinary edit there. So a pane's own undo is an edit in the
58
+ * global history (`undo all` can undo it), and an `undo all` that undid
59
+ * another pane's edit is an edit in this pane's history. Old events without
60
+ * the payload revision pop unconditionally, as they always did.
61
+ */
62
+ function historyStacks(
44
63
  events: readonly HistoryEvent[],
45
- direction: "undo" | "redo",
46
- ): HistoryTarget | undefined {
64
+ include: (event: HistoryEvent) => boolean,
65
+ ): { undo: number[]; redo: number[] } {
47
66
  const undo: number[] = [];
48
67
  const redo: number[] = [];
68
+ const steps = (stack: number[], event: HistoryEvent, key: string) => {
69
+ const top = stack.at(-1);
70
+ if (top === undefined) return false;
71
+ const named = (event.payload as Record<string, unknown> | null)?.[key];
72
+ return typeof named !== "number" || named === events[top]!.revision;
73
+ };
49
74
  events.forEach((event, index) => {
50
- if (!changesComposition(event)) return;
51
- if (event.kind === UNDO_KIND) {
52
- if (undo.pop() !== undefined) redo.push(index);
53
- } else if (event.kind === REDO_KIND) {
54
- if (redo.pop() !== undefined) undo.push(index);
75
+ if (!changesComposition(event) || !include(event)) return;
76
+ if (event.kind === UNDO_KIND && steps(undo, event, "undoneRevision")) {
77
+ undo.pop();
78
+ redo.push(index);
79
+ } else if (
80
+ event.kind === REDO_KIND &&
81
+ steps(redo, event, "redoneRevision")
82
+ ) {
83
+ redo.pop();
84
+ undo.push(index);
55
85
  } else {
56
86
  undo.push(index);
57
87
  redo.length = 0;
58
88
  }
59
89
  });
60
- const top = (direction === "undo" ? undo : redo).at(-1);
90
+ return { undo, redo };
91
+ }
92
+
93
+ /** `undo all` / `redo all`: the global history, whoever made each edit. */
94
+ export function historyTarget(
95
+ composition: unknown,
96
+ events: readonly HistoryEvent[],
97
+ direction: "undo" | "redo",
98
+ ): HistoryTarget | undefined {
99
+ const stacks = historyStacks(events, () => true);
100
+ const top = stacks[direction].at(-1);
61
101
  if (top === undefined) return undefined;
62
102
  const restored = rewindComposition(composition, events, top);
63
103
  if (restored === undefined) return undefined;
64
104
  return { revision: events[top]!.revision, composition: restored };
65
105
  }
66
106
 
107
+ export type PaneStep =
108
+ | Readonly<{ ok: true; revision: number; next: TrackScore }>
109
+ | Readonly<{
110
+ ok: false;
111
+ revision: number;
112
+ /** Why the inverse cannot land (rebase reason or session-wide). */
113
+ reason: string;
114
+ /** Other clients that changed the composition since, oldest first. */
115
+ others: readonly string[];
116
+ }>;
117
+
118
+ /**
119
+ * Per-pane undo and redo (design §12.6): the newest edit `clientId` made,
120
+ * inverted as score operations and rebased onto the current score with the
121
+ * same touch rules as agent intents. Another pane's later edit to the same
122
+ * note or track property refuses with a reason; unrelated edits stay.
123
+ * Undefined when this pane has nothing to step.
124
+ */
125
+ export function paneHistoryStep(
126
+ composition: unknown,
127
+ events: readonly HistoryEvent[],
128
+ direction: "undo" | "redo",
129
+ clientId: string,
130
+ ): PaneStep | undefined {
131
+ const stacks = historyStacks(
132
+ events,
133
+ (event) => event.actor?.clientId === clientId,
134
+ );
135
+ const top = stacks[direction].at(-1);
136
+ if (top === undefined) return undefined;
137
+ const revision = events[top]!.revision;
138
+ const others = [
139
+ ...new Set(
140
+ events
141
+ .slice(top + 1)
142
+ .filter(
143
+ (event) =>
144
+ changesComposition(event) && event.actor?.clientId !== clientId,
145
+ )
146
+ .map((event) => event.actor?.clientId ?? ""),
147
+ ),
148
+ ];
149
+ const refuse = (reason: string): PaneStep => ({
150
+ ok: false,
151
+ revision,
152
+ reason,
153
+ others,
154
+ });
155
+ const before = rewindComposition(composition, events, top);
156
+ const after =
157
+ top + 1 === events.length
158
+ ? composition
159
+ : rewindComposition(composition, events, top + 1);
160
+ if (before === undefined || after === undefined)
161
+ return refuse("history was compacted");
162
+ let operations: readonly ScoreOperation[];
163
+ let base: TrackScore;
164
+ let current: TrackScore;
165
+ try {
166
+ base = scoreFromJSON(after);
167
+ current = scoreFromJSON(composition);
168
+ operations = diffScores(base, scoreFromJSON(before));
169
+ } catch (error) {
170
+ if (error instanceof DiffError) return refuse("session-wide");
171
+ return refuse(error instanceof Error ? error.message : String(error));
172
+ }
173
+ if (operations.length === 0) return refuse("session-wide");
174
+ const result = rebaseOperations(base, current, operations);
175
+ return result.ok
176
+ ? { ok: true, revision, next: result.next }
177
+ : refuse(result.reason);
178
+ }
179
+
67
180
  function changesComposition(event: HistoryEvent): boolean {
68
181
  return event.rewind !== undefined && !isIdentityRewind(event.rewind);
69
182
  }
183
+
184
+ /**
185
+ * The card after undo or redo names the musical change it made
186
+ * (`undid · −1 note on bass (C3)`, `redid · 96 BPM`), and falls back to the
187
+ * revision when the change is not in the music (a rename, a section label).
188
+ */
189
+ export function historyReceipt(
190
+ direction: "undo" | "redo",
191
+ before: TrackScore,
192
+ after: TrackScore,
193
+ revision: number,
194
+ ): string {
195
+ const verb = direction === "undo" ? "undid" : "redid";
196
+ const change = musicalReceipt(before, after);
197
+ return change ? `${verb} · ${change}` : `${verb} · rev ${revision}`;
198
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The modelled piano commands (0.6 keys lane, `core/keys.ts`):
2
+ * The modeled piano commands (0.6 keys lane, `core/keys.ts`):
3
3
  *
4
- * piano the focused track becomes the modelled grand
4
+ * piano the focused track becomes the modeled grand
5
5
  * piano <preset> grand ballad upright felt lofi honkytonk prepared
6
6
  * grand | upright | felt … the same, by the preset word alone
7
7
  * keys list this track's piano settings
@@ -210,11 +210,28 @@ export function keysRowMismatch(
210
210
  return `${instrument} has no ${wrong.join(" ")} (${familiesReading(wrong[0]!).join("/")} row); its parameters: ${rows.join(" ")}`;
211
211
  }
212
212
 
213
+ /**
214
+ * `keys record` / `keys record replace` / `keys record off` (op1-ux §4.7):
215
+ * arm recording in PLAY, overdub or replace each pass. Not a score write
216
+ * of its own; each pass the take commits is.
217
+ */
218
+ export function parseRecordCommand(
219
+ prompt: string,
220
+ ): "overdub" | "replace" | "off" | undefined {
221
+ const match = prompt
222
+ .trim()
223
+ .match(/^\/?keys\s+record(?:\s+(overdub|replace|off))?$/i);
224
+ if (!match) return undefined;
225
+ return (match[1]?.toLowerCase() ?? "overdub") as
226
+ "overdub" | "replace" | "off";
227
+ }
228
+
229
+ /** Parse one keys / piano / organ command. */
213
230
  export function parseKeysCommand(prompt: string): KeysCommand | undefined {
214
231
  if (prompt.length > 1_024) return undefined;
215
232
  let words = prompt.trim().toLowerCase().split(/\s+/);
216
233
  // `instrument piano` / `sound grand`: the generic instrument command with
217
- // a keys word is a new write, so it stores the modelled piano.
234
+ // a keys word is a new write, so it stores the modeled piano.
218
235
  if (
219
236
  words.length === 2 &&
220
237
  ["instrument", "sound", "voice"].includes(words[0]!) &&
@@ -455,8 +472,8 @@ export function applyKeysCommand(
455
472
  const piano = isKeysFamily(track.instrument);
456
473
  const elsewhere =
457
474
  track.instrument === "organ"
458
- ? `keys · ${trackId} is the legacy organ; type tonewheel (or combo, pipe) for drawbars and rotary, or piano for the modelled piano`
459
- : `keys · ${trackId} is ${track.instrument}; type piano first (${PIANO_FAMILIES.join(" ")}), ${ELECTRIC_FAMILIES.join(" ")} or ${ORGAN_FAMILIES.join(" ")} for modelled keys`;
475
+ ? `keys · ${trackId} is the legacy organ; type tonewheel (or combo, pipe) for drawbars and rotary, or piano for the modeled piano`
476
+ : `keys · ${trackId} is ${track.instrument}; type piano first (${PIANO_FAMILIES.join(" ")}), ${ELECTRIC_FAMILIES.join(" ")} or ${ORGAN_FAMILIES.join(" ")} for modeled keys`;
460
477
  if (command.type === "keys-list")
461
478
  return {
462
479
  ok: true,
@@ -483,7 +500,7 @@ export function applyKeysCommand(
483
500
  if (track.sampler && command.type !== "keys-preset")
484
501
  return {
485
502
  ok: false,
486
- message: `keys · ${trackId} is a sampler track; type piano to make it a modelled piano`,
503
+ message: `keys · ${trackId} is a sampler track; type piano to make it a modeled piano`,
487
504
  };
488
505
  let patch: TrackPatch;
489
506
  if (command.type === "keys-preset") {
@@ -516,7 +533,7 @@ export function applyKeysCommand(
516
533
  return {
517
534
  ok: false,
518
535
  message: keysSynth
519
- ? `keys · keys shapes the modelled piano; ${trackId} is the keys synth: use synth ${first ?? "decay"} …, or type piano`
536
+ ? `keys · keys shapes the modeled piano; ${trackId} is the keys synth: use synth ${first ?? "decay"} …, or type piano`
520
537
  : elsewhere,
521
538
  };
522
539
  } else if (command.type === "keys-rotary-at") {
@@ -605,7 +622,7 @@ export function applyKeysCommand(
605
622
 
606
623
  /**
607
624
  * The fields a new track named after a piano word starts with
608
- * (`/track piano` → the modelled grand); empty for any other name. `lofi`
625
+ * (`/track piano` → the modeled grand); empty for any other name. `lofi`
609
626
  * is left out: it also names a drum kit and pattern.
610
627
  */
611
628
  export function newPianoTrack(name: string): {
@@ -86,7 +86,7 @@ export function parseModalCommand(prompt: string): ModalCommand | undefined {
86
86
  if (rest.length === 1 && rest[0] === "gamelan")
87
87
  return {
88
88
  type: "modal-hint",
89
- message: `gamelan · ${GAMELAN_PRESETS.join(" ")} · modal gangsa to start · pair two tracks with modal pair <track> · tuning slendro or pelog · ctrl-k Sound › browse sounds › Mallets and bells › Gamelan`,
89
+ message: `gamelan · ${GAMELAN_PRESETS.join(" ")} · modal gangsa to start · pair two tracks with modal pair <track> · tuning slendro or pelog · ctrl-k Sound › instruments › mallets › gamelan`,
90
90
  };
91
91
  if (rest[0] === "preset") {
92
92
  const preset = rest.length === 2 ? modalPresetFor(rest[1]!) : undefined;
@@ -83,7 +83,7 @@ export type EffectAutomationParameter = Exclude<
83
83
  "volume" | "pan"
84
84
  >;
85
85
 
86
- const LANE_ALIASES: Readonly<Record<string, EffectAutomationParameter>> =
86
+ export const LANE_ALIASES: Readonly<Record<string, EffectAutomationParameter>> =
87
87
  Object.freeze({
88
88
  filter: "filter",
89
89
  cutoff: "filter",
@@ -0,0 +1,53 @@
1
+ /**
2
+ * One vocabulary matcher for every "did you mean" in dawg: instruments,
3
+ * effects, styles, kits, rigs, sections, presets, params and verbs.
4
+ */
5
+
6
+ /**
7
+ * Optimal-string-alignment distance: insert, delete, substitute, and an
8
+ * adjacent swap (`/hlep` → `/help`) each cost one edit.
9
+ */
10
+ export function editDistance(a: string, b: string): number {
11
+ let before: number[] = [];
12
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
13
+ for (let i = 1; i <= a.length; i += 1) {
14
+ const row = [i];
15
+ for (let j = 1; j <= b.length; j += 1) {
16
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
17
+ let best = Math.min(
18
+ previous[j]! + 1,
19
+ row[j - 1]! + 1,
20
+ previous[j - 1]! + cost,
21
+ );
22
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
23
+ best = Math.min(best, before[j - 2]! + 1);
24
+ row.push(best);
25
+ }
26
+ before = previous;
27
+ previous = row;
28
+ }
29
+ return previous[b.length]!;
30
+ }
31
+
32
+ /**
33
+ * The word in `vocabulary` nearest to `word` (edit distance, adjacent swaps
34
+ * cost one), within one edit for words of four letters or fewer and two
35
+ * otherwise; undefined when nothing is that close or `word` is itself in it.
36
+ */
37
+ export function nearest(
38
+ word: string,
39
+ vocabulary: Iterable<string>,
40
+ ): string | undefined {
41
+ const typed = word.trim().toLowerCase();
42
+ if (!typed) return undefined;
43
+ const limit = typed.length <= 4 ? 1 : 2;
44
+ let best: { word: string; distance: number } | undefined;
45
+ for (const candidate of vocabulary) {
46
+ const lower = candidate.toLowerCase();
47
+ if (lower === typed) return undefined;
48
+ const distance = editDistance(typed, lower);
49
+ if (distance <= limit && (!best || distance < best.distance))
50
+ best = { word: candidate, distance };
51
+ }
52
+ return best?.word;
53
+ }
@@ -124,10 +124,10 @@ export type KitCommand =
124
124
 
125
125
  /** `/kit [bank]` (default kit) and `/kit list`; undefined for other commands. */
126
126
  export function parseKitCommand(command: string): KitCommand | undefined {
127
- const match = command.trim().match(/^\/kit(?:\s+(\S+))?\s*$/i);
127
+ const match = command.trim().match(/^\/?kits?(?:\s+(\S+))?\s*$/i);
128
128
  if (!match) return undefined;
129
129
  const bank = match[1];
130
- if (bank && /^(list|ls)$/i.test(bank)) return { kind: "list" };
130
+ if (bank && /^(list|ls|presets)$/i.test(bank)) return { kind: "list" };
131
131
  return { kind: "set", bank: bank ?? DEFAULT_KIT };
132
132
  }
133
133
 
@@ -389,6 +389,8 @@ export function kitListLines(aliases: BankAliases): string[] {
389
389
  export type CacheReport = Readonly<{
390
390
  packs: CacheUsage & { max: number };
391
391
  assets: CacheUsage & { max: number };
392
+ /** Pitch curves in `.dawg/analysis` (0.7); omitted when empty. */
393
+ analysis?: CacheUsage & { max: number };
392
394
  freed?: Readonly<{ files: number; bytes: number }>;
393
395
  }>;
394
396
 
@@ -408,6 +410,11 @@ export function cacheLines(report: CacheReport): string[] {
408
410
  : []),
409
411
  row("pack downloads", report.packs, "DAWG_PACKS_CACHE_MAX"),
410
412
  row("decoded audio (this project)", report.assets, "DAWG_ASSETS_CACHE_MAX"),
413
+ ...(report.analysis?.files
414
+ ? [
415
+ `pitch analysis (this project) · ${formatBytes(report.analysis.bytes)} of ${formatBytes(report.analysis.max)} · ${report.analysis.files} curve${report.analysis.files === 1 ? "" : "s"} · .dawg/analysis, re-tracked on demand`,
416
+ ]
417
+ : []),
411
418
  "least recently used files go first; this project's sounds are never evicted",
412
419
  "evicted sounds re-fetch by their pinned sha256 on next use",
413
420
  "/pack cache prune [size] · /pack cache clear",
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `✗ synth lpf 99999 · lpf takes 20…20000 Hz · synth lpf 1200`: the range
3
+ * card for a synth or effect parameter, built from the same specs that
4
+ * validate it (core/params), so the card and the check never disagree.
5
+ */
6
+ import type { ParamSpec } from "../../core/params.ts";
7
+ import { SYNTH_PARAMS, synthParamName } from "../../core/synth.ts";
8
+ import { effectSpec } from "../../core/fx.ts";
9
+ import { parseEffectName, parseParamName } from "./fx.ts";
10
+ import { usageError } from "./grammar.ts";
11
+
12
+ /** The range card for the first out-of-range number in the line, if any. */
13
+ export function paramRangeError(line: string): string | undefined {
14
+ const words = line.trim().replace(/^\//, "").split(/\s+/);
15
+ const verb = words[0]?.toLowerCase();
16
+ if (verb === "synth")
17
+ return firstOutOfRange(line, words.slice(1), "synth", (word) => {
18
+ const name = synthParamName(word);
19
+ return name ? [name, SYNTH_PARAMS[name]] : undefined;
20
+ });
21
+ if (verb === "fx" && words[1]) {
22
+ const effect = parseEffectName(words[1]);
23
+ if (!effect) return undefined;
24
+ const params = effectSpec(effect).params;
25
+ return firstOutOfRange(line, words.slice(2), `fx ${effect}`, (word) => {
26
+ const name = parseParamName(effect, word);
27
+ return name ? [name, params[name]] : undefined;
28
+ });
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ function firstOutOfRange(
34
+ line: string,
35
+ rest: readonly string[],
36
+ prefix: string,
37
+ lookup: (word: string) => [string, ParamSpec | undefined] | undefined,
38
+ ): string | undefined {
39
+ for (let index = 0; index + 1 < rest.length; index += 2) {
40
+ const found = lookup(rest[index]!);
41
+ if (!found) continue;
42
+ const [name, spec] = found;
43
+ if (spec?.kind !== "number") continue;
44
+ const value = Number(rest[index + 1]);
45
+ if (!Number.isFinite(value)) continue;
46
+ if (value >= spec.min && value <= spec.max) continue;
47
+ return usageError(line.trim(), {
48
+ command: name,
49
+ min: spec.min,
50
+ max: spec.max,
51
+ unit: spec.unit ?? "",
52
+ example: `${prefix} ${name} ${spec.default}`,
53
+ });
54
+ }
55
+ return undefined;
56
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Whether a prompt line is a command the human prompt bar runs locally (no
3
+ * model call): every command parser the prompt tries, in one list. The TUI
4
+ * uses it for typo fixes; show-me (src/agent/show-me.ts) uses it in tests to
5
+ * prove every command it ghost-types is one a human can type.
6
+ */
7
+ import type { TrackScore } from "../../core/score.ts";
8
+ import { parsePrompt } from "../agent/ops.ts";
9
+ import { parseSectionCommand } from "./arrange.ts";
10
+ import { parseRangeCommand } from "./range.ts";
11
+ import { parseAutotuneCommand } from "./autotune.ts";
12
+ import { parseClipCommand, parseLyricsCommand } from "./clips.ts";
13
+ import { parsePatternCommand } from "./drums.ts";
14
+ import { parseProgressionCommand } from "./progression.ts";
15
+ import { parseEditCommand } from "./edit.ts";
16
+ import { parseExpressionCommand } from "./expression.ts";
17
+ import { parseFitCommand } from "./fit.ts";
18
+ import { parseFormantCommand, parseVowelCommand } from "./formant.ts";
19
+ import { parseFxCommand } from "./fx.ts";
20
+ import { parseGranularCommand } from "./granular.ts";
21
+ import { parseKeysCommand, parseRecordCommand } from "./keys.ts";
22
+ import { parseMasterCommand } from "./master.ts";
23
+ import { parseModalCommand } from "./modal.ts";
24
+ import { parseMusicCommand } from "./music.ts";
25
+ import { parseKitCommand, parsePackCommand } from "./pack.ts";
26
+ import { parseResampleCommand } from "./resample.ts";
27
+ import { parseRhythmCommand } from "./rhythm.ts";
28
+ import { parseRigCommand } from "./rig.ts";
29
+ import { parseSampleCommand } from "./sample.ts";
30
+ import { parseShiftCommand } from "./shift.ts";
31
+ import { parseSingCommand } from "./sing.ts";
32
+ import { parseStringCommand } from "./string.ts";
33
+ import { parseGuitarCommand, parseStrumCommand } from "./strum.ts";
34
+ import { parseSynthCommand } from "./synth.ts";
35
+ import { parseTimeCommand } from "./time.ts";
36
+ import { parseTuningCommand } from "./tuning.ts";
37
+ import { parseVocalCommand } from "./vocal.ts";
38
+ import { parseVocoderCommand } from "./vocoder.ts";
39
+ import { parseWavetableCommand } from "./wavetable.ts";
40
+ import { parseWindCommand } from "./wind.ts";
41
+ import {
42
+ candidates,
43
+ parseExportCommand,
44
+ parseLoopCommand,
45
+ parseTrackEdit,
46
+ } from "./grammar.ts";
47
+
48
+ /** One parser's reading of a line: its name and what it parsed. */
49
+ export type ParsedCommand = Readonly<{ parser: string; value: unknown }>;
50
+
51
+ /**
52
+ * The first parser that takes `text` exactly as typed, with its result
53
+ * (no grammar rewrites): what the prompt bar would run for this spelling.
54
+ */
55
+ export function parseExact(
56
+ text: string,
57
+ score: TrackScore,
58
+ ): ParsedCommand | undefined {
59
+ for (const parse of parsers(score)) {
60
+ const value = parse(text);
61
+ if (value !== undefined)
62
+ return { parser: parse.name || "anonymous", value };
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ /**
68
+ * What `text` runs as: the exact spelling first, then the grammar's other
69
+ * readings (`/x` ≡ `x`, aliases, remove and list words) in order.
70
+ */
71
+ export function parseCommand(
72
+ text: string,
73
+ score: TrackScore,
74
+ ): ParsedCommand | undefined {
75
+ for (const line of [text, ...candidates(text)]) {
76
+ const parsed = parseExact(line, score);
77
+ if (parsed) return parsed;
78
+ }
79
+ return undefined;
80
+ }
81
+
82
+ /** Whether a line runs locally, in any spelling the grammar accepts. */
83
+ export function commandParses(text: string, score: TrackScore): boolean {
84
+ return parseCommand(text, score) !== undefined;
85
+ }
86
+
87
+ function parsers(score: TrackScore): ((text: string) => unknown)[] {
88
+ return [
89
+ function parseRange(value: string) {
90
+ return parseRangeCommand(value, score);
91
+ },
92
+ parsePrompt,
93
+ parseMusicCommand,
94
+ parseEditCommand,
95
+ parseRhythmCommand,
96
+ parseFxCommand,
97
+ parseSynthCommand,
98
+ parseStringCommand,
99
+ parseGranularCommand,
100
+ parseRecordCommand,
101
+ parseKeysCommand,
102
+ parseExpressionCommand,
103
+ parseMasterCommand,
104
+ function parseSection(value: string) {
105
+ return parseSectionCommand(value, score);
106
+ },
107
+ parsePatternCommand,
108
+ parseKitCommand,
109
+ parsePackCommand,
110
+ parseSampleCommand,
111
+ parseFitCommand,
112
+ parseShiftCommand,
113
+ parseResampleCommand,
114
+ parseWavetableCommand,
115
+ parseTimeCommand,
116
+ parseTuningCommand,
117
+ parseRigCommand,
118
+ parseModalCommand,
119
+ parseGuitarCommand,
120
+ parseStrumCommand,
121
+ parseProgressionCommand,
122
+ parseWindCommand,
123
+ parseAutotuneCommand,
124
+ parseSingCommand,
125
+ parseVocoderCommand,
126
+ parseVocalCommand,
127
+ parseFormantCommand,
128
+ parseVowelCommand,
129
+ parseClipCommand,
130
+ parseLyricsCommand,
131
+ parseExportCommand,
132
+ parseLoopCommand,
133
+ parseTrackEdit,
134
+ ];
135
+ }