@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,263 @@
1
+ /**
2
+ * The TAPE view model: a score, the playhead and this pane's state become
3
+ * the cells tui/tape.ts paints. Density per cell is the notes starting in
4
+ * it, weighted by velocity (` ▁▂▃▄▅▆▇█`); a held note shows `▁` across the
5
+ * cells it sustains through, and an audio clip `▃` where it plays.
6
+ *
7
+ * With a form the tape is drawn unrolled (op1-ux §4): every pass in play
8
+ * order, the first pass of each section solid and every later one ghosted
9
+ * `░`. Each cell keeps the score bar it shows, so a click, a drag or a
10
+ * gesture on a ghost acts on the source section.
11
+ */
12
+ import { formPositionAt, formSegments } from "../../core/sections.ts";
13
+ import type { TrackScore } from "../../core/score.ts";
14
+ import { barStartTick, bpmAtTick } from "../../core/tempo.ts";
15
+ import type { KnobIndex } from "../../tui/knobs.ts";
16
+ import type {
17
+ TapeRow,
18
+ TapeSection,
19
+ TapeView,
20
+ TapeZoom,
21
+ } from "../../tui/tape.ts";
22
+ import {
23
+ barOfBeat,
24
+ clipboardLabel,
25
+ formPasses,
26
+ loopRange,
27
+ rangeLine,
28
+ tapeKnobs,
29
+ TAPE_HINT,
30
+ type TapeContext,
31
+ } from "./tape-mode.ts";
32
+
33
+ export type TapeViewInput = TapeContext &
34
+ Readonly<{
35
+ zoom: TapeZoom;
36
+ selected: KnobIndex;
37
+ /** Other panes on a track (§12.7): `B`, `C●`. */
38
+ marks: (trackId: string) => string;
39
+ /**
40
+ * Transport beat (arranged time), for the playhead on an unrolled form:
41
+ * which pass it is in. Absent, the playhead sits on the first pass.
42
+ */
43
+ transportBeat?: number | undefined;
44
+ }>;
45
+
46
+ /** One drawn cell on an unrolled form: a score cell, its pass, a ghost. */
47
+ type Unrolled = Readonly<{ cell: number; segment: number; ghost: boolean }>;
48
+
49
+ /** The form in play order as score cells; undefined without a form. */
50
+ function unroll(
51
+ score: TrackScore,
52
+ bars: readonly number[],
53
+ ): Unrolled[] | undefined {
54
+ const segments = formSegments(score);
55
+ if (segments.length === 0) return undefined;
56
+ const firstCellOf = new Map<number, number>();
57
+ bars.forEach((bar, index) => {
58
+ if (!firstCellOf.has(bar)) firstCellOf.set(bar, index);
59
+ });
60
+ const seen = new Set<string>();
61
+ const cells: Unrolled[] = [];
62
+ segments.forEach((segment, index) => {
63
+ const ghost = seen.has(segment.section.name);
64
+ seen.add(segment.section.name);
65
+ const from = firstCellOf.get(segment.section.startBar);
66
+ if (from === undefined) return;
67
+ const end = segment.section.startBar + segment.section.bars;
68
+ for (let cell = from; cell < bars.length && bars[cell]! < end; cell += 1)
69
+ cells.push({ cell, segment: index, ghost });
70
+ });
71
+ // Bars the form never plays stay reachable, after it, in score order.
72
+ const played = new Set(cells.map((item) => item.cell));
73
+ bars.forEach((_, cell) => {
74
+ if (!played.has(cell)) cells.push({ cell, segment: -1, ghost: false });
75
+ });
76
+ return cells;
77
+ }
78
+
79
+ /** Cell start ticks and the bar each cell is in, for a zoom. */
80
+ export function tapeCells(
81
+ score: TrackScore,
82
+ zoom: TapeZoom,
83
+ ): { starts: number[]; bars: number[] } {
84
+ const starts: number[] = [];
85
+ const bars: number[] = [];
86
+ for (let bar = 0; bar < score.bars; bar += 1) {
87
+ const from = barStartTick(score, bar);
88
+ const to = barStartTick(score, bar + 1);
89
+ const step =
90
+ zoom === "bar"
91
+ ? to - from
92
+ : zoom === "beat"
93
+ ? score.ticksPerBeat
94
+ : score.ticksPerBeat / 2;
95
+ for (let tick = from; tick < to - 1e-6; tick += step) {
96
+ starts.push(tick);
97
+ bars.push(bar);
98
+ }
99
+ }
100
+ return { starts, bars };
101
+ }
102
+
103
+ function cellAt(starts: readonly number[], tick: number): number {
104
+ let low = 0;
105
+ let high = starts.length - 1;
106
+ if (high < 0 || tick < starts[0]!) return -1;
107
+ while (low < high) {
108
+ const mid = (low + high + 1) >> 1;
109
+ if (starts[mid]! <= tick) low = mid;
110
+ else high = mid - 1;
111
+ }
112
+ return low;
113
+ }
114
+
115
+ /** Density levels (0…8) per cell for one track. */
116
+ export function trackLevels(
117
+ score: TrackScore,
118
+ trackId: string,
119
+ starts: readonly number[],
120
+ end: number,
121
+ ): number[] {
122
+ const weight = starts.map(() => 0);
123
+ const held = starts.map(() => false);
124
+ for (const note of score.notes) {
125
+ if (note.trackId !== trackId || note.startTick >= end) continue;
126
+ const cell = cellAt(starts, note.startTick);
127
+ if (cell < 0) continue;
128
+ weight[cell]! += Math.max(0.15, note.velocity);
129
+ const last = cellAt(
130
+ starts,
131
+ Math.min(end - 1, note.startTick + note.durationTicks - 1),
132
+ );
133
+ for (let index = cell + 1; index <= last; index += 1) held[index] = true;
134
+ }
135
+ const track = score.tracks.find((item) => item.id === trackId);
136
+ for (const clip of track?.clips ?? []) {
137
+ const first = cellAt(starts, clip.startTick);
138
+ if (first < 0) continue;
139
+ const length =
140
+ clip.dur !== undefined
141
+ ? clip.dur *
142
+ (bpmAtTick(score, clip.startTick) / 60) *
143
+ score.ticksPerBeat
144
+ : starts.length > first + 1
145
+ ? starts[first + 1]! - starts[first]!
146
+ : 0;
147
+ const last = cellAt(starts, Math.min(end - 1, clip.startTick + length - 1));
148
+ for (let index = first; index <= last; index += 1)
149
+ weight[index] = Math.max(weight[index]!, 1);
150
+ }
151
+ return weight.map((value, index) =>
152
+ value > 0
153
+ ? Math.max(1, Math.min(8, Math.ceil(value * 3)))
154
+ : held[index]
155
+ ? 1
156
+ : 0,
157
+ );
158
+ }
159
+
160
+ export function tapeView(input: TapeViewInput): TapeView {
161
+ const score = input.score;
162
+ const { starts, bars } = tapeCells(score, input.zoom);
163
+ const end = barStartTick(score, score.bars);
164
+ const tick = Math.max(0, Math.round(input.beat * score.ticksPerBeat));
165
+ const scoreHead = Math.max(0, cellAt(starts, Math.min(end - 1, tick)));
166
+ const unrolled = unroll(score, bars);
167
+ // The pass under the transport, so the playhead walks through ghosts.
168
+ const pass =
169
+ unrolled && input.transportBeat !== undefined
170
+ ? formPositionAt(score, input.transportBeat).index
171
+ : undefined;
172
+ const inPass = unrolled?.findIndex(
173
+ (item) =>
174
+ item.cell === scoreHead && (pass === undefined || item.segment === pass),
175
+ );
176
+ const playheadCell = unrolled
177
+ ? Math.max(
178
+ 0,
179
+ inPass !== undefined && inPass >= 0
180
+ ? inPass
181
+ : unrolled.findIndex((item) => item.cell === scoreHead),
182
+ )
183
+ : scoreHead;
184
+ // A section the form plays more than once shows `×N` after its name.
185
+ const passes = formPasses(score);
186
+ const named = (name: string): string => {
187
+ const times = passes.get(name) ?? 1;
188
+ return times > 1 ? `${name} ×${times}` : name;
189
+ };
190
+ const sections: TapeSection[] = score.sections.map((section) => ({
191
+ name: named(section.name),
192
+ startBar: section.startBar,
193
+ bars: section.bars,
194
+ }));
195
+ const loopSection =
196
+ score.loop === undefined && score.loopSection !== undefined
197
+ ? sections.find((section) => section.name.startsWith(score.loopSection!))
198
+ ?.name
199
+ : undefined;
200
+ const tempo = [...(score.time?.tempo ?? [])]
201
+ .filter((event) => event.tick > 0 && event.tick < end)
202
+ .sort((a, b) => a.tick - b.tick);
203
+ const pick = <T>(values: readonly T[]): T[] =>
204
+ unrolled ? unrolled.map((item) => values[item.cell]!) : [...values];
205
+ const rows: TapeRow[] = score.tracks.map((track) => ({
206
+ id: track.id,
207
+ name: track.name,
208
+ muted: track.muted,
209
+ levels: pick(trackLevels(score, track.id, starts, end)),
210
+ marks: input.marks(track.id).trim(),
211
+ }));
212
+ // Tempo marks land on the cell their tick starts (the first pass).
213
+ const tempoCell = (at: number): number => {
214
+ const cell = Math.max(0, cellAt(starts, at));
215
+ return unrolled
216
+ ? Math.max(
217
+ 0,
218
+ unrolled.findIndex((item) => item.cell === cell),
219
+ )
220
+ : cell;
221
+ };
222
+ const focused = Math.max(
223
+ 0,
224
+ score.tracks.findIndex((track) => track.id === input.trackId),
225
+ );
226
+ return {
227
+ bars: score.bars,
228
+ cellBars: pick(bars),
229
+ ...(unrolled
230
+ ? {
231
+ ghosts: unrolled.map((item) => item.ghost),
232
+ passes: unrolled.map((item) => item.segment),
233
+ passNames: formSegments(score).map((segment) =>
234
+ named(segment.section.name),
235
+ ),
236
+ }
237
+ : {}),
238
+ playheadCell,
239
+ loop: loopRange(score),
240
+ sections,
241
+ loopSection,
242
+ tempo:
243
+ tempo.length > 0
244
+ ? [
245
+ { cell: 0, bpm: Math.round(score.tempoBpm) },
246
+ ...tempo.map((event) => ({
247
+ cell: tempoCell(event.tick),
248
+ bpm: Math.round(event.bpm),
249
+ })),
250
+ ]
251
+ : [],
252
+ rows,
253
+ focused,
254
+ range: rangeLine(input),
255
+ clipboard: clipboardLabel(score, input.clipboard),
256
+ knobs: tapeKnobs(input),
257
+ selected: input.selected,
258
+ hint: TAPE_HINT,
259
+ };
260
+ }
261
+
262
+ /** The playhead's bar, for the header (`5.3`) callers. */
263
+ export { barOfBeat };
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Ctrl-k menu rows for the wind engine (0.6.1): the "Winds and brass" group
3
- * under Sound › browse sounds, and the WIND_PARAMS rows under Sound ›
3
+ * under Sound › instruments, and the WIND_PARAMS rows under Sound ›
4
4
  * Parameters on a wind-engine track. Every row runs a `wind …` command.
5
5
  */
6
6
  import type { EnumParam, NumberParam } from "../../core/params.ts";
@@ -27,12 +27,12 @@ function presetRow(name: WindPresetName): MenuNode {
27
27
  };
28
28
  }
29
29
 
30
- /** Sound › browse sounds › Winds and brass: flutes, reeds, brass. */
30
+ /** Sound › instruments › winds and brass: flutes, reeds, brass. */
31
31
  export function windsMenu(): MenuNode {
32
32
  return {
33
33
  kind: "menu",
34
34
  id: "winds",
35
- label: "Winds and brass",
35
+ label: "winds and brass",
36
36
  detail: `${WIND_PRESET_NAMES.length} blown · flute, sax, trumpet …`,
37
37
  help: "breath-driven waveguides: flutes, reeds and brass; wind players 4 for a section",
38
38
  build: () =>
package/src/version.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The package version, read from package.json by a static JSON import so
3
+ * `bun build --compile` embeds it: a runtime read relative to
4
+ * import.meta.url finds nothing inside a compiled binary.
5
+ */
6
+ import pkg from "../package.json" with { type: "json" };
7
+
8
+ export const VERSION: string = pkg.version;