@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
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Ctrl-k menu rows for the sing engine (0.7): the Choir, Solo and Throat
3
+ * groups under Voice › voice presets, the SING_PARAMS rows under
4
+ * Sound › Parameters on a sing track (with a Throat sub-menu) and the
5
+ * Vowels row under Sound › Performance. Every row runs a `sing …` or
6
+ * `note vowel …` command, so it stages for the audition loop's A/B.
7
+ */
8
+ import type { EnumParam, NumberParam } from "../../core/params.ts";
9
+ import type { Note, Track } from "../../core/score.ts";
10
+ import {
11
+ DEFAULT_SING_PRESET,
12
+ resolveSing,
13
+ SING_GROUPS,
14
+ SING_PARAMS,
15
+ SING_PRESETS,
16
+ SING_PRESET_NAMES,
17
+ SING_THROAT_PARAMS,
18
+ SING_VOWELS,
19
+ singNoteName,
20
+ singDroneFollows,
21
+ singTrack,
22
+ type SingPreset,
23
+ } from "../../core/sing.ts";
24
+ import { num, specStep, type MenuContext, type MenuNode } from "./menu.ts";
25
+
26
+ function presetRow(name: SingPreset): MenuNode {
27
+ return {
28
+ kind: "action",
29
+ label: `${name.padEnd(9)} ${SING_PRESETS[name].doc}`,
30
+ command: `sing ${name}`,
31
+ help: SING_PRESETS[name].styles,
32
+ };
33
+ }
34
+
35
+ /** Voice › voice presets: choir, solo and throat (sing lane). */
36
+ export function singBrowseRows(): MenuNode[] {
37
+ return SING_GROUPS.map((group): MenuNode => ({
38
+ kind: "menu",
39
+ id: `voices:${group.label.toLowerCase()}`,
40
+ label: group.label.toLowerCase(),
41
+ detail: group.presets.slice(0, 4).join(", ") + " …",
42
+ help:
43
+ group.label === "Throat"
44
+ ? "Tuvan throat singing: a held drone, notes steer the overtone"
45
+ : `${group.label.toLowerCase()} on the built-in singing voice`,
46
+ build: () => group.presets.map(presetRow),
47
+ }));
48
+ }
49
+
50
+ /** Parameters shown first; the rest sit under "advanced". */
51
+ const SING_SIMPLE = Object.freeze([
52
+ "vowel",
53
+ "voice",
54
+ "voices",
55
+ "bright",
56
+ "breath",
57
+ "vibmod",
58
+ "formant",
59
+ "ring",
60
+ ] as const);
61
+
62
+ function numberRow(
63
+ name: string,
64
+ spec: NumberParam,
65
+ value: number,
66
+ fallback: number,
67
+ ): MenuNode {
68
+ return {
69
+ kind: "number",
70
+ label: name,
71
+ value,
72
+ min: spec.min,
73
+ max: spec.max,
74
+ step: specStep(spec),
75
+ format: (v) => (spec.unit ? `${num(v)} ${spec.unit}` : num(v)),
76
+ command: (v) => `sing ${name} ${num(v)}`,
77
+ reset: `sing ${name} off`,
78
+ help: `${spec.doc} · preset ${num(fallback)} · x resets`,
79
+ };
80
+ }
81
+
82
+ /** Every vowel and two-vowel morph, for the pickers. */
83
+ export function vowelOptions(): string[] {
84
+ const out: string[] = [...SING_VOWELS];
85
+ for (const from of SING_VOWELS)
86
+ for (const to of SING_VOWELS) if (from !== to) out.push(`${from}>${to}`);
87
+ return out;
88
+ }
89
+
90
+ /** Sound › Parameters rows on a sing track. */
91
+ export function singParameterNodes(track: Track, keyRoot?: number): MenuNode[] {
92
+ if (!singTrack(track)) return [];
93
+ const sing = track.sing!;
94
+ const follows = singDroneFollows(sing, keyRoot);
95
+ const resolved = resolveSing(sing, keyRoot) as unknown as Readonly<
96
+ Record<string, unknown>
97
+ >;
98
+ const fallback = SING_PRESETS[sing.preset ?? DEFAULT_SING_PRESET]
99
+ .settings as unknown as Readonly<Record<string, unknown>>;
100
+ const own = sing as Readonly<Record<string, unknown>>;
101
+ const row = (name: string): MenuNode | undefined => {
102
+ if (name === "vowel")
103
+ return {
104
+ kind: "choice",
105
+ label: "vowel",
106
+ value: String(resolved.vowel),
107
+ options: vowelOptions(),
108
+ command: (option) => `sing vowel ${option}`,
109
+ help: `${SING_PARAMS.vowel!.doc} · preset ${String(fallback.vowel)}`,
110
+ };
111
+ if (name === "drone") {
112
+ const drone = resolved.drone as number | undefined;
113
+ const spec = SING_PARAMS.drone as NumberParam;
114
+ return {
115
+ kind: "number",
116
+ label: "drone",
117
+ value: drone,
118
+ min: spec.min,
119
+ max: spec.max,
120
+ step: (v, direction) =>
121
+ Math.max(spec.min, Math.min(spec.max, Math.round(v) + direction)),
122
+ // The drone you hear: the preset's moved to the song key.
123
+ format: (v) =>
124
+ follows && v === drone ? `${singNoteName(v)} (key)` : singNoteName(v),
125
+ command: (v) => `sing drone ${singNoteName(v)}`,
126
+ off: "off (melody)",
127
+ start: 50,
128
+ reset: "sing drone off",
129
+ help: `${spec.doc} · x back to the preset`,
130
+ };
131
+ }
132
+ if (name === "harmonics") {
133
+ const [lo, hi] = resolved.harmonics as readonly [number, number];
134
+ const options = ["4-8", "4-12", "5-10", "6-12", "6-16", "8-16", "8-24"];
135
+ const value = `${lo}-${hi}`;
136
+ return {
137
+ kind: "choice",
138
+ label: "harmonics",
139
+ value,
140
+ options: options.includes(value) ? options : [value, ...options],
141
+ command: (option) => `sing harmonics ${option}`,
142
+ help: "the overtone range a throat track's notes pick from (khoomei 6-12, sygyt 8-16)",
143
+ };
144
+ }
145
+ const spec = SING_PARAMS[name]!;
146
+ if (spec.kind === "enum")
147
+ return {
148
+ kind: "choice",
149
+ label: name,
150
+ value: own[name] === undefined ? "preset" : String(own[name]),
151
+ options: ["preset", ...(spec as EnumParam).values],
152
+ command: (option) =>
153
+ option === "preset" ? `sing ${name} unset` : `sing ${name} ${option}`,
154
+ help: `${spec.doc} · preset ${String(fallback[name])}`,
155
+ };
156
+ if (spec.kind !== "number") return undefined;
157
+ return numberRow(
158
+ name,
159
+ spec,
160
+ Number(resolved[name]),
161
+ Number(fallback[name]),
162
+ );
163
+ };
164
+ const nodes: MenuNode[] = [
165
+ {
166
+ kind: "choice",
167
+ label: "preset",
168
+ value: sing.preset ?? DEFAULT_SING_PRESET,
169
+ options: SING_PRESET_NAMES,
170
+ command: (option) => `sing preset ${option}`,
171
+ help: "the voice: choir, solo or throat singing",
172
+ },
173
+ ];
174
+ for (const name of SING_SIMPLE) {
175
+ const node = row(name);
176
+ if (node) nodes.push(node);
177
+ }
178
+ const throat = SING_THROAT_PARAMS as readonly string[];
179
+ nodes.push({
180
+ kind: "menu",
181
+ id: "sing:throat",
182
+ label: "throat",
183
+ detail:
184
+ resolved.drone === undefined
185
+ ? "off · drone, overtone, harmonics, sub"
186
+ : `drone ${singNoteName(resolved.drone as number)} · ${throat.slice(1).join(", ")}`,
187
+ help: "throat singing: a drone, notes pick its overtones; sub for kargyraa",
188
+ build: () => throat.flatMap((name) => row(name) ?? []),
189
+ });
190
+ const rest = Object.keys(SING_PARAMS).filter(
191
+ (name) =>
192
+ !(SING_SIMPLE as readonly string[]).includes(name) &&
193
+ !throat.includes(name),
194
+ );
195
+ nodes.push({
196
+ kind: "menu",
197
+ id: "sing:advanced",
198
+ label: "advanced",
199
+ detail: rest.join(", "),
200
+ help: "every sing parameter",
201
+ build: () => rest.flatMap((name) => row(name) ?? []),
202
+ });
203
+ if (Object.keys(sing).some((key) => key !== "preset"))
204
+ nodes.push({
205
+ kind: "action",
206
+ label: "reset to preset",
207
+ command: "sing reset",
208
+ help: "clear overrides, keep the preset",
209
+ });
210
+ return nodes;
211
+ }
212
+
213
+ function sharedVowel(notes: readonly Note[]): string {
214
+ const values = new Set(notes.map((note) => note.vowel ?? "track"));
215
+ if (values.size === 0) return "track";
216
+ return values.size === 1 ? [...values][0]! : "mixed";
217
+ }
218
+
219
+ /**
220
+ * Sound › Performance › Vowels on a sing track: left/right steps the notes'
221
+ * vowel through a e i o u, enter opens the morph picker, and a pattern
222
+ * row cycles vowels over the notes in order.
223
+ */
224
+ export function singVowelNodes(context: MenuContext): MenuNode[] {
225
+ const track = context.score.tracks.find(
226
+ (item) => item.id === context.trackId,
227
+ );
228
+ if (!singTrack(track)) return [];
229
+ const notes = context.score.notes.filter(
230
+ (note) => note.trackId === track!.id,
231
+ );
232
+ const current = sharedVowel(notes);
233
+ return [
234
+ {
235
+ kind: "menu",
236
+ id: "sing:vowels",
237
+ label: "vowels",
238
+ detail: `${current} · ${notes.length} notes`,
239
+ help: "the sung vowel of every note on the track (a note's vowel wins over the track's)",
240
+ build: (inner) => {
241
+ const now = inner.score.notes.filter(
242
+ (note) => note.trackId === context.trackId,
243
+ );
244
+ return [
245
+ {
246
+ kind: "choice",
247
+ label: "vowel",
248
+ value: sharedVowel(now),
249
+ options: ["track", ...SING_VOWELS],
250
+ command: (option) =>
251
+ `note vowel ${option === "track" ? "off" : option}`,
252
+ help: "left/right steps a e i o u; track uses the track's vowel",
253
+ },
254
+ {
255
+ kind: "choice",
256
+ label: "morph",
257
+ value: sharedVowel(now),
258
+ options: [
259
+ "track",
260
+ ...vowelOptions().filter((v) => v.includes(">")),
261
+ ],
262
+ command: (option) =>
263
+ `note vowel ${option === "track" ? "off" : option}`,
264
+ help: "each note travels from the first vowel to the second",
265
+ },
266
+ {
267
+ kind: "choice",
268
+ label: "pattern",
269
+ value: "",
270
+ options: ["a e i o u", "a o", "o u", "a e i", "u o a"],
271
+ command: (option) => `sing vowels ${option}`,
272
+ help: "vowels over the notes in time order, cycled",
273
+ },
274
+ ];
275
+ },
276
+ },
277
+ ];
278
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * ctrl-k › Style: the style taxonomy as a menu tree (families, then each
3
+ * branch down to its leaves). Every row is a `style` command, so the menu
4
+ * and the prompt share one grammar; typing filters the rows of a level.
5
+ */
6
+ import { hasCard, STYLE_TREE } from "../../core/styles/index.ts";
7
+ import { STYLE_FAMILIES } from "../../core/styles/taxonomy.ts";
8
+ import type { StyleId } from "../../core/styles/schema.ts";
9
+ import type { MenuContext, MenuNode } from "./menu.ts";
10
+
11
+ const BAR_CHOICES = [4, 8, 16, 32] as const;
12
+
13
+ /** The Style row's detail: the song's style, or a hint. */
14
+ export function styleMenuDetail(context: MenuContext): string {
15
+ const style = context.score.style;
16
+ if (!style) return "a whole song from a style";
17
+ const name = style.blend ? `${style.id} + ${style.blend.id}` : style.id;
18
+ return `${name} · seed ${style.seed}`;
19
+ }
20
+
21
+ function styleActions(id: StyleId): MenuNode[] {
22
+ const node = STYLE_TREE.get(id)!;
23
+ return [
24
+ ...BAR_CHOICES.map((bars): MenuNode => ({
25
+ kind: "action",
26
+ label: `make ${bars} bars`,
27
+ command: `/style ${id} ${bars}`,
28
+ help: `replace the song with ${bars} bars of ${node.title} (undo restores it)`,
29
+ })),
30
+ {
31
+ kind: "action",
32
+ label: "about",
33
+ command: `/style info ${id}`,
34
+ help: "meter, tempo, groove, tuning, harmony and roles",
35
+ },
36
+ ];
37
+ }
38
+
39
+ function styleNode(id: StyleId): MenuNode {
40
+ const node = STYLE_TREE.get(id)!;
41
+ const card = hasCard(id) ? "" : " · inherits";
42
+ return {
43
+ kind: "menu",
44
+ id: `style:${id}`,
45
+ label: node.title,
46
+ detail: node.leaf
47
+ ? `${id}${card}`
48
+ : `${node.children.length} styles${card}`,
49
+ help: node.region.length ? `${id} · ${node.region.join(", ")}` : id,
50
+ build: () => [
51
+ ...styleActions(id),
52
+ ...node.children.map((child) => styleNode(child)),
53
+ ],
54
+ };
55
+ }
56
+
57
+ /** Style: again, find, then the families. */
58
+ export function styleMenuNodes(context: MenuContext): MenuNode[] {
59
+ const current = context.score.style;
60
+ return [
61
+ ...(current
62
+ ? [
63
+ {
64
+ kind: "action",
65
+ label: "again",
66
+ command: "/style again",
67
+ help: `${current.id} with the next seed`,
68
+ } satisfies MenuNode,
69
+ ]
70
+ : []),
71
+ {
72
+ kind: "entry",
73
+ label: "find",
74
+ value: "",
75
+ placeholder: "style name, alias or region",
76
+ command: (text) =>
77
+ text.trim() ? `/style search ${text.trim()}` : undefined,
78
+ example: "/style search samba",
79
+ help: "search every style by name, alias or region",
80
+ },
81
+ {
82
+ kind: "entry",
83
+ label: "blend",
84
+ value: "",
85
+ placeholder: "a b [weight]",
86
+ command: (text) => {
87
+ const words = text.trim().split(/\s+/).filter(Boolean);
88
+ return words.length >= 2 && words.length <= 3
89
+ ? `/style blend ${words.join(" ")}`
90
+ : undefined;
91
+ },
92
+ example: "/style blend bebop bossa-nova 0.3",
93
+ help: "mix two styles; weight 0 is the first, 1 the second",
94
+ },
95
+ ...STYLE_FAMILIES.map((family): MenuNode => ({
96
+ kind: "menu",
97
+ id: `family:${family.key}`,
98
+ label: family.title,
99
+ detail: family.roots.join(" · "),
100
+ help: `${family.key} family`,
101
+ build: () => family.roots.map((root) => styleNode(root)),
102
+ })),
103
+ ];
104
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Ctrl-k rows for the vocoder (0.7, vocoder.md §8): Effects › Voice ›
3
+ * Vocoder (Source picker, Preset, one row per VOCODER_PARAMS key in table
4
+ * order) and Voice › voice presets › Vocoder. Every row runs a
5
+ * `vocoder …` command, so it stages for the audition loop like any other.
6
+ */
7
+ import type { NumberParam } from "../../core/params.ts";
8
+ import type { Track } from "../../core/score.ts";
9
+ import {
10
+ DEFAULT_VOCODER_PRESET,
11
+ VOCODER_INSTRUMENT,
12
+ VOCODER_PARAMS,
13
+ VOCODER_PRESETS,
14
+ VOCODER_PRESET_NAMES,
15
+ resolveVocoder,
16
+ vocoderPresetOf,
17
+ type TrackVocoder,
18
+ } from "../../core/vocoder.ts";
19
+ import {
20
+ vocoderEntryLines,
21
+ trackHasAudio,
22
+ vocoderCandidates,
23
+ } from "../commands/vocoder.ts";
24
+ import { num, specStep, type MenuContext, type MenuNode } from "./menu.ts";
25
+
26
+ const NONE = "none";
27
+
28
+ function trackOf(context: MenuContext): Track | undefined {
29
+ return context.score.tracks.find((track) => track.id === context.trackId);
30
+ }
31
+
32
+ /** One row per VOCODER_PARAMS key; x and `preset` return it to the preset. */
33
+ function paramRow(vocoder: TrackVocoder, name: string): MenuNode {
34
+ const spec = VOCODER_PARAMS[name]!;
35
+ const own = (vocoder as Record<string, unknown>)[name];
36
+ const preset = vocoderPresetOf(vocoder);
37
+ const fallback = (VOCODER_PRESETS[preset].values as Record<string, unknown>)[
38
+ name
39
+ ];
40
+ const resolved = (resolveVocoder(vocoder) as Record<string, unknown>)[name];
41
+ if (spec.kind === "enum")
42
+ return {
43
+ kind: "choice",
44
+ label: name,
45
+ value: own === undefined ? "preset" : String(own),
46
+ options: ["preset", ...spec.values],
47
+ command: (option) =>
48
+ option === "preset"
49
+ ? `vocoder ${name} reset`
50
+ : `vocoder ${name} ${option}`,
51
+ help: `${spec.doc} · preset ${String(resolved)}`,
52
+ };
53
+ if (spec.kind === "boolean")
54
+ return {
55
+ kind: "choice",
56
+ label: name,
57
+ value: own === undefined ? "preset" : own ? "on" : "off",
58
+ options: ["preset", "on", "off"],
59
+ command: (option) =>
60
+ option === "preset"
61
+ ? `vocoder ${name} reset`
62
+ : `vocoder ${name} ${option}`,
63
+ help: `${spec.doc} · preset ${fallback ? "on" : "off"}`,
64
+ };
65
+ const number = spec as NumberParam;
66
+ // gate's preset value is "auto" (from the source's assets): show it as off.
67
+ const value = typeof resolved === "number" ? resolved : undefined;
68
+ return {
69
+ kind: "number",
70
+ label: name,
71
+ value,
72
+ min: number.min,
73
+ max: number.max,
74
+ step: specStep(number),
75
+ format: (v) => (number.unit ? `${num(v)} ${number.unit}` : num(v)),
76
+ command: (v) => `vocoder ${name} ${num(v)}`,
77
+ reset: `vocoder ${name} reset`,
78
+ ...(value === undefined ? { off: "auto", start: number.min } : {}),
79
+ help: `${number.doc} · preset ${typeof fallback === "number" ? num(fallback) : String(resolved ?? "auto")} · x resets`,
80
+ };
81
+ }
82
+
83
+ /** A source as the picker shows it: its name, or its id when names clash. */
84
+ function sourceLabel(track: Track, all: readonly Track[]): string {
85
+ const clash = all.some((t) => t.id !== track.id && t.name === track.name);
86
+ return clash || !track.name.trim() ? track.id : track.name;
87
+ }
88
+
89
+ /** `vocoder src <name>`, quoted when the name has spaces. */
90
+ function srcCommand(label: string): string {
91
+ return /\s/.test(label) ? `vocoder src "${label}"` : `vocoder src ${label}`;
92
+ }
93
+
94
+ function entryRows(): MenuNode[] {
95
+ return vocoderEntryLines().map((line, index) => ({
96
+ kind: "info",
97
+ label: index === 0 ? "no voice yet" : "",
98
+ value: line.trim(),
99
+ }));
100
+ }
101
+
102
+ /** The rows inside Effects › Voice › Vocoder for the focused track. */
103
+ export function vocoderRows(context: MenuContext): MenuNode[] {
104
+ const track = trackOf(context);
105
+ if (!track || track.instrument === "kit") return [];
106
+ const vocoder = track.vocoder;
107
+ const candidates = vocoderCandidates(context.score, track.id);
108
+ const all = context.score.tracks;
109
+ if (!vocoder) {
110
+ if (trackHasAudio(track))
111
+ return [
112
+ {
113
+ kind: "action",
114
+ label: "vocode",
115
+ command: "vocoder",
116
+ help: "adds a vocoder track driven by this voice (follows the song's chords, or drones on the key's root); mutes it",
117
+ },
118
+ ];
119
+ if (candidates.length === 0) return entryRows();
120
+ return [
121
+ {
122
+ kind: "choice",
123
+ label: "source",
124
+ value: NONE,
125
+ options: [NONE, ...candidates.map((t) => sourceLabel(t, all))],
126
+ command: (option) =>
127
+ option === NONE ? "vocoder off" : srcCommand(option),
128
+ help: "the voice that shapes this track's sound",
129
+ },
130
+ ];
131
+ }
132
+ const others = context.score.tracks.filter(
133
+ (t) => t.id !== track.id && t.instrument !== "kit",
134
+ );
135
+ const options = [
136
+ ...candidates.map((t) => sourceLabel(t, all)),
137
+ ...others
138
+ .filter((t) => !candidates.some((c) => c.id === t.id))
139
+ .map((t) => sourceLabel(t, all)),
140
+ ];
141
+ const current = all.find((t) => t.id === vocoder.src);
142
+ const nodes: MenuNode[] = [
143
+ {
144
+ kind: "choice",
145
+ label: "source",
146
+ value: current ? sourceLabel(current, all) : (vocoder.src ?? NONE),
147
+ options: vocoder.src === undefined ? [NONE, ...options] : options,
148
+ command: (option) => (option === NONE ? "vocoder" : srcCommand(option)),
149
+ help: "the modulator; it is heard through the vocoder even when muted",
150
+ },
151
+ {
152
+ kind: "choice",
153
+ label: "preset",
154
+ value: vocoder.preset ?? DEFAULT_VOCODER_PRESET,
155
+ options: VOCODER_PRESET_NAMES,
156
+ command: (option) => `vocoder preset ${option}`,
157
+ help: "classic robot talkbox choir glass whisper smear lofi · keeps your overrides",
158
+ },
159
+ ...Object.keys(VOCODER_PARAMS).map((name) => paramRow(vocoder, name)),
160
+ ];
161
+ if (Object.keys(vocoder).some((key) => key !== "preset" && key !== "src"))
162
+ nodes.push({
163
+ kind: "action",
164
+ label: "reset to preset",
165
+ command: "vocoder reset",
166
+ help: "clear overrides, keep the source and preset",
167
+ });
168
+ nodes.push({
169
+ kind: "action",
170
+ label: "remove the vocoder",
171
+ command: "vocoder off",
172
+ help: "the track plays its own sound again",
173
+ });
174
+ return nodes;
175
+ }
176
+
177
+ /** Effects › Voice › Vocoder. */
178
+ export function vocoderEffectNode(context: MenuContext): MenuNode[] {
179
+ const track = trackOf(context);
180
+ if (!track || track.instrument === "kit") return [];
181
+ const vocoder = track.vocoder;
182
+ return [
183
+ {
184
+ kind: "menu",
185
+ id: "voice:vocoder",
186
+ label: "vocoder",
187
+ detail: vocoder
188
+ ? `${vocoder.preset ?? DEFAULT_VOCODER_PRESET}${vocoder.src ? ` · from ${vocoder.src}` : " · no source"}`
189
+ : trackHasAudio(track)
190
+ ? "vocode"
191
+ : "off",
192
+ help: "a voice shapes this track's sound: robot, talkbox, choir",
193
+ build: vocoderRows,
194
+ },
195
+ ];
196
+ }
197
+
198
+ /** Voice › voice presets › Vocoder: the built-in carrier. */
199
+ export function vocoderBrowseNode(context: MenuContext): MenuNode[] {
200
+ const track = trackOf(context);
201
+ return [
202
+ {
203
+ kind: "menu",
204
+ id: "voices:vocoder",
205
+ label: "vocoder",
206
+ detail: `${VOCODER_PRESET_NAMES.length} presets · robot, talkbox, choir …`,
207
+ help: "a built-in synth carrier spoken through a voice track",
208
+ build: (inner) => {
209
+ const here = trackOf(inner) ?? track;
210
+ if (here && trackHasAudio(here))
211
+ return [
212
+ {
213
+ kind: "action",
214
+ label: "vocode",
215
+ command: "vocoder",
216
+ help: "adds a vocoder track driven by this voice",
217
+ },
218
+ ];
219
+ if (here?.instrument !== VOCODER_INSTRUMENT)
220
+ return [
221
+ {
222
+ kind: "action",
223
+ label: "make a vocoder",
224
+ command: `instrument ${VOCODER_INSTRUMENT}`,
225
+ help: "a supersaw carrier; then pick its presets and source here",
226
+ },
227
+ ...(vocoderCandidates(inner.score, inner.trackId).length === 0
228
+ ? entryRows()
229
+ : []),
230
+ ];
231
+ return VOCODER_PRESET_NAMES.map((name): MenuNode => ({
232
+ kind: "action",
233
+ label: `${name.padEnd(8)} ${VOCODER_PRESETS[name].doc}`,
234
+ command: `vocoder ${name}`,
235
+ help: VOCODER_PRESETS[name].doc,
236
+ })).concat(
237
+ vocoderCandidates(inner.score, inner.trackId).length === 0
238
+ ? entryRows()
239
+ : vocoderRows(inner).filter((row) => row.label === "source"),
240
+ );
241
+ },
242
+ },
243
+ ];
244
+ }