@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
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Which value each of the four knobs turns, per page (design §7.3): one
3
+ * table, read by the fader drawer's front page, the mixer page and euclid.
4
+ * A row is four menu paths from the Ctrl-K root (`sound/attack`,
5
+ * `effects/reverb/mix`, `mix/volume`): segments match a submenu's id or a
6
+ * row's label, and `more effects` is searched on the way. An undefined slot
7
+ * shows as `·` and ↑↓ skips it.
8
+ *
9
+ * The rule (tui/knobs.ts): blue moves, green sizes, white shapes, orange
10
+ * level. Pages not in the table (an effect without a row) derive one: their
11
+ * first three number rows on blue, green and white, and `mix` on orange.
12
+ *
13
+ * Data, not code, so the patcher can map a patch's macros onto the same
14
+ * four knobs by adding rows (`patch:<id>`), and a test checks every path
15
+ * resolves for every instrument family. A `*` at the end of a segment
16
+ * matches the first submenu whose id starts with it (`sample:*`).
17
+ */
18
+ import { isWavetableInstrument, type Track } from "../../core/score.ts";
19
+ import { isDrumInstrument } from "../../core/drums.ts";
20
+ import {
21
+ isElectricFamily,
22
+ isKeysFamily,
23
+ isOrganFamily,
24
+ } from "../../core/keys.ts";
25
+ import { isStringTrack } from "../../core/strings.ts";
26
+ import { isGranularInstrument } from "../../core/granular.ts";
27
+
28
+ export type KnobRef = string | undefined;
29
+ export type KnobMap = readonly [KnobRef, KnobRef, KnobRef, KnobRef];
30
+
31
+ /**
32
+ * Pages, by id. `sound:<family>` rows use each engine's own param names (the
33
+ * rows its Sound menu shows); orange is the track level on every sound page,
34
+ * so ◆ always means "how loud".
35
+ */
36
+ export const KNOB_MAPS: Readonly<Record<string, KnobMap>> = Object.freeze({
37
+ "sound:synth": [
38
+ "sound/preset",
39
+ "sound/attack",
40
+ "sound/synth filter",
41
+ "mix/volume",
42
+ ],
43
+ "sound:wavetable": [
44
+ "sound/position (wt)",
45
+ "sound/attack",
46
+ "sound/synth filter",
47
+ "mix/volume",
48
+ ],
49
+ "sound:piano": [
50
+ "sound/preset",
51
+ "sound/decay",
52
+ "sound/hardness",
53
+ "mix/volume",
54
+ ],
55
+ "sound:electric": ["sound/preset", "sound/bell", "sound/tone", "mix/volume"],
56
+ "sound:organ": ["sound/preset", "sound/perc", "sound/drive", "mix/volume"],
57
+ "sound:string": [
58
+ "sound/preset",
59
+ "sound/ring s",
60
+ "sound/bright",
61
+ "mix/volume",
62
+ ],
63
+ "sound:wind": [
64
+ "sound/breath",
65
+ "sound/wind:advanced/vib",
66
+ "sound/bright",
67
+ "mix/volume",
68
+ ],
69
+ "sound:modal": [
70
+ "sound/position",
71
+ "sound/ring",
72
+ "sound/hardness",
73
+ "mix/volume",
74
+ ],
75
+ "sound:sing": ["sound/vowel", "sound/voices", "sound/bright", "mix/volume"],
76
+ "sound:granular": [
77
+ "sound/granular/position",
78
+ "sound/granular/grain (s)",
79
+ "sound/granular/spray (s)",
80
+ "mix/volume",
81
+ ],
82
+ // `sample:*` is the track's first sample voice.
83
+ "sound:sampler": [
84
+ "sound/sample:*/begin",
85
+ "sound/sample:*/len",
86
+ "sound/sample:*/speed",
87
+ "mix/volume",
88
+ ],
89
+ // A kit has no voice params: blue loosens the timing, green the room it
90
+ // plays in, white its tone.
91
+ "sound:kit": [
92
+ "sound/performance/humanize timing (ms)",
93
+ "effects/reverb/mix",
94
+ "effects/filter/cutoff",
95
+ "mix/volume",
96
+ ],
97
+ // The mixer page: this track's pan, reverb send, tone and level.
98
+ mix: ["mix/pan", "effects/reverb/mix", "effects/filter/cutoff", "mix/volume"],
99
+ master: [
100
+ "mix/master/master:eq/low",
101
+ "mix/master/master:eq/bell1",
102
+ "mix/master/master:eq/high",
103
+ "mix/master/target LUFS",
104
+ ],
105
+ tempo: [
106
+ "project/tempo",
107
+ "project/loop length",
108
+ "project/beats per bar",
109
+ undefined,
110
+ ],
111
+ // Effects: time or rate, feedback / size / depth, tone, mix.
112
+ "fx:filter": [
113
+ "effects/filter/type",
114
+ "effects/filter/resonance",
115
+ "effects/filter/cutoff",
116
+ undefined,
117
+ ],
118
+ "fx:delay": [
119
+ "effects/delay/beats",
120
+ "effects/delay/feedback",
121
+ undefined,
122
+ "effects/delay/mix",
123
+ ],
124
+ "fx:reverb": [
125
+ "effects/reverb/impulse (ir)",
126
+ "effects/reverb/size",
127
+ undefined,
128
+ "effects/reverb/mix",
129
+ ],
130
+ "fx:chorus": [
131
+ "effects/chorus/rate",
132
+ "effects/chorus/depth",
133
+ undefined,
134
+ "effects/chorus/mix",
135
+ ],
136
+ /**
137
+ * The Euclid editor (design §8.7): its row's own fields, not menu paths.
138
+ * Blue picks the drum row; ←→ on it moves between rows.
139
+ */
140
+ euclid: ["drum", "pulses", "rotate", "velocity"],
141
+ /**
142
+ * TAPE (design §7.3): its own values, not menu paths. Blue the playhead
143
+ * (a beat; ⇧ a bar), green the loop length, white the tempo of the
144
+ * segment under the playhead, orange the focused track's volume.
145
+ */
146
+ tape: ["playhead", "loop", "tempo", "volume"],
147
+ "fx:distort": [
148
+ undefined,
149
+ "effects/distort/drive",
150
+ "effects/distort/tone",
151
+ "effects/distort/mix",
152
+ ],
153
+ });
154
+
155
+ /** The sound page's family for a track: which engine its Sound menu shows. */
156
+ export function soundFamily(track: Track): string {
157
+ if (isDrumInstrument(track.instrument)) return "kit";
158
+ if (track.sampler) return "sampler";
159
+ if (isGranularInstrument(track.instrument)) return "granular";
160
+ if (isOrganFamily(track.instrument) && track.keys) return "organ";
161
+ if (isKeysFamily(track.instrument) && track.keys)
162
+ return isElectricFamily(track.instrument) ? "electric" : "piano";
163
+ if (isStringTrack(track)) return "string";
164
+ if (track.instrument === "modal") return "modal";
165
+ if (track.instrument === "wind" && track.wind) return "wind";
166
+ if (track.instrument === "sing" && track.sing) return "sing";
167
+ if (isWavetableInstrument(track.instrument)) return "wavetable";
168
+ return "synth";
169
+ }
170
+
171
+ /** The page id for `knobs <page>`, or undefined when no page has that name. */
172
+ export function knobPageId(
173
+ word: string | undefined,
174
+ track: Track | undefined,
175
+ ): string | undefined {
176
+ const name = (word ?? "sound").trim().toLowerCase().replace(/\s+/g, " ");
177
+ if (name === "sound" || name === "")
178
+ return `sound:${track ? soundFamily(track) : "synth"}`;
179
+ if (name === "mix" || name === "master" || name === "tempo") return name;
180
+ const fx = name.match(/^(?:fx|effect|effects)\s+(\S+)$/)?.[1];
181
+ if (fx) return `fx:${fx}`;
182
+ if (`sound:${name}` in KNOB_MAPS) return `sound:${name}`;
183
+ return undefined;
184
+ }
185
+
186
+ /** The page's row; an unmapped effect gets none (the menu derives one). */
187
+ export function knobMap(page: string): KnobMap | undefined {
188
+ return KNOB_MAPS[page];
189
+ }
190
+
191
+ /** The pages `knobs` names, for help and completion. */
192
+ export const KNOB_PAGE_WORDS = Object.freeze([
193
+ "sound",
194
+ "mix",
195
+ "master",
196
+ "tempo",
197
+ "fx <effect>",
198
+ ]);
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Voice › Clips and Lyrics, and Voice › voice presets >
3
+ * Vocal (0.7 clips lane). Every row runs a prompt command (`/clip`,
4
+ * `/lyrics`, `/vocal`), so the menu, the prompt and undo stay one path.
5
+ */
6
+ import { basename } from "node:path";
7
+ import {
8
+ CLIP_GAIN_MAX_DB,
9
+ CLIP_GAIN_MIN_DB,
10
+ clipGainDb,
11
+ lyricText,
12
+ } from "../../core/clips.ts";
13
+ import { SCORE_LIMITS, type AudioClip } from "../../core/score.ts";
14
+ import { userBarLabel, VOCAL_SETUPS } from "../commands/clips.ts";
15
+ import type { MenuContext, MenuNode } from "./menu.ts";
16
+
17
+ const round = (value: number, places = 2) => {
18
+ const scale = 10 ** places;
19
+ return Math.round(value * scale) / scale;
20
+ };
21
+ const dbText = (db: number) =>
22
+ Number.isFinite(db) ? `${db > 0 ? "+" : ""}${round(db, 1)} dB` : "-inf dB";
23
+
24
+ function clipDetail(context: MenuContext, clip: AudioClip): string {
25
+ return [
26
+ `bar ${userBarLabel(context.score, clip.startTick)}`,
27
+ clip.gain !== undefined ? dbText(clipGainDb(clip.gain)) : "",
28
+ clip.rev ? "rev" : "",
29
+ clip.mute ? "muted" : "",
30
+ ]
31
+ .filter(Boolean)
32
+ .join(" · ");
33
+ }
34
+
35
+ /** The rows for one clip: gain, fades, place, rev, mute, repeat, remove. */
36
+ function clipRows(context: MenuContext, id: string): MenuNode[] {
37
+ const track = context.score.tracks.find((t) => t.id === context.trackId);
38
+ const clip = track?.clips?.find((c) => c.id === id);
39
+ if (!clip) return [{ kind: "info", label: "clip", value: "removed" }];
40
+ const fadeIn = clip.fadeInTime ?? 0.005;
41
+ const fadeOut = clip.fadeTime ?? 0.005;
42
+ const fadeMax = SCORE_LIMITS.maxClipFadeSeconds;
43
+ const fadeStep = (value: number, direction: 1 | -1) =>
44
+ Math.max(
45
+ 0,
46
+ Math.min(
47
+ fadeMax,
48
+ round(
49
+ value < 0.05 ? value + 0.005 * direction : value + 0.05 * direction,
50
+ 3,
51
+ ),
52
+ ),
53
+ );
54
+ return [
55
+ {
56
+ kind: "number",
57
+ label: "gain",
58
+ help: "clip gain in dB before the track's effects",
59
+ value: round(clipGainDb(clip.gain ?? 1), 1),
60
+ min: CLIP_GAIN_MIN_DB,
61
+ max: CLIP_GAIN_MAX_DB,
62
+ step: (value, direction) =>
63
+ Math.max(
64
+ CLIP_GAIN_MIN_DB,
65
+ Math.min(CLIP_GAIN_MAX_DB, round(value + direction, 1)),
66
+ ),
67
+ format: dbText,
68
+ command: (value) => `/clip ${id} gain ${value}`,
69
+ reset: `/clip ${id} gain 0`,
70
+ },
71
+ {
72
+ kind: "number",
73
+ label: "fade in",
74
+ help: "equal-power fade-in, seconds",
75
+ value: fadeIn,
76
+ min: 0,
77
+ max: fadeMax,
78
+ step: fadeStep,
79
+ format: (value) => `${round(value, 3)} s`,
80
+ command: (value) => `/clip ${id} fade in ${value}`,
81
+ reset: `/clip ${id} fade in default`,
82
+ },
83
+ {
84
+ kind: "number",
85
+ label: "fade out",
86
+ help: "equal-power fade-out, seconds",
87
+ value: fadeOut,
88
+ min: 0,
89
+ max: fadeMax,
90
+ step: fadeStep,
91
+ format: (value) => `${round(value, 3)} s`,
92
+ command: (value) => `/clip ${id} fade out ${value}`,
93
+ reset: `/clip ${id} fade out default`,
94
+ },
95
+ {
96
+ kind: "number",
97
+ label: "start in file",
98
+ help: "seconds into the file where the clip starts playing",
99
+ value: clip.offset ?? 0,
100
+ min: 0,
101
+ max: SCORE_LIMITS.maxClipSeconds,
102
+ step: (value, direction) =>
103
+ Math.max(0, round(value + 0.1 * direction, 3)),
104
+ format: (value) => `${round(value, 3)} s`,
105
+ command: (value) => `/clip ${id} trim offset ${value}`,
106
+ reset: `/clip ${id} trim offset 0`,
107
+ },
108
+ {
109
+ kind: "entry",
110
+ label: "length",
111
+ value: clip.dur === undefined ? "to file end" : `${round(clip.dur, 3)} s`,
112
+ help: "seconds of the file to play; `end` plays to the file's end",
113
+ placeholder: "seconds, e.g. 4.5, or end",
114
+ command: (text) => {
115
+ const value = text.trim().toLowerCase().replace(/s$/, "");
116
+ if (value === "end") return `/clip ${id} trim dur end`;
117
+ return Number(value) > 0 ? `/clip ${id} trim dur ${value}` : undefined;
118
+ },
119
+ example: `/clip ${id} trim dur 4.5`,
120
+ },
121
+ {
122
+ kind: "toggle",
123
+ label: "reverse",
124
+ help: "play the clip backwards",
125
+ value: clip.rev === true,
126
+ command: () => `/clip ${id} rev`,
127
+ },
128
+ {
129
+ kind: "toggle",
130
+ label: "mute",
131
+ help: "silence this clip only",
132
+ value: clip.mute === true,
133
+ command: () => `/clip ${id} mute`,
134
+ },
135
+ {
136
+ kind: "entry",
137
+ label: "move to bar",
138
+ value: userBarLabel(context.score, clip.startTick),
139
+ help: "1-based bar, or bar.beat",
140
+ placeholder: "bar, e.g. 9 or 5.3",
141
+ command: (text) =>
142
+ text.trim() ? `/clip ${id} move ${text.trim()}` : undefined,
143
+ example: `/clip ${id} move 9`,
144
+ },
145
+ {
146
+ kind: "entry",
147
+ label: "split at bar",
148
+ value: "",
149
+ help: "cut the clip in two at a bar (5 ms fades at the cut)",
150
+ placeholder: "bar, e.g. 7",
151
+ command: (text) =>
152
+ text.trim() ? `/clip ${id} split ${text.trim()}` : undefined,
153
+ example: `/clip ${id} split 7`,
154
+ },
155
+ {
156
+ kind: "entry",
157
+ label: "repeat",
158
+ value: "",
159
+ help: "copies every N bars up to a bar: type `2 to 32`",
160
+ placeholder: "every N to bar",
161
+ command: (text) => {
162
+ const match = text
163
+ .trim()
164
+ .match(/^(?:every\s+)?(\d+(?:\.\d+)?)\s+(?:to\s+)?(\d+)$/i);
165
+ return match
166
+ ? `/clip ${id} repeat every ${match[1]} to ${match[2]}`
167
+ : undefined;
168
+ },
169
+ example: `/clip ${id} repeat every 2 to 32`,
170
+ },
171
+ {
172
+ kind: "info",
173
+ label: "file",
174
+ value: basename(clip.src),
175
+ help: clip.src,
176
+ },
177
+ {
178
+ kind: "action",
179
+ label: "remove clip",
180
+ command: `/clip ${id} rm`,
181
+ help: "remove this clip (the file stays in the project; undo brings it back)",
182
+ },
183
+ ];
184
+ }
185
+
186
+ /** Voice rows from the clips lane: Clips and Lyrics. */
187
+ export function clipMenuRows(context: MenuContext): MenuNode[] {
188
+ const track = context.score.tracks.find((t) => t.id === context.trackId);
189
+ if (!track) return [];
190
+ const clips = track.clips ?? [];
191
+ const notes = context.score.notes.filter((n) => n.trackId === track.id);
192
+ const sung = lyricText(notes);
193
+ return [
194
+ {
195
+ kind: "menu",
196
+ id: "voice:clips",
197
+ label: "clips",
198
+ detail: clips.length
199
+ ? `${clips.length} clip${clips.length === 1 ? "" : "s"}`
200
+ : "import a vocal",
201
+ help: "audio clips on this track: import a file or the vocals stem, then edit",
202
+ build: (inner) => {
203
+ const now =
204
+ inner.score.tracks.find((t) => t.id === inner.trackId)?.clips ?? [];
205
+ return [
206
+ ...now.map((clip): MenuNode => ({
207
+ kind: "menu",
208
+ id: `clip:${clip.id}`,
209
+ label: clip.id,
210
+ detail: clipDetail(inner, clip),
211
+ help: `${basename(clip.src)} · gain, fades, reverse, mute, move, split, repeat`,
212
+ build: (deeper) => clipRows(deeper, clip.id),
213
+ })),
214
+ {
215
+ kind: "entry",
216
+ label: "import a file",
217
+ value: "",
218
+ help: "copy an audio file into the track (48 kHz mono, peak -6 dBFS) at a bar",
219
+ placeholder: "<file> [bar]",
220
+ command: (text) =>
221
+ text.trim() ? `/vocal import ${text.trim()}` : undefined,
222
+ example: "/vocal import ~/take.wav 9",
223
+ },
224
+ {
225
+ kind: "entry",
226
+ label: "vocals stem",
227
+ value: "",
228
+ help: "place the vocals stem from the last split_stems at a bar",
229
+ placeholder: "bar (default 1)",
230
+ command: (text) => `/vocal stem ${text.trim() || "1"}`,
231
+ example: "/vocal stem 1",
232
+ },
233
+ {
234
+ kind: "choice",
235
+ label: "setup",
236
+ value: "",
237
+ options: VOCAL_SETUPS.filter(
238
+ (setup) => !setup.needs?.includes("say"),
239
+ ).map((setup) => setup.name),
240
+ help: "a vocal chain in one step (setups naming later tools say what is missing)",
241
+ command: (option) => `/vocal setups ${option}`,
242
+ },
243
+ ];
244
+ },
245
+ },
246
+ {
247
+ kind: "menu",
248
+ id: "voice:lyrics",
249
+ label: "lyrics",
250
+ detail: sung ? sung.slice(0, 32) : "none",
251
+ help: "words on this track's notes: - splits syllables, _ holds, ~ skips",
252
+ build: (inner) => {
253
+ const text = lyricText(
254
+ inner.score.notes.filter((n) => n.trackId === inner.trackId),
255
+ );
256
+ return [
257
+ {
258
+ kind: "entry",
259
+ label: "lyrics",
260
+ value: text,
261
+ help: "typed words are split into syllables onto the notes in order",
262
+ placeholder: "sun-lit morn-ing _ glow",
263
+ command: (typed) =>
264
+ typed.trim() ? `/lyrics ${typed.trim()}` : undefined,
265
+ example: "/lyrics sun-lit morn-ing",
266
+ },
267
+ {
268
+ kind: "entry",
269
+ label: "from bar",
270
+ value: "",
271
+ help: "lyrics starting at a bar: type `9 la la la`",
272
+ placeholder: "<bar> <words>",
273
+ command: (typed) =>
274
+ /^\d+(?:\.\d+)?\s+\S/.test(typed.trim())
275
+ ? `/lyrics ${typed.trim()}`
276
+ : undefined,
277
+ example: "/lyrics 9 la la la",
278
+ },
279
+ { kind: "action", label: "clear lyrics", command: "/lyrics clear" },
280
+ ];
281
+ },
282
+ },
283
+ ];
284
+ }
285
+
286
+ /** Voice › voice presets › vocal: the guide-note vocal track. */
287
+ export function vocalBrowseRows(_context: MenuContext): MenuNode[] {
288
+ return [
289
+ {
290
+ kind: "action",
291
+ label:
292
+ "vocal clips sound, notes are a silent guide · hpf 90, 3:1, plate",
293
+ command: "instrument vocal",
294
+ help: "a vocal track: import a take or stem in Voice › clips",
295
+ },
296
+ ];
297
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Project › Tempo & meter: the song's tempo map, meter changes and
3
- * fermatas, and the focused track's rate, phase and cycle. Every row runs a
2
+ * Project › Tempo and meter: the song's tempo map, meter changes and
3
+ * fermatas, and the focused track's rate, phase and loop. Every row runs a
4
4
  * prompt command from `src/commands/time.ts`, so the menu and the prompt
5
5
  * stay one grammar.
6
6
  */
@@ -28,7 +28,7 @@ export function openingMeterCommand(score: TimeScore, value: number): string {
28
28
  return unit === 4 ? `meter ${beats}` : `meter ${beats}/${unit}`;
29
29
  }
30
30
 
31
- /** One-line summary for the Project row and the Tempo & meter row. */
31
+ /** One-line summary for the Project row and the Tempo and meter row. */
32
32
  export function tempoDetail(score: TimeScore): string {
33
33
  const time = score.time;
34
34
  const parts: string[] = [];
@@ -55,19 +55,24 @@ export function tempoDetail(score: TimeScore): string {
55
55
  return parts.join(" · ");
56
56
  }
57
57
 
58
- /** The Tempo & meter submenu node for Project. */
58
+ /** The Tempo and meter submenu node for Project. */
59
59
  export function tempoMenuNode(context: MenuContext): MenuNode {
60
60
  return {
61
61
  kind: "menu",
62
62
  id: "tempo",
63
- label: "tempo & meter",
63
+ label: "tempo and meter",
64
64
  detail: tempoDetail(context.score),
65
65
  help: "tempo changes, rit. and accel., fermatas, meter changes, track rate and phase",
66
66
  build: tempoNodes,
67
67
  };
68
68
  }
69
69
 
70
- /** Rows of Project › Tempo & meter. */
70
+ /** What x puts the song's tempo, meter and loop length back to. */
71
+ export const START_TEMPO_BPM = 120;
72
+ export const START_BEATS_PER_BAR = 4;
73
+ export const START_BARS = 8;
74
+
75
+ /** Rows of Project › Tempo and meter. */
71
76
  export function tempoNodes(context: MenuContext): MenuNode[] {
72
77
  const score = context.score;
73
78
  const tpb = score.ticksPerBeat;
@@ -78,6 +83,7 @@ export function tempoNodes(context: MenuContext): MenuNode[] {
78
83
  label: "start tempo",
79
84
  help: "beats per minute at the top of the song",
80
85
  value: score.tempoBpm,
86
+ start: START_TEMPO_BPM,
81
87
  min: TIME_LIMITS.minBpm,
82
88
  max: TIME_LIMITS.maxBpm,
83
89
  step: stepBy(1, TIME_LIMITS.minBpm, TIME_LIMITS.maxBpm),
@@ -182,6 +188,7 @@ export function tempoNodes(context: MenuContext): MenuNode[] {
182
188
  label: "beats per bar",
183
189
  help: "the song's opening meter; meter changes below override it later",
184
190
  value: score.beatsPerBar,
191
+ start: START_BEATS_PER_BAR,
185
192
  min: 1,
186
193
  max: TIME_LIMITS.maxBeatsPerBar,
187
194
  step: stepBy(1, 1, TIME_LIMITS.maxBeatsPerBar),
@@ -223,7 +230,7 @@ export function tempoNodes(context: MenuContext): MenuNode[] {
223
230
  id: "track-time",
224
231
  label: `${track.name ?? track.id} time`,
225
232
  detail: trackTimeDetail(context),
226
- help: "the focused track's tempo ratio, phase and cycle (polytempo, polymeter, phasing)",
233
+ help: "the focused track's tempo ratio, phase and loop (polytempo, polymeter, phasing)",
227
234
  build: trackTimeNodes,
228
235
  });
229
236
  if (time?.tempo?.length)
@@ -278,7 +285,7 @@ function trackTimeDetail(context: MenuContext): string {
278
285
  const parts: string[] = [];
279
286
  if (time.rate !== undefined) parts.push(`rate ${fmt(time.rate, 4)}×`);
280
287
  if (time.phase !== undefined) parts.push(`phase ${fmt(time.phase / tpb)}`);
281
- if (time.cycle !== undefined) parts.push(`cycle ${fmt(time.cycle / tpb)}`);
288
+ if (time.cycle !== undefined) parts.push(`loop ${fmt(time.cycle / tpb)}`);
282
289
  return parts.join(" · ");
283
290
  }
284
291
 
@@ -320,8 +327,8 @@ export function trackTimeNodes(context: MenuContext): MenuNode[] {
320
327
  },
321
328
  {
322
329
  kind: "number",
323
- label: "cycle",
324
- help: "beats before the track's pattern repeats (polymeter) · x follows the song loop",
330
+ label: "loop",
331
+ help: "beats before the track's own loop repeats (polymeter) · x follows the song loop",
325
332
  value: time?.cycle !== undefined ? time.cycle / tpb : undefined,
326
333
  off: "song loop",
327
334
  start: score.beatsPerBar,
@@ -329,8 +336,8 @@ export function trackTimeNodes(context: MenuContext): MenuNode[] {
329
336
  max: TIME_LIMITS.maxTick / tpb,
330
337
  step: stepBy(0.25, 0.25, TIME_LIMITS.maxTick / tpb),
331
338
  format: (value) => `${fmt(value)} beat${value === 1 ? "" : "s"}`,
332
- command: (value) => `track cycle ${fmt(value, 4)}`,
333
- reset: "track cycle off",
339
+ command: (value) => `track loop ${fmt(value, 4)}`,
340
+ reset: "track loop off",
334
341
  },
335
342
  entry(
336
343
  "phasing",
@@ -350,7 +357,7 @@ export function trackTimeNodes(context: MenuContext): MenuNode[] {
350
357
  kind: "action",
351
358
  label: "follow the song",
352
359
  command: "track time off",
353
- help: "clear rate, phase and cycle",
360
+ help: "clear rate, phase and loop",
354
361
  },
355
362
  ];
356
363
  }