@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,217 @@
1
+ /**
2
+ * Agent tools for the style engine (core/styles): `list_styles` and
3
+ * `style_info` read the taxonomy and cards; `apply_style` generates a song
4
+ * from a style (optionally blended with a second one) and replaces the
5
+ * score with it in one undo step, the same path as the `style` command.
6
+ */
7
+
8
+ import { DiffError, diffScores } from "../../core/diff.ts";
9
+ import { STYLE_TREE, searchStyles } from "../../core/styles/index.ts";
10
+ import {
11
+ STYLE_LIMITS,
12
+ StyleError,
13
+ styleScore,
14
+ } from "../../core/styles/generate.ts";
15
+ import { STYLE_FAMILIES } from "../../core/styles/taxonomy.ts";
16
+ import { resolveStyle } from "../../core/styles/index.ts";
17
+ import {
18
+ describeStyle,
19
+ findStyle,
20
+ generateFromCommand,
21
+ generatedSummary,
22
+ } from "../commands/style.ts";
23
+ import type { AgentTool } from "./tools.ts";
24
+
25
+ class StyleToolError extends Error {
26
+ constructor(message: string) {
27
+ super(message);
28
+ this.name = "ToolArgumentError";
29
+ }
30
+ }
31
+
32
+ const MAX_LIST = 60;
33
+
34
+ function nodeJSON(id: string) {
35
+ const node = STYLE_TREE.get(id)!;
36
+ return {
37
+ id,
38
+ title: node.title,
39
+ ...(node.leaf ? {} : { children: node.children.length }),
40
+ };
41
+ }
42
+
43
+ function knownStyle(value: unknown, name: string): string {
44
+ if (typeof value !== "string" || value.length === 0 || value.length > 120)
45
+ throw new StyleToolError(`${name} must be a style id or name`);
46
+ const id = findStyle(value);
47
+ if (!id) {
48
+ const near = searchStyles(value, 5).map((match) => match.id);
49
+ throw new StyleToolError(
50
+ `no style "${value}"${near.length ? `; did you mean ${near.join(", ")}` : "; call list_styles with a query"}`,
51
+ );
52
+ }
53
+ return id;
54
+ }
55
+
56
+ function integer(value: unknown, name: string, min: number, max: number) {
57
+ if (value === undefined) return undefined;
58
+ if (
59
+ typeof value !== "number" ||
60
+ !Number.isInteger(value) ||
61
+ value < min ||
62
+ value > max
63
+ )
64
+ throw new StyleToolError(`${name} must be an integer ${min}..${max}`);
65
+ return value;
66
+ }
67
+
68
+ export const STYLE_TOOLS = Object.freeze([
69
+ {
70
+ name: "list_styles",
71
+ description:
72
+ "Browse the style taxonomy (read-only). query: ranked search over ids, names, aliases and regions. parent: the children of one style id. Neither: the families and their root styles. Returns ids to pass to style_info and apply_style.",
73
+ parameters: {
74
+ type: "object",
75
+ properties: {
76
+ query: { type: "string", maxLength: 120 },
77
+ parent: { type: "string", maxLength: 120 },
78
+ },
79
+ additionalProperties: false,
80
+ },
81
+ plan(args) {
82
+ for (const key of Object.keys(args))
83
+ if (key !== "query" && key !== "parent")
84
+ throw new StyleToolError(`unknown argument ${key}`);
85
+ let result: unknown;
86
+ let summary: string;
87
+ if (args.query !== undefined) {
88
+ if (typeof args.query !== "string" || !args.query.trim())
89
+ throw new StyleToolError("query must be a non-empty string");
90
+ const matches = searchStyles(args.query, MAX_LIST).map((match) =>
91
+ nodeJSON(match.id),
92
+ );
93
+ result = { query: args.query, matches };
94
+ summary = `styles · ${matches.length} for "${args.query.slice(0, 40)}"`;
95
+ } else if (args.parent !== undefined) {
96
+ const id = knownStyle(args.parent, "parent");
97
+ const node = STYLE_TREE.get(id)!;
98
+ result = {
99
+ parent: id,
100
+ children: node.children.slice(0, MAX_LIST * 2).map(nodeJSON),
101
+ };
102
+ summary = `styles · ${node.children.length} under ${id}`;
103
+ } else {
104
+ result = {
105
+ families: STYLE_FAMILIES.map((family) => ({
106
+ family: family.key,
107
+ title: family.title,
108
+ roots: family.roots.map(nodeJSON),
109
+ })),
110
+ };
111
+ summary = "styles · families";
112
+ }
113
+ const content = JSON.stringify(result);
114
+ return {
115
+ kind: "action",
116
+ summary,
117
+ async run() {
118
+ return { content, summary };
119
+ },
120
+ };
121
+ },
122
+ },
123
+ {
124
+ name: "style_info",
125
+ description:
126
+ "Describe one style (read-only): taxonomy path, meter, tempo range, groove and swing, tuning and scales, harmony model, texture roles, and children.",
127
+ parameters: {
128
+ type: "object",
129
+ properties: { id: { type: "string", maxLength: 120 } },
130
+ required: ["id"],
131
+ additionalProperties: false,
132
+ },
133
+ plan(args) {
134
+ const id = knownStyle(args.id, "id");
135
+ const node = STYLE_TREE.get(id)!;
136
+ const content = JSON.stringify({
137
+ id,
138
+ lines: describeStyle(resolveStyle(id)),
139
+ children: node.children,
140
+ });
141
+ const summary = `style ${id}`;
142
+ return {
143
+ kind: "action",
144
+ summary,
145
+ async run() {
146
+ return { content, summary };
147
+ },
148
+ };
149
+ },
150
+ },
151
+ {
152
+ name: "apply_style",
153
+ description: `Replace the whole song with one generated from a style: drums, bass, harmony, melody and form from the style's patterns (one undo step). bars ${STYLE_LIMITS.minBars}..${STYLE_LIMITS.maxBars} (default ${STYLE_LIMITS.defaultBars}); seed picks a variation (same seed, same song). blend + weight (0 is id, 1 is blend) mixes two styles.`,
154
+ parameters: {
155
+ type: "object",
156
+ properties: {
157
+ id: { type: "string", maxLength: 120 },
158
+ bars: {
159
+ type: "integer",
160
+ minimum: STYLE_LIMITS.minBars,
161
+ maximum: STYLE_LIMITS.maxBars,
162
+ },
163
+ seed: { type: "integer", minimum: 0, maximum: STYLE_LIMITS.maxSeed },
164
+ blend: { type: "string", maxLength: 120 },
165
+ weight: { type: "number", minimum: 0, maximum: 1 },
166
+ },
167
+ required: ["id"],
168
+ additionalProperties: false,
169
+ },
170
+ plan(args, context) {
171
+ for (const key of Object.keys(args))
172
+ if (!["id", "bars", "seed", "blend", "weight"].includes(key))
173
+ throw new StyleToolError(`unknown argument ${key}`);
174
+ const id = knownStyle(args.id, "id");
175
+ const bars = integer(
176
+ args.bars,
177
+ "bars",
178
+ STYLE_LIMITS.minBars,
179
+ STYLE_LIMITS.maxBars,
180
+ );
181
+ const seed = integer(args.seed, "seed", 0, STYLE_LIMITS.maxSeed);
182
+ if (args.weight !== undefined && args.blend === undefined)
183
+ throw new StyleToolError("weight needs blend");
184
+ let blend: { id: string; weight: number } | undefined;
185
+ if (args.blend !== undefined) {
186
+ const weight = args.weight ?? 0.5;
187
+ if (
188
+ typeof weight !== "number" ||
189
+ !Number.isFinite(weight) ||
190
+ weight < 0 ||
191
+ weight > 1
192
+ )
193
+ throw new StyleToolError("weight must be 0..1");
194
+ blend = { id: knownStyle(args.blend, "blend"), weight };
195
+ }
196
+ try {
197
+ const generated = generateFromCommand({
198
+ type: "style-apply",
199
+ id,
200
+ ...(bars !== undefined ? { bars } : {}),
201
+ ...(seed !== undefined ? { seed } : {}),
202
+ ...(blend ? { blend } : {}),
203
+ });
204
+ const next = styleScore(generated);
205
+ return {
206
+ kind: "score",
207
+ operations: diffScores(context.score, next),
208
+ summary: generatedSummary(generated).slice(0, 160),
209
+ };
210
+ } catch (error) {
211
+ if (error instanceof StyleError || error instanceof DiffError)
212
+ throw new StyleToolError(error.message);
213
+ throw error;
214
+ }
215
+ },
216
+ },
217
+ ] satisfies AgentTool[]);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The tool-argument error, in its own module so tool files that `tools.ts`
3
+ * spreads (voice-tools.ts) can throw it without an import cycle.
4
+ */
5
+
6
+ /** A model-supplied argument that the tool refused; never mutates the score. */
7
+ export class ToolArgumentError extends Error {
8
+ constructor(message: string) {
9
+ super(message);
10
+ this.name = "ToolArgumentError";
11
+ }
12
+ }
@@ -1,4 +1,8 @@
1
+ import { portableSchema } from "./portable-schema.ts";
2
+ import { midiToPitch } from "../../core/pitch.ts";
3
+ import { isGuideInstrument, vocalChainPatch } from "../../core/clips.ts";
1
4
  import { INSTRUMENT_WORDS } from "../../core/instruments.ts";
5
+ import type { TrackVocoder } from "../../core/vocoder.ts";
2
6
  import {
3
7
  DEFAULT_STRING_PRESET,
4
8
  STRING_INSTRUMENT,
@@ -83,6 +87,8 @@ import {
83
87
  applyFxCommand,
84
88
  effectPatch,
85
89
  effectValues,
90
+ FORMANT_VOWEL_HINT,
91
+ formantGotVowel,
86
92
  parseEffectName,
87
93
  parseParamName,
88
94
  parseFxCommand,
@@ -102,12 +108,16 @@ import { RHYTHM_TOOLS } from "./rhythm-tools.ts";
102
108
  import { CHORD_TOOLS } from "./chord-tools.ts";
103
109
  import { EXPRESSION_TOOLS } from "./expression-tools.ts";
104
110
  import { TUNING_TOOLS } from "./tuning-tools.ts";
111
+ import { CALIBRATION_TOOLS } from "./calibration-tools.ts";
105
112
  import { MASTER_TOOLS } from "./master-tools.ts";
113
+ import { STYLE_TOOLS } from "./style-tools.ts";
106
114
  import { DRUM_TOOLS } from "./drum-tools.ts";
107
115
  import { TIME_TOOLS } from "./time-tools.ts";
108
116
  import { SECTION_TOOLS } from "./section-tools.ts";
117
+ import { RANGE_TOOLS } from "./range-tools.ts";
109
118
  import { GRANULAR_TOOLS } from "./granular-tools.ts";
110
119
  import { RESAMPLE_TOOLS } from "./resample-tool.ts";
120
+ import { VOICE_TOOLS } from "./voice-tools.ts";
111
121
  import type { MediaResult, MediaRunContext } from "../media/types.ts";
112
122
  import { instrumentPatch, pitchToMidi } from "./ops.ts";
113
123
  import { TUNING_LIMITS } from "../../core/tuning.ts";
@@ -255,17 +265,17 @@ export type ToolContext = Readonly<{
255
265
  export type AgentTool = Readonly<{
256
266
  name: string;
257
267
  description: string;
268
+ /**
269
+ * Callable but not advertised: a duplicate kept so older transcripts and
270
+ * models still work. `chatTools` and the xcb catalog leave it out.
271
+ */
272
+ hidden?: boolean;
258
273
  parameters: Record<string, unknown>;
259
274
  plan: (args: Record<string, unknown>, context: ToolContext) => ToolPlan;
260
275
  }>;
261
276
 
262
- /** A model-supplied argument that the tool refused; never mutates the score. */
263
- export class ToolArgumentError extends Error {
264
- constructor(message: string) {
265
- super(message);
266
- this.name = "ToolArgumentError";
267
- }
268
- }
277
+ import { ToolArgumentError } from "./tool-error.ts";
278
+ export { ToolArgumentError };
269
279
 
270
280
  const MAX_NOTES_PER_CALL = 128;
271
281
  const MAX_EXPLAIN_CHARS = 2_000;
@@ -404,7 +414,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
404
414
  {
405
415
  name: "update_notes",
406
416
  description:
407
- "Move, resize, transpose, re-velocity or detune existing notes by id. Times are in beats.",
417
+ "Move, resize, transpose, re-velocity or detune existing notes by id. Times are in beats. To transpose, prefer transpose (semitones relative to the note's current pitch, e.g. 2 up a whole step, -12 down an octave) over an absolute pitch.",
408
418
  parameters: {
409
419
  type: "object",
410
420
  properties: {
@@ -424,6 +434,13 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
424
434
  { type: "string", maxLength: 4 },
425
435
  ],
426
436
  },
437
+ transpose: {
438
+ type: "integer",
439
+ minimum: -127,
440
+ maximum: 127,
441
+ description:
442
+ "Semitones relative to the current pitch (not with pitch)",
443
+ },
427
444
  velocity: { type: "number", minimum: 0, maximum: 1 },
428
445
  cents: {
429
446
  type: "number",
@@ -442,6 +459,9 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
442
459
  },
443
460
  plan(args, context) {
444
461
  const tpb = context.score.ticksPerBeat;
462
+ // Pitch changes as before→after, so the model sees what it did and
463
+ // does not transpose again from the refreshed brief.
464
+ const moves: string[] = [];
445
465
  const operations = list(args, "updates", 1, MAX_NOTES_PER_CALL).map(
446
466
  (value, index): ScoreOperation => {
447
467
  const update = record(value, `updates[${index}]`);
@@ -459,8 +479,41 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
459
479
  });
460
480
  if (duration !== undefined)
461
481
  patch.durationTicks = Math.max(1, Math.round(duration * tpb));
482
+ const before = context.score.notes.find((n) => n.id === noteId);
462
483
  if (update.pitch !== undefined)
463
484
  patch.pitch = pitch(update.pitch, `updates[${index}].pitch`);
485
+ // Small models fill every field: transpose 0 beside a pitch means
486
+ // no shift, and a pitch that equals the transposed note agrees.
487
+ if (
488
+ patch.pitch !== undefined &&
489
+ update.transpose !== undefined &&
490
+ update.transpose !== 0 &&
491
+ patch.pitch !== (before?.pitch ?? 0) + Number(update.transpose)
492
+ )
493
+ throw new ToolArgumentError(
494
+ `updates[${index}] sets both pitch and transpose; use one`,
495
+ );
496
+ if (update.transpose !== undefined && patch.pitch === undefined) {
497
+ const shift = update.transpose;
498
+ if (typeof shift !== "number" || !Number.isInteger(shift))
499
+ throw new ToolArgumentError(
500
+ `updates[${index}].transpose must be whole semitones`,
501
+ );
502
+ const moved = (before?.pitch ?? 0) + shift;
503
+ if (moved < 0 || moved > 127)
504
+ throw new ToolArgumentError(
505
+ `updates[${index}].transpose moves ${noteId} to MIDI ${moved}, outside 0..127`,
506
+ );
507
+ patch.pitch = moved;
508
+ }
509
+ if (
510
+ patch.pitch !== undefined &&
511
+ before &&
512
+ patch.pitch !== before.pitch
513
+ )
514
+ moves.push(
515
+ `${noteId} ${midiToPitch(before.pitch)}→${midiToPitch(patch.pitch)}`,
516
+ );
464
517
  const velocity = optionalNumber(update, "velocity", {
465
518
  min: 0,
466
519
  max: 1,
@@ -479,7 +532,11 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
479
532
  return {
480
533
  kind: "score",
481
534
  operations,
482
- summary: `~${operations.length} note${operations.length === 1 ? "" : "s"}`,
535
+ summary: `~${operations.length} note${operations.length === 1 ? "" : "s"}${
536
+ moves.length === 0
537
+ ? ""
538
+ : ` · pitch ${moves.slice(0, 8).join(", ")}${moves.length > 8 ? ` +${moves.length - 8} more` : ""}`
539
+ }`,
483
540
  };
484
541
  },
485
542
  },
@@ -520,7 +577,18 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
520
577
  return {
521
578
  kind: "score",
522
579
  operations: [
523
- { type: "updateTrack", trackId, patch: { ...patch, ...rig } },
580
+ {
581
+ type: "updateTrack",
582
+ trackId,
583
+ patch: {
584
+ // `vocal` (0.7): the vocal chain fills unset effects.
585
+ ...(isGuideInstrument(patch.instrument)
586
+ ? vocalChainPatch(track)
587
+ : {}),
588
+ ...patch,
589
+ ...rig,
590
+ },
591
+ },
524
592
  ],
525
593
  trackId,
526
594
  summary:
@@ -899,7 +967,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
899
967
  },
900
968
  {
901
969
  name: "set_keys",
902
- description: `Shape a modelled piano, electric keys (epiano wurli clav) or organ: preset (instrument, keys and its effects), params ${KEYS_SIMPLE.join(" ")}… (pianos: sym 0.5 adds sympathetic bloom under the sustain pedal, good for Chopin and Debussy; epiano: bark bell tone vibe; wurli: trem; clav: pickup mute; null unsets; DAWG.md lists all), or reset. Stored "piano" stays legacy; set_instrument piano writes grand. Organs (not "organ", a legacy synth): presets tonewheel combo pipe… (aliases hammond b3 farfisa church); drawbars "888800008", registers "08880", stops (plenum, flute8…), rotary; params perc percdecay percvol click scanner drive (tonewheel), voice vib vibmod drive (combo), chiff wind trem (pipe). A row the family does not read is refused.`,
970
+ description: `Shape a modeled piano, electric keys (epiano wurli clav) or organ: preset (instrument, keys and its effects), params ${KEYS_SIMPLE.join(" ")}… (pianos: sym 0.5 adds sympathetic bloom under the sustain pedal, good for Chopin and Debussy; epiano: bark bell tone vibe; wurli: trem; clav: pickup mute; null unsets; DAWG.md lists all), or reset. Stored "piano" stays legacy; set_instrument piano writes grand. Organs (not "organ", a legacy synth): presets tonewheel combo pipe… (aliases hammond b3 farfisa church); drawbars "888800008", registers "08880", stops (plenum, flute8…), rotary; params perc percdecay percvol click scanner drive (tonewheel), voice vib vibmod drive (combo), chiff wind trem (pipe). A row the family does not read is refused.`,
903
971
  parameters: {
904
972
  type: "object",
905
973
  properties: {
@@ -1278,7 +1346,8 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1278
1346
  },
1279
1347
  {
1280
1348
  name: "set_effects",
1281
- description: `Shorthand: low-pass filter, delay (beats) and reverb (mix 0.15..0.35 is a room); null removes one. Prefer set_fx.`,
1349
+ hidden: true,
1350
+ description: `Shorthand for set_fx (use set_fx): low-pass filter, delay (beats) and reverb (mix 0.15..0.35 is a room); null removes one.`,
1282
1351
  parameters: {
1283
1352
  type: "object",
1284
1353
  properties: {
@@ -1438,7 +1507,8 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1438
1507
  },
1439
1508
  {
1440
1509
  name: "add_drums",
1441
- description: `Add one-off drum hits to a kit track (create one with create_track instrument "kit"); prefer set_rhythm for repeating beats. Voices: ${DRUM_VOICES.map((info) => info.voice).join(", ")}. Give explicit hits, and/or patterns that repeat a voice every N beats across the loop.`,
1510
+ hidden: true,
1511
+ description: `Use set_rhythm. Add one-off drum hits to a kit track (create one with create_track instrument "kit"); prefer set_rhythm for repeating beats. Voices: ${DRUM_VOICES.map((info) => info.voice).join(", ")}. Give explicit hits, and/or patterns that repeat a voice every N beats across the loop.`,
1442
1512
  parameters: {
1443
1513
  type: "object",
1444
1514
  properties: {
@@ -1490,14 +1560,14 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1490
1560
  const track = context.score.tracks.find((t) => t.id === trackId)!;
1491
1561
  if (!isDrumInstrument(track.instrument))
1492
1562
  throw new ToolArgumentError(
1493
- `track ${trackId} is ${track.instrument}, not a drum kit; set_instrument kit or create a kit track`,
1563
+ `track ${trackId} is ${track.instrument}, not a kit; set_instrument kit or create a kit track`,
1494
1564
  );
1495
1565
  const tpb = context.score.ticksPerBeat;
1496
1566
  const loopBeats = loopTicksOf(context.score) / tpb;
1497
1567
  const voiceOf = (value: unknown, label: string) => {
1498
1568
  const voice =
1499
1569
  typeof value === "string" ? parseDrumVoice(value) : undefined;
1500
- if (!voice) throw new ToolArgumentError(`${label}: unknown drum voice`);
1570
+ if (!voice) throw new ToolArgumentError(`${label}: unknown drum`);
1501
1571
  return voice;
1502
1572
  };
1503
1573
  const hits: { voice: DrumVoice; beat: number; velocity: number }[] = [];
@@ -1962,14 +2032,19 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1962
2032
  ...CHORD_TOOLS,
1963
2033
  ...EXPRESSION_TOOLS,
1964
2034
  ...TUNING_TOOLS,
2035
+ ...CALIBRATION_TOOLS,
1965
2036
  ...DRUM_TOOLS,
1966
2037
  ...TIME_TOOLS,
1967
2038
  ...SECTION_TOOLS,
2039
+ ...RANGE_TOOLS,
1968
2040
  ...MEDIA_TOOLS,
1969
2041
  ...PACK_TOOLS,
1970
2042
  ...MASTER_TOOLS,
2043
+ ...STYLE_TOOLS,
1971
2044
  ...GRANULAR_TOOLS,
1972
2045
  ...RESAMPLE_TOOLS,
2046
+ // 0.7 Voice: one array per lane in voice-tools.ts.
2047
+ ...VOICE_TOOLS,
1973
2048
  // Looks tools up at call time, so it can plan any of the above.
1974
2049
  previewSoundTool((name) => findAgentTool(name)),
1975
2050
  ] satisfies AgentTool[]);
@@ -2066,14 +2141,16 @@ export function findAgentTool(
2066
2141
  export function chatTools(
2067
2142
  tools: readonly AgentTool[] = AGENT_TOOLS,
2068
2143
  ): ChatTool[] {
2069
- return tools.map((tool) => ({
2070
- type: "function",
2071
- function: {
2072
- name: tool.name,
2073
- description: tool.description,
2074
- parameters: tool.parameters,
2075
- },
2076
- }));
2144
+ return tools
2145
+ .filter((tool) => !tool.hidden)
2146
+ .map((tool) => ({
2147
+ type: "function",
2148
+ function: {
2149
+ name: tool.name,
2150
+ description: tool.description,
2151
+ parameters: portableSchema(tool.parameters),
2152
+ },
2153
+ }));
2077
2154
  }
2078
2155
 
2079
2156
  function fxToolCommand(
@@ -2099,6 +2176,14 @@ function fxToolCommand(
2099
2176
  throw new ToolArgumentError("set_fx needs on, preset, or params");
2100
2177
  }
2101
2178
  const params = record(args.params, "params");
2179
+ if (
2180
+ effect === "formant" &&
2181
+ formantGotVowel([
2182
+ ...Object.keys(params),
2183
+ ...Object.values(params),
2184
+ ] as string[])
2185
+ )
2186
+ throw new ToolArgumentError(FORMANT_VOWEL_HINT);
2102
2187
  const values: Record<string, number | string | boolean> = {};
2103
2188
  for (const [name, value] of Object.entries(params)) {
2104
2189
  const param = parseParamName(effect, name);
@@ -2296,6 +2381,7 @@ function instrumentName(value: unknown): {
2296
2381
  string?: { preset: string };
2297
2382
  modal?: TrackModal;
2298
2383
  wind?: TrackWind;
2384
+ vocoder?: TrackVocoder;
2299
2385
  } {
2300
2386
  const word = typeof value === "string" ? value.trim() : undefined;
2301
2387
  if (word === STRING_INSTRUMENT)
@@ -2309,6 +2395,7 @@ function instrumentName(value: unknown): {
2309
2395
  (!patch.string &&
2310
2396
  !patch.modal &&
2311
2397
  !patch.wind &&
2398
+ !patch.vocoder &&
2312
2399
  !(AVAILABLE_INSTRUMENTS as readonly string[]).includes(patch.instrument))
2313
2400
  )
2314
2401
  throw new ToolArgumentError(
@@ -232,7 +232,7 @@ export class SpendMeter {
232
232
 
233
233
  /**
234
234
  * The spend line: `$0.12 session · $0.48 today · opus-5.5 · gateway`;
235
- * `subscription · sonnet · claude`; `no model · dawg login`. Narrow widths
235
+ * `subscription · sonnet · claude`; `commands only`. Narrow widths
236
236
  * drop the today total, then the session total, then the provider.
237
237
  */
238
238
  export function spendLine(
@@ -245,7 +245,7 @@ export function spendLine(
245
245
  },
246
246
  width = 120,
247
247
  ): string {
248
- if (options.kind === "offline") return "no model · dawg login";
248
+ if (options.kind === "offline") return "commands only";
249
249
  const tail = [options.model, options.provider].filter(Boolean) as string[];
250
250
  const money =
251
251
  options.kind === "subscription"