@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,453 @@
1
+ /**
2
+ * Clip and lyric agent tools (0.7 clips lane): `place_clip`, `edit_clip`
3
+ * and `set_lyrics`. Positions are in beats, gain in dB; the score stores
4
+ * ticks and linear gain. Files must already be inside the project (an
5
+ * `import_sample` or a `split_stems` stem); nothing is fetched.
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import { readFile, stat } from "node:fs/promises";
9
+ import { basename, extname, relative, resolve } from "node:path";
10
+ import {
11
+ assignLyrics,
12
+ CLIP_GAIN_MAX_DB,
13
+ CLIP_GAIN_MIN_DB,
14
+ clipGainDb,
15
+ dbToClipGain,
16
+ importGain,
17
+ nextClipId,
18
+ repeatClip,
19
+ splitClip,
20
+ } from "../../core/clips.ts";
21
+ import {
22
+ SCORE_LIMITS,
23
+ type AudioClip,
24
+ type ScoreOperation,
25
+ type Track,
26
+ } from "../../core/score.ts";
27
+ import { trackSlug } from "../../core/slug.ts";
28
+ import { wavInfo } from "../commands/clips.ts";
29
+ import { ToolArgumentError } from "./tool-error.ts";
30
+ import type { ToolContext } from "./tools.ts";
31
+ import type { VoiceTool } from "./voice-tools.ts";
32
+
33
+ function trackOf(args: Record<string, unknown>, context: ToolContext): Track {
34
+ const id = args.trackId ?? context.focusedTrackId;
35
+ const track = context.score.tracks.find((t) => t.id === id);
36
+ if (!track)
37
+ throw new ToolArgumentError(
38
+ `unknown track ${String(id)}; create it with create_track first`,
39
+ );
40
+ return track;
41
+ }
42
+
43
+ function beats(value: unknown, label: string): number | undefined {
44
+ if (value === undefined) return undefined;
45
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
46
+ throw new ToolArgumentError(`${label} must be beats >= 0`);
47
+ return value;
48
+ }
49
+
50
+ function seconds(value: unknown, label: string, max: number) {
51
+ if (value === undefined) return undefined;
52
+ if (
53
+ typeof value !== "number" ||
54
+ !Number.isFinite(value) ||
55
+ value < 0 ||
56
+ value > max
57
+ )
58
+ throw new ToolArgumentError(`${label} must be seconds 0..${max}`);
59
+ return value;
60
+ }
61
+
62
+ const tickOf = (context: ToolContext, beat: number) =>
63
+ Math.round(beat * context.score.ticksPerBeat);
64
+
65
+ const setClipsOp = (
66
+ trackId: string,
67
+ clips: readonly AudioClip[],
68
+ ): ScoreOperation =>
69
+ ({
70
+ type: "setClips",
71
+ trackId,
72
+ clips: clips.length > 0 ? clips : null,
73
+ }) as ScoreOperation;
74
+
75
+ const placeClip: VoiceTool = {
76
+ name: "place_clip",
77
+ description:
78
+ "Place an audio file already in the project (an import_sample result, a split_stems stem such as tracks/<slug>/downloads/<song>.stems/vocals.wav) as a clip on a track at a beat. Pins the file's sha256 and sets clip gain so the file peaks at -6 dBFS. Clips sum before the track's effects; on a `vocal` track the notes are silent guides. Optional offset and dur (seconds into the file), gain (dB -60..12, overrides the -6 dBFS level; the project stores it linear), fadeIn/fadeOut (seconds, equal-power; stored as fadeInTime/fadeTime), rev.",
79
+ parameters: {
80
+ type: "object",
81
+ properties: {
82
+ trackId: { type: "string", maxLength: SCORE_LIMITS.maxIdLength },
83
+ src: {
84
+ type: "string",
85
+ maxLength: SCORE_LIMITS.maxSamplePathLength,
86
+ description: "Project-relative wav path",
87
+ },
88
+ at: { type: "number", minimum: 0, description: "Start in beats" },
89
+ id: { type: "string", maxLength: SCORE_LIMITS.maxIdLength },
90
+ offset: { type: "number", minimum: 0 },
91
+ dur: { type: "number", exclusiveMinimum: 0 },
92
+ gain: {
93
+ type: "number",
94
+ minimum: CLIP_GAIN_MIN_DB,
95
+ maximum: CLIP_GAIN_MAX_DB,
96
+ },
97
+ fadeIn: {
98
+ type: "number",
99
+ minimum: 0,
100
+ maximum: SCORE_LIMITS.maxClipFadeSeconds,
101
+ },
102
+ fadeOut: {
103
+ type: "number",
104
+ minimum: 0,
105
+ maximum: SCORE_LIMITS.maxClipFadeSeconds,
106
+ },
107
+ rev: { type: "boolean" },
108
+ },
109
+ required: ["src"],
110
+ additionalProperties: false,
111
+ },
112
+ previewable: true,
113
+ plan(args, context) {
114
+ const track = trackOf(args, context);
115
+ if (typeof args.src !== "string" || args.src.length === 0)
116
+ throw new ToolArgumentError("src must be a project-relative wav path");
117
+ const src = args.src.replace(/^\.\//, "");
118
+ const at = beats(args.at, "at") ?? 0;
119
+ const max = SCORE_LIMITS.maxClipSeconds;
120
+ const offset = seconds(args.offset, "offset", max);
121
+ const dur = seconds(args.dur, "dur", max);
122
+ const fadeIn = seconds(
123
+ args.fadeIn,
124
+ "fadeIn",
125
+ SCORE_LIMITS.maxClipFadeSeconds,
126
+ );
127
+ const fadeOut = seconds(
128
+ args.fadeOut,
129
+ "fadeOut",
130
+ SCORE_LIMITS.maxClipFadeSeconds,
131
+ );
132
+ if ((track.clips?.length ?? 0) >= SCORE_LIMITS.maxClipsPerTrack)
133
+ throw new ToolArgumentError(
134
+ `${track.name} already has ${SCORE_LIMITS.maxClipsPerTrack} clips`,
135
+ );
136
+ const wanted =
137
+ typeof args.id === "string" && args.id
138
+ ? args.id
139
+ : trackSlug(basename(src, extname(src))) || "clip";
140
+ const id = nextClipId(track, wanted);
141
+ return {
142
+ kind: "prepare",
143
+ summary: `place ${id} at beat ${at} on ${track.name}`,
144
+ async run(host) {
145
+ const root = host.workspace?.root ?? process.cwd();
146
+ const path = resolve(root, src);
147
+ if (relative(root, path).startsWith(".."))
148
+ throw new ToolArgumentError("src must stay inside the project");
149
+ const info = await stat(path).catch(() => undefined);
150
+ if (!info?.isFile())
151
+ throw new ToolArgumentError(
152
+ `${src} does not exist in the project; import_sample or split_stems first`,
153
+ );
154
+ if (info.size > SCORE_LIMITS.maxSampleFileBytes)
155
+ throw new ToolArgumentError(`${src} is over the sample size cap`);
156
+ const sha256 = createHash("sha256")
157
+ .update(await readFile(path))
158
+ .digest("hex");
159
+ const wav = await wavInfo(path);
160
+ if (!wav) throw new ToolArgumentError(`${src} is not a readable wav`);
161
+ const gain =
162
+ typeof args.gain === "number"
163
+ ? dbToClipGain(args.gain)
164
+ : importGain(wav.peak);
165
+ const clip: AudioClip = {
166
+ id,
167
+ src,
168
+ sha256,
169
+ startTick: tickOf(context, at),
170
+ ...(offset !== undefined && offset > 0 ? { offset } : {}),
171
+ ...(dur !== undefined ? { dur } : {}),
172
+ ...(gain !== undefined && Math.abs(gain - 1) > 1e-6
173
+ ? { gain: Math.round(gain * 1e4) / 1e4 }
174
+ : {}),
175
+ ...(fadeIn !== undefined ? { fadeInTime: fadeIn } : {}),
176
+ ...(fadeOut !== undefined ? { fadeTime: fadeOut } : {}),
177
+ ...(args.rev === true ? { rev: true } : {}),
178
+ };
179
+ return {
180
+ kind: "score",
181
+ operations: [setClipsOp(track.id, [...(track.clips ?? []), clip])],
182
+ summary: `placed ${id} (${wav.seconds.toFixed(1)} s${
183
+ clip.gain !== undefined ? `, ${clipGainDb(clip.gain)} dB` : ""
184
+ }) at beat ${at} on ${track.name}`,
185
+ trackId: track.id,
186
+ };
187
+ },
188
+ };
189
+ },
190
+ };
191
+
192
+ const editClip: VoiceTool = {
193
+ name: "edit_clip",
194
+ description:
195
+ "Edit a clip on a track by id: at (beats), gain (dB -60..12; 0 removes; stored linear), fadeIn/fadeOut (seconds, equal-power; stored as fadeInTime/fadeTime), offset/dur (seconds into the file; dur null plays to the end), rev, mute, split (a beat: cuts the clip in two there with 5 ms fades, the tail gets a new id), repeat {every, until} in beats (writes copies <id>-r2, -r3...), remove.",
196
+ parameters: {
197
+ type: "object",
198
+ properties: {
199
+ trackId: { type: "string", maxLength: SCORE_LIMITS.maxIdLength },
200
+ id: { type: "string", maxLength: SCORE_LIMITS.maxIdLength },
201
+ at: { type: "number", minimum: 0 },
202
+ gain: {
203
+ type: "number",
204
+ minimum: CLIP_GAIN_MIN_DB,
205
+ maximum: CLIP_GAIN_MAX_DB,
206
+ },
207
+ fadeIn: {
208
+ type: "number",
209
+ minimum: 0,
210
+ maximum: SCORE_LIMITS.maxClipFadeSeconds,
211
+ },
212
+ fadeOut: {
213
+ type: "number",
214
+ minimum: 0,
215
+ maximum: SCORE_LIMITS.maxClipFadeSeconds,
216
+ },
217
+ offset: { type: "number", minimum: 0 },
218
+ dur: { type: ["number", "null"], exclusiveMinimum: 0 },
219
+ rev: { type: "boolean" },
220
+ mute: { type: "boolean" },
221
+ repeat: {
222
+ type: "object",
223
+ properties: {
224
+ every: { type: "number", exclusiveMinimum: 0 },
225
+ until: { type: "number", minimum: 0 },
226
+ },
227
+ required: ["every", "until"],
228
+ additionalProperties: false,
229
+ },
230
+ split: { type: "number", minimum: 0 },
231
+ remove: { type: "boolean" },
232
+ },
233
+ required: ["id"],
234
+ additionalProperties: false,
235
+ },
236
+ previewable: true,
237
+ plan(args, context) {
238
+ const track = trackOf(args, context);
239
+ const clips = track.clips ?? [];
240
+ const clip = clips.find((c) => c.id === args.id);
241
+ if (!clip)
242
+ throw new ToolArgumentError(
243
+ `no clip ${String(args.id)} on ${track.name}${clips.length ? ` (${clips.map((c) => c.id).join(", ")})` : ""}`,
244
+ );
245
+ if (args.remove === true)
246
+ return {
247
+ kind: "score",
248
+ operations: [
249
+ setClipsOp(
250
+ track.id,
251
+ clips.filter((c) => c !== clip),
252
+ ),
253
+ ],
254
+ summary: `removed clip ${clip.id}`,
255
+ trackId: track.id,
256
+ };
257
+ const split = beats(args.split, "split");
258
+ if (split !== undefined) {
259
+ const others = Object.keys(args).filter(
260
+ (key) => !["trackId", "id", "split"].includes(key),
261
+ );
262
+ if (others.length > 0)
263
+ throw new ToolArgumentError(
264
+ `split on its own; edit ${others.join(", ")} in a second call`,
265
+ );
266
+ return {
267
+ kind: "prepare",
268
+ summary: `split ${clip.id} at beat ${split}`,
269
+ async run(host) {
270
+ const root = host.workspace?.root ?? process.cwd();
271
+ const fileSeconds = (await wavInfo(resolve(root, clip.src)))?.seconds;
272
+ const cut = splitClip(
273
+ context.score,
274
+ track,
275
+ clip,
276
+ tickOf(context, split),
277
+ fileSeconds,
278
+ );
279
+ if (typeof cut === "string")
280
+ throw new ToolArgumentError(
281
+ cut.replace("there", `at beat ${split}`),
282
+ );
283
+ return {
284
+ kind: "score",
285
+ operations: [
286
+ setClipsOp(
287
+ track.id,
288
+ clips.flatMap((c) => (c === clip ? [cut.head, cut.tail] : [c])),
289
+ ),
290
+ ],
291
+ summary: `split ${clip.id} at beat ${split} into ${clip.id} and ${cut.tail.id}`,
292
+ trackId: track.id,
293
+ };
294
+ },
295
+ };
296
+ }
297
+ const max = SCORE_LIMITS.maxClipSeconds;
298
+ const next: Record<string, unknown> = { ...clip };
299
+ const changes: string[] = [];
300
+ const at = beats(args.at, "at");
301
+ if (at !== undefined) {
302
+ next.startTick = tickOf(context, at);
303
+ changes.push(`at beat ${at}`);
304
+ }
305
+ if (args.gain !== undefined) {
306
+ if (typeof args.gain !== "number" || !Number.isFinite(args.gain))
307
+ throw new ToolArgumentError("gain must be dB");
308
+ const gain = dbToClipGain(args.gain);
309
+ if (Math.abs(gain - 1) < 1e-6) delete next.gain;
310
+ else next.gain = Math.round(gain * 1e4) / 1e4;
311
+ changes.push(`gain ${args.gain} dB`);
312
+ }
313
+ const fadeIn = seconds(
314
+ args.fadeIn,
315
+ "fadeIn",
316
+ SCORE_LIMITS.maxClipFadeSeconds,
317
+ );
318
+ const fadeOut = seconds(
319
+ args.fadeOut,
320
+ "fadeOut",
321
+ SCORE_LIMITS.maxClipFadeSeconds,
322
+ );
323
+ if (fadeIn !== undefined) next.fadeInTime = fadeIn;
324
+ if (fadeOut !== undefined) next.fadeTime = fadeOut;
325
+ if (fadeIn !== undefined || fadeOut !== undefined) changes.push("fades");
326
+ const offset = seconds(args.offset, "offset", max);
327
+ if (offset !== undefined) {
328
+ if (offset > 0) next.offset = offset;
329
+ else delete next.offset;
330
+ changes.push(`offset ${offset} s`);
331
+ }
332
+ if (args.dur === null) {
333
+ delete next.dur;
334
+ changes.push("plays to the end");
335
+ } else {
336
+ const dur = seconds(args.dur, "dur", max);
337
+ if (dur !== undefined) {
338
+ next.dur = dur;
339
+ changes.push(`dur ${dur} s`);
340
+ }
341
+ }
342
+ for (const key of ["rev", "mute"] as const)
343
+ if (args[key] !== undefined) {
344
+ if (args[key] === true) next[key] = true;
345
+ else delete next[key];
346
+ changes.push(`${key} ${args[key] === true ? "on" : "off"}`);
347
+ }
348
+ const edited = next as AudioClip;
349
+ let out = clips.map((c) => (c === clip ? edited : c));
350
+ if (args.repeat !== undefined) {
351
+ const repeat = args.repeat as { every?: unknown; until?: unknown };
352
+ const every = beats(repeat.every, "repeat.every");
353
+ const until = beats(repeat.until, "repeat.until");
354
+ if (!every || until === undefined)
355
+ throw new ToolArgumentError(
356
+ "repeat needs every > 0 and until in beats",
357
+ );
358
+ const copies = repeatClip(
359
+ edited,
360
+ tickOf(context, every),
361
+ tickOf(context, until),
362
+ new Set(out.map((c) => c.id)),
363
+ );
364
+ if (out.length + copies.length > SCORE_LIMITS.maxClipsPerTrack)
365
+ throw new ToolArgumentError(
366
+ `more than ${SCORE_LIMITS.maxClipsPerTrack} clips`,
367
+ );
368
+ out = [...out, ...copies];
369
+ changes.push(`${copies.length} repeats`);
370
+ }
371
+ if (changes.length === 0) throw new ToolArgumentError("nothing to change");
372
+ return {
373
+ kind: "score",
374
+ operations: [setClipsOp(track.id, out)],
375
+ summary: `clip ${clip.id}: ${changes.join(", ")}`,
376
+ trackId: track.id,
377
+ };
378
+ },
379
+ };
380
+
381
+ const setLyrics: VoiceTool = {
382
+ name: "set_lyrics",
383
+ description:
384
+ "Put lyrics on a track's notes in time order (chords sing on their top note): spaces separate syllables, hyphens split words (nev-er), _ holds the previous syllable over a note (melisma), ~ skips a note. Words typed whole are split automatically when there are more notes than syllables. from (beats) starts later; clear removes lyrics from there on. Lyrics are text on notes; they guide sing and future TTS and show on the highway.",
385
+ parameters: {
386
+ type: "object",
387
+ properties: {
388
+ trackId: { type: "string", maxLength: SCORE_LIMITS.maxIdLength },
389
+ text: { type: "string", maxLength: SCORE_LIMITS.maxClipTextLength },
390
+ from: { type: "number", minimum: 0 },
391
+ clear: { type: "boolean" },
392
+ },
393
+ additionalProperties: false,
394
+ },
395
+ plan(args, context) {
396
+ const track = trackOf(args, context);
397
+ const from = tickOf(context, beats(args.from, "from") ?? 0);
398
+ const notes = context.score.notes
399
+ .filter((n) => n.trackId === track.id && n.startTick >= from)
400
+ .sort((a, b) => a.startTick - b.startTick || b.pitch - a.pitch);
401
+ const tops = notes.filter(
402
+ (note, index) =>
403
+ index === 0 || note.startTick !== notes[index - 1]!.startTick,
404
+ );
405
+ const operations: ScoreOperation[] = [];
406
+ if (args.clear === true) {
407
+ for (const note of notes)
408
+ if (note.lyric !== undefined)
409
+ operations.push({
410
+ type: "updateNote",
411
+ noteId: note.id,
412
+ patch: { lyric: null },
413
+ } as ScoreOperation);
414
+ return {
415
+ kind: "score",
416
+ operations,
417
+ summary: `cleared lyrics on ${track.name}`,
418
+ trackId: track.id,
419
+ };
420
+ }
421
+ if (typeof args.text !== "string" || !args.text.trim())
422
+ throw new ToolArgumentError("text is required (or clear: true)");
423
+ if (tops.length === 0)
424
+ throw new ToolArgumentError(
425
+ `${track.name} has no notes to sing on; add notes first`,
426
+ );
427
+ const { lyrics, dropped, split } = assignLyrics(args.text, tops);
428
+ for (const note of tops) {
429
+ const lyric = lyrics.get(note.id);
430
+ if (lyric !== note.lyric)
431
+ operations.push({
432
+ type: "updateNote",
433
+ noteId: note.id,
434
+ patch: { lyric: lyric ?? null },
435
+ } as ScoreOperation);
436
+ }
437
+ return {
438
+ kind: "score",
439
+ operations,
440
+ summary: `lyrics on ${track.name}: ${tops.map((n) => lyrics.get(n.id) ?? "~").join(" ")}${
441
+ split.length ? ` (split ${split.join(", ")})` : ""
442
+ }${dropped.length ? `; ${dropped.length} syllables left over: ${dropped.join(" ")}` : ""}`,
443
+ trackId: track.id,
444
+ };
445
+ },
446
+ };
447
+
448
+ /** The clips lane's tools, spread into CLIPS_TOOLS. */
449
+ export const CLIP_TOOL_LIST: readonly VoiceTool[] = [
450
+ placeClip,
451
+ editClip,
452
+ setLyrics,
453
+ ];