@hraness/dawg 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/DAWG.md +343 -116
  3. package/README.md +27 -25
  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 +182 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/instruments.ts +19 -0
  11. package/core/keys.ts +3 -3
  12. package/core/loop.ts +5 -0
  13. package/core/lyrics.ts +297 -0
  14. package/core/master.ts +3 -3
  15. package/core/resonators.ts +16 -2
  16. package/core/routing.ts +165 -0
  17. package/core/score.ts +832 -13
  18. package/core/sdk/eval-child.ts +7 -2
  19. package/core/sdk/eval.ts +35 -6
  20. package/core/sdk/print.ts +276 -3
  21. package/core/sdk/sync-lyrics.ts +49 -0
  22. package/core/sdk/v1.ts +1849 -42
  23. package/core/sections.ts +292 -26
  24. package/core/sing.ts +815 -0
  25. package/core/style-provenance.ts +80 -0
  26. package/core/styles/africa-mena-southasia.ts +2893 -0
  27. package/core/styles/americas.ts +3810 -0
  28. package/core/styles/art.ts +4993 -0
  29. package/core/styles/base.ts +123 -0
  30. package/core/styles/cycles.ts +106 -0
  31. package/core/styles/electronic.ts +2723 -0
  32. package/core/styles/europe-asia-pacific.ts +2838 -0
  33. package/core/styles/excerpt.ts +29 -0
  34. package/core/styles/gamelan.ts +283 -0
  35. package/core/styles/generate.ts +2199 -0
  36. package/core/styles/index.ts +515 -0
  37. package/core/styles/parts.ts +106 -0
  38. package/core/styles/pop.ts +3189 -0
  39. package/core/styles/rock.ts +2993 -0
  40. package/core/styles/roots.ts +4175 -0
  41. package/core/styles/schema.ts +429 -0
  42. package/core/styles/taxonomy.ts +940 -0
  43. package/core/styles/validate.ts +528 -0
  44. package/core/tempo.ts +32 -2
  45. package/core/tuning.ts +19 -3
  46. package/core/vocoder.ts +524 -0
  47. package/guides/agent.md +29 -0
  48. package/guides/arrange.md +30 -0
  49. package/guides/audition.md +20 -14
  50. package/guides/automation.md +12 -7
  51. package/guides/chords.md +15 -15
  52. package/guides/effects.md +17 -16
  53. package/guides/faders.md +20 -16
  54. package/guides/files.md +13 -9
  55. package/guides/getting-started.md +15 -11
  56. package/guides/keys.md +19 -15
  57. package/guides/media.md +17 -12
  58. package/guides/mix.md +12 -6
  59. package/guides/music.md +23 -8
  60. package/guides/notes.md +15 -9
  61. package/guides/performance.md +15 -13
  62. package/guides/play.md +21 -13
  63. package/guides/project.md +25 -8
  64. package/guides/providers.md +20 -14
  65. package/guides/resample.md +15 -9
  66. package/guides/rhythm.md +17 -13
  67. package/guides/sessions.md +17 -7
  68. package/guides/show-me.md +31 -0
  69. package/guides/sound.md +25 -9
  70. package/guides/sounds.md +15 -13
  71. package/guides/styles.md +31 -0
  72. package/guides/tempo.md +15 -10
  73. package/guides/tracks.md +15 -10
  74. package/guides/tuning.md +32 -0
  75. package/guides/voice.md +31 -0
  76. package/guides/web-search.md +18 -8
  77. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  78. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  79. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  80. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  81. package/native/prebuilt/manifest.json +21 -0
  82. package/package.json +5 -2
  83. package/src/agent/agent.ts +126 -14
  84. package/src/agent/calibration-tools.ts +53 -0
  85. package/src/agent/clip-tools.ts +453 -0
  86. package/src/agent/command-agent.ts +369 -0
  87. package/src/agent/drum-tools.ts +2 -2
  88. package/src/agent/expression-tools.ts +1 -1
  89. package/src/agent/gateway.ts +246 -60
  90. package/src/agent/models.ts +53 -12
  91. package/src/agent/ops.ts +12 -1
  92. package/src/agent/pack-tools.ts +1 -1
  93. package/src/agent/planner.ts +13 -0
  94. package/src/agent/portable-schema.ts +80 -0
  95. package/src/agent/preview-tool.ts +4 -1
  96. package/src/agent/provider.ts +22 -8
  97. package/src/agent/rhythm-tools.ts +1 -1
  98. package/src/agent/section-tools.ts +1 -1
  99. package/src/agent/show-me.ts +497 -0
  100. package/src/agent/steer.ts +15 -0
  101. package/src/agent/style-tools.ts +217 -0
  102. package/src/agent/tool-error.ts +12 -0
  103. package/src/agent/tools.ts +108 -23
  104. package/src/agent/usage.ts +2 -2
  105. package/src/agent/voice-tools.ts +925 -0
  106. package/src/agent/xcb-agent.ts +11 -7
  107. package/src/argv.ts +38 -0
  108. package/src/audio/analysis.ts +253 -0
  109. package/src/audio/arrange.ts +37 -3
  110. package/src/audio/autotune-engine.ts +101 -0
  111. package/src/audio/autotune.ts +640 -0
  112. package/src/audio/clips.ts +240 -0
  113. package/src/audio/doctor.ts +86 -0
  114. package/src/audio/dsp/bandbank.ts +138 -0
  115. package/src/audio/dsp/envelope.ts +10 -0
  116. package/src/audio/dsp/follow.ts +120 -0
  117. package/src/audio/dsp/formant.ts +427 -0
  118. package/src/audio/dsp/glottal.ts +243 -0
  119. package/src/audio/dsp/interp.ts +7 -2
  120. package/src/audio/dsp/lpc.ts +50 -0
  121. package/src/audio/dsp/periodicity.ts +59 -0
  122. package/src/audio/dsp/pitch.ts +995 -0
  123. package/src/audio/dsp/psola.ts +199 -0
  124. package/src/audio/effects/chain.ts +3 -1
  125. package/src/audio/effects/common.ts +43 -0
  126. package/src/audio/effects/convolution.ts +7 -4
  127. package/src/audio/effects/filter.ts +48 -69
  128. package/src/audio/effects/formant.ts +263 -0
  129. package/src/audio/engine.ts +160 -28
  130. package/src/audio/fit.ts +35 -3
  131. package/src/audio/instrument-check.ts +59 -43
  132. package/src/audio/instruments.ts +4 -0
  133. package/src/audio/keys/calibration.ts +56 -0
  134. package/src/audio/keys/electric.ts +8 -1
  135. package/src/audio/keys/engine.ts +13 -1
  136. package/src/audio/keys/piano.ts +22 -2
  137. package/src/audio/kits.ts +135 -6
  138. package/src/audio/live.ts +114 -20
  139. package/src/audio/native.ts +615 -0
  140. package/src/audio/preview.ts +30 -2
  141. package/src/audio/render-worker.ts +2 -0
  142. package/src/audio/renderer.ts +2 -0
  143. package/src/audio/resample.ts +2 -1
  144. package/src/audio/sampler.ts +85 -4
  145. package/src/audio/samples.ts +20 -2
  146. package/src/audio/sing/analysis.ts +193 -0
  147. package/src/audio/sing/engine.ts +949 -0
  148. package/src/audio/strings/bow.ts +48 -5
  149. package/src/audio/strings/engine.ts +8 -2
  150. package/src/audio/synth/oscillators.ts +31 -21
  151. package/src/audio/synth/voice.ts +34 -1
  152. package/src/audio/vocoder/bank.ts +314 -0
  153. package/src/audio/vocoder/carrier.ts +165 -0
  154. package/src/audio/vocoder/control.ts +68 -0
  155. package/src/audio/vocoder/detect.ts +50 -0
  156. package/src/audio/vocoder/index.ts +304 -0
  157. package/src/audio/vocoder/talkbox.ts +143 -0
  158. package/src/audio/wav.ts +500 -77
  159. package/src/audio/winds/engine.ts +5 -1
  160. package/src/audio/winds/trim.ts +28 -4
  161. package/src/audio/winds/trims1.ts +297 -0
  162. package/src/audio/winds/voice.ts +15 -2
  163. package/src/auth/cli.ts +38 -36
  164. package/src/auth/credentials.ts +30 -1
  165. package/src/auth/login.ts +15 -9
  166. package/src/auth/tui.ts +19 -10
  167. package/src/commands/arrange.ts +44 -29
  168. package/src/commands/autotune.ts +421 -0
  169. package/src/commands/calibration.ts +74 -0
  170. package/src/commands/clips.ts +887 -0
  171. package/src/commands/drums.ts +3 -2
  172. package/src/commands/edit.ts +11 -4
  173. package/src/commands/expression.ts +1 -1
  174. package/src/commands/formant.ts +221 -0
  175. package/src/commands/fx.ts +101 -32
  176. package/src/commands/grammar.ts +558 -0
  177. package/src/commands/help.ts +610 -380
  178. package/src/commands/history.ts +18 -0
  179. package/src/commands/keys.ts +8 -8
  180. package/src/commands/modal.ts +1 -1
  181. package/src/commands/music.ts +1 -1
  182. package/src/commands/nearest.ts +53 -0
  183. package/src/commands/pack.ts +9 -2
  184. package/src/commands/param-range.ts +56 -0
  185. package/src/commands/parses.ts +130 -0
  186. package/src/commands/progression.ts +170 -0
  187. package/src/commands/rhythm.ts +3 -0
  188. package/src/commands/rig.ts +3 -24
  189. package/src/commands/sing.ts +478 -0
  190. package/src/commands/strum.ts +13 -1
  191. package/src/commands/style.ts +415 -0
  192. package/src/commands/time.ts +6 -3
  193. package/src/commands/tuning.ts +3 -3
  194. package/src/commands/vocal-pitch.ts +616 -0
  195. package/src/commands/vocal.ts +147 -0
  196. package/src/commands/vocoder.ts +627 -0
  197. package/src/commands/wind.ts +2 -2
  198. package/src/fs/durable.ts +50 -0
  199. package/src/lang/glossary.ts +493 -0
  200. package/src/launch-args.ts +163 -0
  201. package/src/main.ts +1397 -253
  202. package/src/media/cli.ts +20 -3
  203. package/src/media/import.ts +3 -1
  204. package/src/project/check.ts +21 -2
  205. package/src/project/clip-pins.ts +72 -0
  206. package/src/project/init.ts +23 -8
  207. package/src/project/sync.ts +418 -86
  208. package/src/render.ts +20 -1
  209. package/src/session/daemon.ts +3 -0
  210. package/src/session/meta.ts +14 -0
  211. package/src/session/origin.ts +154 -0
  212. package/src/session/port.ts +22 -4
  213. package/src/session/presence.ts +34 -4
  214. package/src/session/protocol.ts +5 -1
  215. package/src/session/rebase.ts +18 -5
  216. package/src/session/receipt.ts +258 -0
  217. package/src/session/store.ts +65 -43
  218. package/src/tui/arrange-menu.ts +65 -31
  219. package/src/tui/audition.ts +1 -1
  220. package/src/tui/euclid.ts +18 -13
  221. package/src/tui/fader.ts +228 -41
  222. package/src/tui/granular-menu.ts +2 -4
  223. package/src/tui/menu-clips.ts +297 -0
  224. package/src/tui/menu-time.ts +20 -13
  225. package/src/tui/menu-voice.ts +405 -0
  226. package/src/tui/menu.ts +759 -177
  227. package/src/tui/modal-menu.ts +6 -6
  228. package/src/tui/performance-menu.ts +5 -2
  229. package/src/tui/play-chords.ts +4 -2
  230. package/src/tui/play-mode.ts +15 -1
  231. package/src/tui/play-session.ts +47 -7
  232. package/src/tui/sing-menu.ts +278 -0
  233. package/src/tui/style-menu.ts +104 -0
  234. package/src/tui/vocoder-menu.ts +244 -0
  235. package/src/tui/wind-menu.ts +3 -3
  236. package/src/version.ts +8 -0
  237. package/src/web/fetch.ts +115 -29
  238. package/tui/activity.ts +180 -9
  239. package/tui/app.ts +274 -40
  240. package/tui/clip-row.ts +132 -0
  241. package/tui/delight.ts +144 -0
  242. package/tui/drawer.ts +70 -22
  243. package/tui/frame-gate.ts +76 -0
  244. package/tui/grammar.ts +112 -63
  245. package/tui/guide.ts +42 -4
  246. package/tui/highway.ts +269 -25
  247. package/tui/hints.ts +192 -0
  248. package/tui/input.ts +60 -9
  249. package/tui/keys.ts +1 -1
  250. package/tui/play-strip.ts +68 -14
  251. package/tui/prompt.ts +1 -1
  252. package/tui/screen.ts +144 -14
  253. package/tui/theme.ts +27 -0
@@ -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,15 @@ 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";
109
117
  import { GRANULAR_TOOLS } from "./granular-tools.ts";
110
118
  import { RESAMPLE_TOOLS } from "./resample-tool.ts";
119
+ import { VOICE_TOOLS } from "./voice-tools.ts";
111
120
  import type { MediaResult, MediaRunContext } from "../media/types.ts";
112
121
  import { instrumentPatch, pitchToMidi } from "./ops.ts";
113
122
  import { TUNING_LIMITS } from "../../core/tuning.ts";
@@ -255,17 +264,17 @@ export type ToolContext = Readonly<{
255
264
  export type AgentTool = Readonly<{
256
265
  name: string;
257
266
  description: string;
267
+ /**
268
+ * Callable but not advertised: a duplicate kept so older transcripts and
269
+ * models still work. `chatTools` and the xcb catalog leave it out.
270
+ */
271
+ hidden?: boolean;
258
272
  parameters: Record<string, unknown>;
259
273
  plan: (args: Record<string, unknown>, context: ToolContext) => ToolPlan;
260
274
  }>;
261
275
 
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
- }
276
+ import { ToolArgumentError } from "./tool-error.ts";
277
+ export { ToolArgumentError };
269
278
 
270
279
  const MAX_NOTES_PER_CALL = 128;
271
280
  const MAX_EXPLAIN_CHARS = 2_000;
@@ -404,7 +413,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
404
413
  {
405
414
  name: "update_notes",
406
415
  description:
407
- "Move, resize, transpose, re-velocity or detune existing notes by id. Times are in beats.",
416
+ "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
417
  parameters: {
409
418
  type: "object",
410
419
  properties: {
@@ -424,6 +433,13 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
424
433
  { type: "string", maxLength: 4 },
425
434
  ],
426
435
  },
436
+ transpose: {
437
+ type: "integer",
438
+ minimum: -127,
439
+ maximum: 127,
440
+ description:
441
+ "Semitones relative to the current pitch (not with pitch)",
442
+ },
427
443
  velocity: { type: "number", minimum: 0, maximum: 1 },
428
444
  cents: {
429
445
  type: "number",
@@ -442,6 +458,9 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
442
458
  },
443
459
  plan(args, context) {
444
460
  const tpb = context.score.ticksPerBeat;
461
+ // Pitch changes as before→after, so the model sees what it did and
462
+ // does not transpose again from the refreshed brief.
463
+ const moves: string[] = [];
445
464
  const operations = list(args, "updates", 1, MAX_NOTES_PER_CALL).map(
446
465
  (value, index): ScoreOperation => {
447
466
  const update = record(value, `updates[${index}]`);
@@ -459,8 +478,41 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
459
478
  });
460
479
  if (duration !== undefined)
461
480
  patch.durationTicks = Math.max(1, Math.round(duration * tpb));
481
+ const before = context.score.notes.find((n) => n.id === noteId);
462
482
  if (update.pitch !== undefined)
463
483
  patch.pitch = pitch(update.pitch, `updates[${index}].pitch`);
484
+ // Small models fill every field: transpose 0 beside a pitch means
485
+ // no shift, and a pitch that equals the transposed note agrees.
486
+ if (
487
+ patch.pitch !== undefined &&
488
+ update.transpose !== undefined &&
489
+ update.transpose !== 0 &&
490
+ patch.pitch !== (before?.pitch ?? 0) + Number(update.transpose)
491
+ )
492
+ throw new ToolArgumentError(
493
+ `updates[${index}] sets both pitch and transpose; use one`,
494
+ );
495
+ if (update.transpose !== undefined && patch.pitch === undefined) {
496
+ const shift = update.transpose;
497
+ if (typeof shift !== "number" || !Number.isInteger(shift))
498
+ throw new ToolArgumentError(
499
+ `updates[${index}].transpose must be whole semitones`,
500
+ );
501
+ const moved = (before?.pitch ?? 0) + shift;
502
+ if (moved < 0 || moved > 127)
503
+ throw new ToolArgumentError(
504
+ `updates[${index}].transpose moves ${noteId} to MIDI ${moved}, outside 0..127`,
505
+ );
506
+ patch.pitch = moved;
507
+ }
508
+ if (
509
+ patch.pitch !== undefined &&
510
+ before &&
511
+ patch.pitch !== before.pitch
512
+ )
513
+ moves.push(
514
+ `${noteId} ${midiToPitch(before.pitch)}→${midiToPitch(patch.pitch)}`,
515
+ );
464
516
  const velocity = optionalNumber(update, "velocity", {
465
517
  min: 0,
466
518
  max: 1,
@@ -479,7 +531,11 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
479
531
  return {
480
532
  kind: "score",
481
533
  operations,
482
- summary: `~${operations.length} note${operations.length === 1 ? "" : "s"}`,
534
+ summary: `~${operations.length} note${operations.length === 1 ? "" : "s"}${
535
+ moves.length === 0
536
+ ? ""
537
+ : ` · pitch ${moves.slice(0, 8).join(", ")}${moves.length > 8 ? ` +${moves.length - 8} more` : ""}`
538
+ }`,
483
539
  };
484
540
  },
485
541
  },
@@ -520,7 +576,18 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
520
576
  return {
521
577
  kind: "score",
522
578
  operations: [
523
- { type: "updateTrack", trackId, patch: { ...patch, ...rig } },
579
+ {
580
+ type: "updateTrack",
581
+ trackId,
582
+ patch: {
583
+ // `vocal` (0.7): the vocal chain fills unset effects.
584
+ ...(isGuideInstrument(patch.instrument)
585
+ ? vocalChainPatch(track)
586
+ : {}),
587
+ ...patch,
588
+ ...rig,
589
+ },
590
+ },
524
591
  ],
525
592
  trackId,
526
593
  summary:
@@ -899,7 +966,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
899
966
  },
900
967
  {
901
968
  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.`,
969
+ 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
970
  parameters: {
904
971
  type: "object",
905
972
  properties: {
@@ -1278,7 +1345,8 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1278
1345
  },
1279
1346
  {
1280
1347
  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.`,
1348
+ hidden: true,
1349
+ 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
1350
  parameters: {
1283
1351
  type: "object",
1284
1352
  properties: {
@@ -1438,7 +1506,8 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1438
1506
  },
1439
1507
  {
1440
1508
  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.`,
1509
+ hidden: true,
1510
+ 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
1511
  parameters: {
1443
1512
  type: "object",
1444
1513
  properties: {
@@ -1490,14 +1559,14 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1490
1559
  const track = context.score.tracks.find((t) => t.id === trackId)!;
1491
1560
  if (!isDrumInstrument(track.instrument))
1492
1561
  throw new ToolArgumentError(
1493
- `track ${trackId} is ${track.instrument}, not a drum kit; set_instrument kit or create a kit track`,
1562
+ `track ${trackId} is ${track.instrument}, not a kit; set_instrument kit or create a kit track`,
1494
1563
  );
1495
1564
  const tpb = context.score.ticksPerBeat;
1496
1565
  const loopBeats = loopTicksOf(context.score) / tpb;
1497
1566
  const voiceOf = (value: unknown, label: string) => {
1498
1567
  const voice =
1499
1568
  typeof value === "string" ? parseDrumVoice(value) : undefined;
1500
- if (!voice) throw new ToolArgumentError(`${label}: unknown drum voice`);
1569
+ if (!voice) throw new ToolArgumentError(`${label}: unknown drum`);
1501
1570
  return voice;
1502
1571
  };
1503
1572
  const hits: { voice: DrumVoice; beat: number; velocity: number }[] = [];
@@ -1962,14 +2031,18 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1962
2031
  ...CHORD_TOOLS,
1963
2032
  ...EXPRESSION_TOOLS,
1964
2033
  ...TUNING_TOOLS,
2034
+ ...CALIBRATION_TOOLS,
1965
2035
  ...DRUM_TOOLS,
1966
2036
  ...TIME_TOOLS,
1967
2037
  ...SECTION_TOOLS,
1968
2038
  ...MEDIA_TOOLS,
1969
2039
  ...PACK_TOOLS,
1970
2040
  ...MASTER_TOOLS,
2041
+ ...STYLE_TOOLS,
1971
2042
  ...GRANULAR_TOOLS,
1972
2043
  ...RESAMPLE_TOOLS,
2044
+ // 0.7 Voice: one array per lane in voice-tools.ts.
2045
+ ...VOICE_TOOLS,
1973
2046
  // Looks tools up at call time, so it can plan any of the above.
1974
2047
  previewSoundTool((name) => findAgentTool(name)),
1975
2048
  ] satisfies AgentTool[]);
@@ -2066,14 +2139,16 @@ export function findAgentTool(
2066
2139
  export function chatTools(
2067
2140
  tools: readonly AgentTool[] = AGENT_TOOLS,
2068
2141
  ): 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
- }));
2142
+ return tools
2143
+ .filter((tool) => !tool.hidden)
2144
+ .map((tool) => ({
2145
+ type: "function",
2146
+ function: {
2147
+ name: tool.name,
2148
+ description: tool.description,
2149
+ parameters: portableSchema(tool.parameters),
2150
+ },
2151
+ }));
2077
2152
  }
2078
2153
 
2079
2154
  function fxToolCommand(
@@ -2099,6 +2174,14 @@ function fxToolCommand(
2099
2174
  throw new ToolArgumentError("set_fx needs on, preset, or params");
2100
2175
  }
2101
2176
  const params = record(args.params, "params");
2177
+ if (
2178
+ effect === "formant" &&
2179
+ formantGotVowel([
2180
+ ...Object.keys(params),
2181
+ ...Object.values(params),
2182
+ ] as string[])
2183
+ )
2184
+ throw new ToolArgumentError(FORMANT_VOWEL_HINT);
2102
2185
  const values: Record<string, number | string | boolean> = {};
2103
2186
  for (const [name, value] of Object.entries(params)) {
2104
2187
  const param = parseParamName(effect, name);
@@ -2296,6 +2379,7 @@ function instrumentName(value: unknown): {
2296
2379
  string?: { preset: string };
2297
2380
  modal?: TrackModal;
2298
2381
  wind?: TrackWind;
2382
+ vocoder?: TrackVocoder;
2299
2383
  } {
2300
2384
  const word = typeof value === "string" ? value.trim() : undefined;
2301
2385
  if (word === STRING_INSTRUMENT)
@@ -2309,6 +2393,7 @@ function instrumentName(value: unknown): {
2309
2393
  (!patch.string &&
2310
2394
  !patch.modal &&
2311
2395
  !patch.wind &&
2396
+ !patch.vocoder &&
2312
2397
  !(AVAILABLE_INSTRUMENTS as readonly string[]).includes(patch.instrument))
2313
2398
  )
2314
2399
  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"