@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,405 @@
1
+ /**
2
+ * 0.7 Voice menu rows. The Voice root (Ctrl-K › Voice) gathers every lane:
3
+ * sing, clips and lyrics, pitch, autotune, formant and vocoder, then the
4
+ * voice presets. It is always shown; on a track that is not a voice yet it
5
+ * opens on "turn this track into a voice". Effects keeps a "voice effects"
6
+ * row for formant and vocoder. Each lane fills only its own function below.
7
+ */
8
+ import { effectSpec } from "../../core/fx.ts";
9
+ import { effectValues } from "../commands/fx.ts";
10
+ import {
11
+ hzName,
12
+ pitchSummary,
13
+ pitchTargets,
14
+ pitchTraceFor,
15
+ } from "../commands/vocal-pitch.ts";
16
+ import {
17
+ effectParamNodes,
18
+ num,
19
+ type MenuContext,
20
+ type MenuNode,
21
+ } from "./menu.ts";
22
+ import {
23
+ singBrowseRows,
24
+ singParameterNodes,
25
+ singVowelNodes,
26
+ } from "./sing-menu.ts";
27
+ import { parseKey } from "../../core/chords.ts";
28
+ import type { Track } from "../../core/score.ts";
29
+ import { clipMenuRows, vocalBrowseRows } from "./menu-clips.ts";
30
+ import { vocoderBrowseNode, vocoderEffectNode } from "./vocoder-menu.ts";
31
+ import {
32
+ AUTOTUNE_PARAMS,
33
+ AUTOTUNE_PRESET_HELP,
34
+ AUTOTUNE_PRESETS,
35
+ AUTOTUNE_TARGETS,
36
+ AUTOTUNE_VOICES,
37
+ describeAutotune,
38
+ resolveAutotune,
39
+ } from "../../core/autotune.ts";
40
+ import { autotuneEngineNote } from "../audio/autotune.ts";
41
+
42
+ /** Voice: Clips and Lyrics (clips lane). */
43
+ export function clipsSoundRows(context: MenuContext): MenuNode[] {
44
+ return clipMenuRows(context);
45
+ }
46
+
47
+ /** Voice: Pitch with trace, detected key and Make notes (pitch lane). */
48
+ export function pitchSoundRows(context: MenuContext): MenuNode[] {
49
+ const track = context.score.tracks.find((t) => t.id === context.trackId);
50
+ if (!track || pitchTargets(track).length === 0) return [];
51
+ const summary = pitchSummary(track.id);
52
+ return [
53
+ {
54
+ kind: "menu",
55
+ id: "voice-pitch",
56
+ label: "pitch",
57
+ detail: summary
58
+ ? `${summary.key ?? "key unclear"} · ${hzName(summary.median)}`
59
+ : "detect key and melody",
60
+ help: "what the audio sings: key, median pitch, a trace on the highway, guide notes",
61
+ build: pitchMenuRows,
62
+ },
63
+ ];
64
+ }
65
+
66
+ /** Voice › Pitch. */
67
+ export function pitchMenuRows(context: MenuContext): MenuNode[] {
68
+ const summary = pitchSummary(context.trackId);
69
+ return [
70
+ {
71
+ kind: "action",
72
+ label: "analyze",
73
+ command: "/vocal pitch",
74
+ help: "track the pitch of the focused clip or sample (cached in .dawg/analysis)",
75
+ },
76
+ {
77
+ kind: "info",
78
+ label: "detected key",
79
+ value: summary ? (summary.key ?? "unclear") : "-",
80
+ help: summary
81
+ ? `from ${summary.label}`
82
+ : "Analyze first; the key comes from the notes the audio sings",
83
+ },
84
+ {
85
+ kind: "info",
86
+ label: "median pitch",
87
+ value: summary ? hzName(summary.median) : "-",
88
+ },
89
+ {
90
+ kind: "toggle",
91
+ label: "trace",
92
+ value: pitchTraceFor(context.trackId) !== undefined,
93
+ command: (on) => `/vocal pitch trace ${on ? "on" : "off"}`,
94
+ help: "a dotted line of the sung pitch on the highway; warning color past 15 cents off",
95
+ },
96
+ {
97
+ kind: "action",
98
+ label: "make notes",
99
+ command: "/vocal notes",
100
+ help: "a new guide-notes track with one note per sung note",
101
+ },
102
+ ];
103
+ }
104
+
105
+ /**
106
+ * Voice: Autotune (autotune lane). One sub-menu on a track with
107
+ * audio to tune (clips, a sampler, the `vocal` word, or autotune set): Preset (off or one of nine), To, From, Key, the AUTOTUNE_PARAMS
108
+ * numbers and Voice. Each row runs an `autotune …` command, so space
109
+ * auditions it with staged A/B and x puts a field back to the preset's.
110
+ */
111
+ export function autotuneSoundRows(context: MenuContext): MenuNode[] {
112
+ const track = context.score.tracks.find(
113
+ (candidate) => candidate.id === context.trackId,
114
+ );
115
+ // It tunes audio: clips and sampler voices (and a track already set).
116
+ if (
117
+ !track ||
118
+ !(
119
+ track.autotune ||
120
+ track.sampler ||
121
+ (track.clips?.length ?? 0) > 0 ||
122
+ track.instrument === "vocal"
123
+ )
124
+ )
125
+ return [];
126
+ const current = track.autotune;
127
+ return [
128
+ {
129
+ kind: "menu",
130
+ id: "voice:autotune",
131
+ label: "autotune",
132
+ detail: current
133
+ ? `${describeAutotune(current)}${autotuneEngineNote()}`
134
+ : "off",
135
+ help: "pitch correction on the track's clips and sampler voices, gentle to hard",
136
+ build: autotuneRows,
137
+ },
138
+ ];
139
+ }
140
+
141
+ function autotuneRows(context: MenuContext): MenuNode[] {
142
+ const track = context.score.tracks.find(
143
+ (candidate) => candidate.id === context.trackId,
144
+ );
145
+ if (!track) return [];
146
+ const current = track.autotune;
147
+ const resolved = current ? resolveAutotune(current) : undefined;
148
+ const base = resolveAutotune({ preset: current?.preset ?? "pop" });
149
+ const own = (current ?? {}) as Readonly<Record<string, unknown>>;
150
+ const nodes: MenuNode[] = [
151
+ {
152
+ kind: "choice",
153
+ label: "preset",
154
+ value: current ? (current.preset ?? "pop") : "off",
155
+ options: ["off", ...AUTOTUNE_PRESETS],
156
+ command: (option) =>
157
+ option === "off" ? "autotune off" : `autotune ${option}`,
158
+ help: current
159
+ ? AUTOTUNE_PRESET_HELP[current.preset ?? "pop"]
160
+ : "hard robot warble trap pop natural gentle guided locked",
161
+ },
162
+ {
163
+ kind: "choice",
164
+ label: "to",
165
+ value: own.to === undefined ? "preset" : String(own.to),
166
+ options: ["preset", ...AUTOTUNE_TARGETS],
167
+ command: (option) =>
168
+ option === "preset" ? "autotune to off" : `autotune to ${option}`,
169
+ help: `scale (song or track key and tuning), chromatic, chord timeline or another track's notes · preset ${base.to}`,
170
+ },
171
+ {
172
+ kind: "choice",
173
+ label: "from",
174
+ value: current?.from ?? "own notes",
175
+ options: [
176
+ "own notes",
177
+ ...context.score.tracks
178
+ .filter(
179
+ (other) => other.id !== track.id && other.instrument !== "kit",
180
+ )
181
+ .map((other) => other.id),
182
+ ],
183
+ command: (option) =>
184
+ option === "own notes"
185
+ ? "autotune from off"
186
+ : `autotune to notes ${option}`,
187
+ help: "the track whose notes guide the notes target; own notes uses this track's notes",
188
+ },
189
+ {
190
+ kind: "entry",
191
+ label: "key",
192
+ value: current?.key ?? (context.score.key ? "song" : "none (chromatic)"),
193
+ placeholder: "a key: A minor, D bayati, C# major",
194
+ command: (text) =>
195
+ text.trim() === "" || /^(song|off)$/i.test(text.trim())
196
+ ? "autotune key off"
197
+ : `autotune key ${text.trim()}`,
198
+ example: "/autotune key D bayati",
199
+ help: "the scale target's key; empty follows the song key",
200
+ },
201
+ ];
202
+ for (const spec of AUTOTUNE_PARAMS) {
203
+ const value = (resolved as Readonly<Record<string, unknown>> | undefined)?.[
204
+ spec.name
205
+ ] as number | undefined;
206
+ const fallback =
207
+ ((base as Readonly<Record<string, unknown>>)[spec.name] as
208
+ number | undefined) ?? spec.def;
209
+ nodes.push({
210
+ kind: "number",
211
+ label: spec.label.toLowerCase(),
212
+ value: current ? (value ?? fallback) : undefined,
213
+ min: spec.min,
214
+ max: spec.max,
215
+ step: (v, direction) =>
216
+ Math.min(
217
+ spec.max,
218
+ Math.max(
219
+ spec.min,
220
+ Math.round((v + spec.step * direction) / spec.step) * spec.step,
221
+ ),
222
+ ),
223
+ // glide is stored in seconds and reads in ms, like /glide.
224
+ format: (v) =>
225
+ spec.name === "glide"
226
+ ? `${Math.round(v * 1000)} ms`
227
+ : `${num(v)}${spec.unit ? ` ${spec.unit}` : ""}`,
228
+ command: (v) =>
229
+ spec.name === "glide"
230
+ ? `autotune glide ${Math.round(v * 1000)}ms`
231
+ : `autotune ${spec.name} ${num(v)}`,
232
+ off: "off",
233
+ start: fallback,
234
+ reset: `autotune ${spec.name} off`,
235
+ help: `${spec.help} · preset ${num(fallback)} · x resets`,
236
+ });
237
+ }
238
+ nodes.push({
239
+ kind: "choice",
240
+ label: "voice",
241
+ value: current?.voice ?? "auto",
242
+ options: AUTOTUNE_VOICES,
243
+ command: (option) => `autotune voice ${option}`,
244
+ help: "the tracking range: auto, or bass tenor alto soprano to stop octave slips",
245
+ });
246
+ if (current && Object.keys(current).some((key) => key !== "preset"))
247
+ nodes.push({
248
+ kind: "action",
249
+ label: "reset to preset",
250
+ command: "autotune reset",
251
+ help: "keep the preset, drop every override",
252
+ });
253
+ return nodes;
254
+ }
255
+
256
+ /**
257
+ * Effects > Voice: Formant (formant lane). The fx rows (on, preset, shift,
258
+ * mix, advanced, reset) are the shared effect rows, so left/right, x and
259
+ * the space audition behave as every other effect's.
260
+ */
261
+ export function formantEffectRows(context: MenuContext): MenuNode[] {
262
+ const track = context.score.tracks.find(
263
+ (candidate) => candidate.id === context.trackId,
264
+ );
265
+ if (!track) return [];
266
+ const values = effectValues(track, "formant");
267
+ return [
268
+ {
269
+ kind: "menu",
270
+ id: "formant",
271
+ label: "formant",
272
+ detail: values
273
+ ? `shift ${values.shift} st · mix ${values.mix}`
274
+ : "off · shift the throat, keep the pitch",
275
+ help: effectSpec("formant").doc,
276
+ build: (inner) => effectParamNodes(inner, "formant", false),
277
+ },
278
+ ];
279
+ }
280
+
281
+ /** Effects > Voice: Vocoder with a Source picker (vocoder lane). */
282
+ export function vocoderEffectRows(context: MenuContext): MenuNode[] {
283
+ return vocoderEffectNode(context);
284
+ }
285
+
286
+ /**
287
+ * Voice › voice presets: Vocal (clips), Choir, Solo and Throat
288
+ * (sing), Vocoder (vocoder).
289
+ */
290
+ export function voicesBrowseGroup(context: MenuContext): MenuNode[] {
291
+ return [
292
+ ...vocalBrowseRows(context),
293
+ ...singBrowseRows(),
294
+ ...vocoderBrowseNode(context),
295
+ ];
296
+ }
297
+
298
+ /** Every Voice row, in lane order. */
299
+ export function voiceSoundRows(context: MenuContext): MenuNode[] {
300
+ return [
301
+ ...clipsSoundRows(context),
302
+ ...pitchSoundRows(context),
303
+ ...autotuneSoundRows(context),
304
+ ];
305
+ }
306
+
307
+ /** Voice effects (formant, vocoder), in lane order; Effects links here. */
308
+ export function voiceEffectRows(context: MenuContext): MenuNode[] {
309
+ return [...formantEffectRows(context), ...vocoderEffectRows(context)];
310
+ }
311
+
312
+ /** True when the focused track already sings, speaks or carries vocals. */
313
+ export function isVoiceTrack(track: Track | undefined): boolean {
314
+ if (!track) return false;
315
+ return (
316
+ track.instrument === "vocal" ||
317
+ track.instrument === "sing" ||
318
+ (track.clips?.length ?? 0) > 0 ||
319
+ track.autotune !== undefined ||
320
+ track.vocoder !== undefined
321
+ );
322
+ }
323
+
324
+ /** The Voice root's detail: what the focused track's voice is now. */
325
+ export function voiceRootDetail(context: MenuContext): string {
326
+ const track = context.score.tracks.find((t) => t.id === context.trackId);
327
+ if (!isVoiceTrack(track)) return "turn this track into a voice";
328
+ const parts: string[] = [];
329
+ if (track!.instrument === "sing")
330
+ parts.push(`sing${track!.sing?.preset ? ` ${track!.sing.preset}` : ""}`);
331
+ const clips = track!.clips?.length ?? 0;
332
+ if (clips) parts.push(`${clips} clip${clips === 1 ? "" : "s"}`);
333
+ if (track!.autotune) parts.push("autotune");
334
+ if (track!.vocoder) parts.push("vocoder");
335
+ if (parts.length === 0) parts.push(track!.instrument);
336
+ return parts.join(" · ");
337
+ }
338
+
339
+ /** Ctrl-K › Voice › voice presets: vocal, choirs, solos, throat, vocoders. */
340
+ function voicePresetsNode(detail: string): MenuNode {
341
+ return {
342
+ kind: "menu",
343
+ id: "voice:presets",
344
+ label: "voice presets",
345
+ detail,
346
+ help: "vocal clips, sung choirs and solos, throat singing, vocoders",
347
+ build: voicesBrowseGroup,
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Ctrl-K › Voice. Always shown: a track that is not a voice yet opens on
353
+ * the voice presets ("turn this track into a voice"), then clips and lyrics
354
+ * so a take can be imported straight away.
355
+ */
356
+ export function voiceRootNodes(context: MenuContext): MenuNode[] {
357
+ const track = context.score.tracks.find((t) => t.id === context.trackId);
358
+ if (!track) return [];
359
+ const voice = isVoiceTrack(track);
360
+ const sing: MenuNode[] =
361
+ track.instrument === "sing"
362
+ ? [
363
+ {
364
+ kind: "menu",
365
+ id: "voice:sing",
366
+ label: "sing",
367
+ detail: track.sing?.preset ?? "default",
368
+ help: "the built-in singing voice: preset, vowel, voices, breath, formant, throat",
369
+ build: (inner) => {
370
+ const found = inner.score.tracks.find(
371
+ (t) => t.id === inner.trackId,
372
+ );
373
+ if (!found) return [];
374
+ // A bare `instrument sing` has no settings yet: show the
375
+ // defaults, so the first nudge writes them.
376
+ const now = found.sing ? found : { ...found, sing: {} };
377
+ const context = found.sing
378
+ ? inner
379
+ : {
380
+ ...inner,
381
+ score: inner.score.withTracks(
382
+ inner.score.tracks.map((t) =>
383
+ t.id === now.id ? now : t,
384
+ ),
385
+ ),
386
+ };
387
+ return [
388
+ ...singParameterNodes(
389
+ now,
390
+ parseKey(inner.score.key ?? undefined)?.tonic,
391
+ ),
392
+ ...singVowelNodes(context),
393
+ ];
394
+ },
395
+ },
396
+ ]
397
+ : [];
398
+ return [
399
+ ...(voice ? [] : [voicePresetsNode("turn this track into a voice")]),
400
+ ...sing,
401
+ ...voiceSoundRows(context),
402
+ ...voiceEffectRows(context),
403
+ ...(voice ? [voicePresetsNode("vocal, choir, solo, throat, vocoder")] : []),
404
+ ];
405
+ }