@hraness/dawg 0.6.0 → 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 (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  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/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  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 +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -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,
@@ -14,6 +18,15 @@ import {
14
18
  type TrackModal,
15
19
  } from "../../core/resonators.ts";
16
20
  import { applyModalCommand, type ModalCommand } from "../commands/modal.ts";
21
+ import { applyWindCommand, type WindCommand } from "../commands/wind.ts";
22
+ import {
23
+ type TrackWind,
24
+ WIND_ALIASES,
25
+ WIND_PARAMS,
26
+ WIND_PRESET_NAMES,
27
+ windParamName,
28
+ windPresetFor,
29
+ } from "../../core/winds.ts";
17
30
  import {
18
31
  AUTOMATION_PARAMETERS,
19
32
  automationPoints,
@@ -46,9 +59,11 @@ import {
46
59
  } from "../commands/keys.ts";
47
60
  import {
48
61
  KEYS_PRESETS,
62
+ ORGAN_ALIASES,
49
63
  KEYS_SIMPLE,
50
64
  isKeysPreset,
51
65
  keysParamName,
66
+ pianoWrite,
52
67
  } from "../../core/keys.ts";
53
68
  import {
54
69
  setSampleControls,
@@ -72,6 +87,8 @@ import {
72
87
  applyFxCommand,
73
88
  effectPatch,
74
89
  effectValues,
90
+ FORMANT_VOWEL_HINT,
91
+ formantGotVowel,
75
92
  parseEffectName,
76
93
  parseParamName,
77
94
  parseFxCommand,
@@ -91,11 +108,15 @@ import { RHYTHM_TOOLS } from "./rhythm-tools.ts";
91
108
  import { CHORD_TOOLS } from "./chord-tools.ts";
92
109
  import { EXPRESSION_TOOLS } from "./expression-tools.ts";
93
110
  import { TUNING_TOOLS } from "./tuning-tools.ts";
111
+ import { CALIBRATION_TOOLS } from "./calibration-tools.ts";
94
112
  import { MASTER_TOOLS } from "./master-tools.ts";
113
+ import { STYLE_TOOLS } from "./style-tools.ts";
95
114
  import { DRUM_TOOLS } from "./drum-tools.ts";
96
115
  import { TIME_TOOLS } from "./time-tools.ts";
97
116
  import { SECTION_TOOLS } from "./section-tools.ts";
98
117
  import { GRANULAR_TOOLS } from "./granular-tools.ts";
118
+ import { RESAMPLE_TOOLS } from "./resample-tool.ts";
119
+ import { VOICE_TOOLS } from "./voice-tools.ts";
99
120
  import type { MediaResult, MediaRunContext } from "../media/types.ts";
100
121
  import { instrumentPatch, pitchToMidi } from "./ops.ts";
101
122
  import { TUNING_LIMITS } from "../../core/tuning.ts";
@@ -243,17 +264,17 @@ export type ToolContext = Readonly<{
243
264
  export type AgentTool = Readonly<{
244
265
  name: string;
245
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;
246
272
  parameters: Record<string, unknown>;
247
273
  plan: (args: Record<string, unknown>, context: ToolContext) => ToolPlan;
248
274
  }>;
249
275
 
250
- /** A model-supplied argument that the tool refused; never mutates the score. */
251
- export class ToolArgumentError extends Error {
252
- constructor(message: string) {
253
- super(message);
254
- this.name = "ToolArgumentError";
255
- }
256
- }
276
+ import { ToolArgumentError } from "./tool-error.ts";
277
+ export { ToolArgumentError };
257
278
 
258
279
  const MAX_NOTES_PER_CALL = 128;
259
280
  const MAX_EXPLAIN_CHARS = 2_000;
@@ -392,7 +413,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
392
413
  {
393
414
  name: "update_notes",
394
415
  description:
395
- "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.",
396
417
  parameters: {
397
418
  type: "object",
398
419
  properties: {
@@ -412,6 +433,13 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
412
433
  { type: "string", maxLength: 4 },
413
434
  ],
414
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
+ },
415
443
  velocity: { type: "number", minimum: 0, maximum: 1 },
416
444
  cents: {
417
445
  type: "number",
@@ -430,6 +458,9 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
430
458
  },
431
459
  plan(args, context) {
432
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[] = [];
433
464
  const operations = list(args, "updates", 1, MAX_NOTES_PER_CALL).map(
434
465
  (value, index): ScoreOperation => {
435
466
  const update = record(value, `updates[${index}]`);
@@ -447,8 +478,41 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
447
478
  });
448
479
  if (duration !== undefined)
449
480
  patch.durationTicks = Math.max(1, Math.round(duration * tpb));
481
+ const before = context.score.notes.find((n) => n.id === noteId);
450
482
  if (update.pitch !== undefined)
451
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
+ );
452
516
  const velocity = optionalNumber(update, "velocity", {
453
517
  min: 0,
454
518
  max: 1,
@@ -467,14 +531,18 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
467
531
  return {
468
532
  kind: "score",
469
533
  operations,
470
- 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
+ }`,
471
539
  };
472
540
  },
473
541
  },
474
542
  {
475
543
  name: "set_instrument",
476
544
  description:
477
- "Change a track's instrument voice. Mallets and bells (modal): modal (marimba) vibes xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani; set_modal shapes them.",
545
+ "Change a track's instrument voice. Mallets and bells (modal): modal (marimba) vibes xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani, gamelan saron gangsa bonang …; set_modal shapes them. Electric keys: epiano suitcase dyno wurli clav funkclav (set_keys). Winds and brass (wind engine): flute recorder clarinet oboe bassoon sax trumpet trombone tuba horn …; set_wind shapes them.",
478
546
  parameters: {
479
547
  type: "object",
480
548
  properties: {
@@ -490,7 +558,8 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
490
558
  const track = context.score.tracks.find((t) => t.id === trackId);
491
559
  if (track && typeof args.instrument === "string") {
492
560
  const word = args.instrument.toLowerCase();
493
- const preset = word === "piano" ? "grand" : word;
561
+ // `rhodes`, `wurlitzer`, `clavinet` name their electric presets.
562
+ const preset = pianoWrite(word)?.preset ?? word;
494
563
  if (isKeysPreset(preset)) {
495
564
  const patch = keysPresetPatch(track, preset);
496
565
  return {
@@ -507,13 +576,24 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
507
576
  return {
508
577
  kind: "score",
509
578
  operations: [
510
- { 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
+ },
511
591
  ],
512
592
  trackId,
513
593
  summary:
514
594
  patch.instrument === "marimba" && !patch.modal
515
595
  ? `${trackId} → marimba (legacy tone) · set_modal preset marimba for the mallet engine`
516
- : `${trackId} → ${patch.string?.preset ?? patch.modal?.preset ?? patch.instrument}`,
596
+ : `${trackId} → ${patch.string?.preset ?? patch.modal?.preset ?? patch.wind?.preset ?? patch.instrument}`,
517
597
  };
518
598
  },
519
599
  },
@@ -587,7 +667,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
587
667
  parameter: {
588
668
  type: "string",
589
669
  description:
590
- "volume, pan, filter, resonance, delay-feedback, delay-mix, or <effect>-<param>",
670
+ "volume, pan, filter, resonance, delay-feedback, delay-mix, <effect>-<param>, or grain-<param> on a granular track",
591
671
  },
592
672
  mode: { type: "string", enum: ["replace", "merge"] },
593
673
  points: {
@@ -713,7 +793,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
713
793
  {
714
794
  name: "set_rig",
715
795
  description:
716
- "Guitar rig on a track (stomp → amp head with noise gate → speaker cab, before tremolo). rig loads a whole rig (clean crunch punk ragged lead metal fuzz octave funk wah bachata spring bassdrive reese jangle alt) or reset removes it; stomp/head/cab set stage params (stomp type fuzz|face|od|rat|octave gain tone level; head type clean|chime|crunch|lead|high|solid|bass gain bass mid treble presence master sag gate(dB); cab type 1x12|2x12|4x12|1x10|open|8x10|1x15|di mic), or null removes a stage. Heads are level-matched. `amp` is Strudel gain, not this.",
796
+ "Guitar rig on a track (stomp → amp head with noise gate → speaker cab, before tremolo). rig loads a whole rig (clean crunch punk ragged lead metal fuzz octave funk wah bachata spring bassdrive reese jangle alt, and the shoegaze rigs shoegaze glide dreampop swell ebow that add wobble/bloom/swell/double and a long reverb) or reset removes it; stomp/head/cab set stage params (stomp type fuzz|face|od|rat|octave gain tone level; head type clean|chime|crunch|lead|high|solid|bass gain bass mid treble presence master sag gate(dB); cab type 1x12|2x12|4x12|1x10|open|8x10|1x15|di mic), or null removes a stage. Heads are level-matched. `amp` is Strudel gain, not this.",
717
797
  parameters: {
718
798
  type: "object",
719
799
  properties: {
@@ -844,7 +924,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
844
924
  {
845
925
  name: "set_string",
846
926
  description:
847
- "Make a track a plucked string (physical model): preset picks the instrument; params override it (ring s, bright, damp, pos, mute, buzz = jawari, body, sym = sympathetic strings, stiff, exciter pick|finger|hammer|noise); null unsets one. reset keeps the preset and drops overrides; off returns the track to a plain pluck voice.",
927
+ "Make a track a plucked, struck or bowed string (physical model). Bowed presets (exciter bow): violin viola cello contrabass fiddle erhu kamancheh, sections violins violas cellos contrabasses, pizz trem. preset picks the instrument; params override it (ring s, bright, damp, pos, mute, buzz = jawari, body, sym = sympathetic strings, stiff, exciter pick|finger|hammer|noise|bow); null unsets one. Bowed presets take pressure speed attack vib vibmod vibdelay tremhz sord dyn. reset keeps the preset and drops overrides; off returns the track to a plain pluck voice.",
848
928
  parameters: {
849
929
  type: "object",
850
930
  properties: {
@@ -886,17 +966,25 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
886
966
  },
887
967
  {
888
968
  name: "set_keys",
889
- description: `Shape a modelled piano: preset (instrument, keys and its effects), params ${KEYS_SIMPLE.join(" ")}… (null unsets; DAWG.md lists all), or reset. Stored "piano" stays legacy; set_instrument piano writes grand.`,
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.`,
890
970
  parameters: {
891
971
  type: "object",
892
972
  properties: {
893
973
  trackId: trackIdSchema,
894
- preset: { type: "string", enum: Object.keys(KEYS_PRESETS) },
974
+ preset: {
975
+ type: "string",
976
+ enum: [...Object.keys(KEYS_PRESETS), ...Object.keys(ORGAN_ALIASES)],
977
+ },
895
978
  reset: { type: "boolean" },
896
979
  params: {
897
980
  type: "object",
898
981
  additionalProperties: { type: ["number", "string", "null"] },
899
982
  },
983
+ // f061-organ
984
+ drawbars: { type: "string", pattern: "^[0-8]{9}$" },
985
+ registers: { type: "string", pattern: "^[0-8]{5}$" },
986
+ stops: { type: ["array", "string"], items: { type: "string" } },
987
+ rotary: { type: "string", enum: ["slow", "fast", "stop"] },
900
988
  },
901
989
  additionalProperties: false,
902
990
  },
@@ -935,7 +1023,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
935
1023
  {
936
1024
  name: "set_modal",
937
1025
  description:
938
- "Mallets and bells on the modal engine: preset (marimba vibes xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani) switches the voice and keeps overrides; mallet yarn|cord|rubber|plastic|brass; params sets MODAL_PARAMS (hardness position ring tilt release damp motor motordepth ombak buzz click strikebend strikedecay gain), null returns one to the preset; reset clears overrides. Turns the track into instrument modal.",
1026
+ "Mallets and bells on the modal engine: preset (marimba vibes xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani) switches the voice and keeps overrides (gamelan: crotales musicbox toypiano saron demung slenthem gangsa gender bonang kenong kethuk kempul; frame drums daf bodhran tabla); mallet yarn|cord|rubber|plastic|brass; params sets MODAL_PARAMS (hardness position ring tilt release damp motor motordepth ombak buzz click strikebend strikedecay gain), null returns one to the preset; pair names a partner modal track: this track (pengisep) sounds ombak Hz above it and the partner (pengumbang) gets ombak 0, null unpairs; reset clears overrides. Turns the track into instrument modal.",
939
1027
  parameters: {
940
1028
  type: "object",
941
1029
  properties: {
@@ -945,6 +1033,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
945
1033
  enum: [...MODAL_PRESET_NAMES, ...Object.keys(MODAL_ALIASES)],
946
1034
  },
947
1035
  mallet: { type: "string", enum: [...MODAL_MALLET_NAMES] },
1036
+ pair: { type: ["string", "null"] },
948
1037
  reset: { type: "boolean" },
949
1038
  params: {
950
1039
  type: "object",
@@ -974,6 +1063,11 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
974
1063
  throw new ToolArgumentError("mallet must be a string");
975
1064
  values.mallet = args.mallet;
976
1065
  }
1066
+ if (args.pair !== undefined) {
1067
+ if (args.pair !== null && typeof args.pair !== "string")
1068
+ throw new ToolArgumentError("pair must be a track id or null");
1069
+ values.pair = args.pair;
1070
+ }
977
1071
  if (args.params !== undefined) {
978
1072
  if (
979
1073
  typeof args.params !== "object" ||
@@ -1019,9 +1113,20 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1019
1113
  messages.push(result.message);
1020
1114
  }
1021
1115
  const next = score.tracks.find((t) => t.id === trackId)!;
1116
+ // A pair also sets the partner (pengumbang) to ombak 0.
1117
+ const partners = score.tracks.filter(
1118
+ (t) =>
1119
+ t.id !== trackId &&
1120
+ t.modal !== context.score.tracks.find((o) => o.id === t.id)?.modal,
1121
+ );
1022
1122
  return {
1023
1123
  kind: "score",
1024
1124
  operations: [
1125
+ ...partners.map((t) => ({
1126
+ type: "updateTrack" as const,
1127
+ trackId: t.id,
1128
+ patch: { modal: t.modal ?? null },
1129
+ })),
1025
1130
  {
1026
1131
  type: "updateTrack",
1027
1132
  trackId,
@@ -1033,10 +1138,111 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1033
1138
  };
1034
1139
  },
1035
1140
  },
1141
+ {
1142
+ name: "set_wind",
1143
+ description:
1144
+ "Winds and brass on the wind engine (breath-driven waveguides): preset (flute recorder whistle ney shakuhachi panpipe suling bansuri clarinet bassclarinet oboe bassoon sax altosax barisax trumpet harmon plunger trombone tuba horn) switches the voice and keeps overrides; params sets WIND_PARAMS (breath bright mute players growl noise attack release vib vibmod reed stopped wah wahenv flutter gain model), null returns one to the preset; players 2..8 is a section that spreads over chord tones; reset clears overrides; off returns to the legacy wind tone. Lines slur by default (a note that starts while one note is held is not re-tongued). Turns the track into the wind engine.",
1145
+ parameters: {
1146
+ type: "object",
1147
+ properties: {
1148
+ trackId: trackIdSchema,
1149
+ preset: {
1150
+ type: "string",
1151
+ enum: [...WIND_PRESET_NAMES, ...Object.keys(WIND_ALIASES)],
1152
+ },
1153
+ reset: { type: "boolean" },
1154
+ off: { type: "boolean" },
1155
+ params: {
1156
+ type: "object",
1157
+ additionalProperties: {
1158
+ type: ["number", "string", "boolean", "null"],
1159
+ },
1160
+ },
1161
+ },
1162
+ additionalProperties: false,
1163
+ },
1164
+ plan(args, context) {
1165
+ const trackId = targetTrack(args, context);
1166
+ const commands: WindCommand[] = [];
1167
+ if (args.off === true) commands.push({ type: "wind-off" });
1168
+ if (args.reset === true) commands.push({ type: "wind-reset" });
1169
+ if (args.preset !== undefined) {
1170
+ const preset =
1171
+ typeof args.preset === "string"
1172
+ ? windPresetFor(args.preset)
1173
+ : undefined;
1174
+ if (!preset)
1175
+ throw new ToolArgumentError(
1176
+ `preset must be one of ${WIND_PRESET_NAMES.join(", ")}`,
1177
+ );
1178
+ commands.push({ type: "wind-preset", preset });
1179
+ }
1180
+ const values: Record<string, number | string | boolean | null> = {};
1181
+ if (args.params !== undefined) {
1182
+ if (
1183
+ typeof args.params !== "object" ||
1184
+ args.params === null ||
1185
+ Array.isArray(args.params)
1186
+ )
1187
+ throw new ToolArgumentError("params must be an object");
1188
+ for (const [key, value] of Object.entries(args.params)) {
1189
+ const name = windParamName(key);
1190
+ if (!name)
1191
+ throw new ToolArgumentError(
1192
+ `wind has no parameter ${key} (${Object.keys(WIND_PARAMS).join(" ")})`,
1193
+ );
1194
+ if (
1195
+ value !== null &&
1196
+ typeof value !== "number" &&
1197
+ typeof value !== "string" &&
1198
+ typeof value !== "boolean"
1199
+ )
1200
+ throw new ToolArgumentError(
1201
+ `${key} must be a number, string or boolean`,
1202
+ );
1203
+ values[name] = value;
1204
+ }
1205
+ }
1206
+ if (Object.keys(values).length > 0)
1207
+ commands.push({ type: "wind-set", values });
1208
+ if (commands.length === 0) {
1209
+ // An empty call turns the track into the wind engine.
1210
+ const current = context.score.tracks.find((t) => t.id === trackId);
1211
+ commands.push({
1212
+ type: "wind-preset",
1213
+ preset:
1214
+ current?.instrument === "wind" && current.wind?.preset
1215
+ ? current.wind.preset
1216
+ : "flute",
1217
+ });
1218
+ }
1219
+ let score = context.score;
1220
+ const messages: string[] = [];
1221
+ for (const command of commands) {
1222
+ const result = applyWindCommand(score, trackId, command);
1223
+ if (!result.ok) throw new ToolArgumentError(result.message);
1224
+ if (result.next) score = result.next;
1225
+ messages.push(result.message);
1226
+ }
1227
+ const next = score.tracks.find((t) => t.id === trackId)!;
1228
+ return {
1229
+ kind: "score",
1230
+ operations: [
1231
+ {
1232
+ type: "updateTrack",
1233
+ trackId,
1234
+ patch: { instrument: next.instrument, wind: next.wind ?? null },
1235
+ },
1236
+ ],
1237
+ trackId,
1238
+ summary: `${trackId} ${messages.at(-1)}`,
1239
+ };
1240
+ },
1241
+ },
1036
1242
  {
1037
1243
  name: "set_sample",
1038
1244
  description:
1039
- "Set a sampler voice's Strudel sample controls: begin end gain speed unit(r|c|s) loop loopBegin loopEnd clip(legato) fit loopAt accelerate squiz cut; null unsets one.",
1245
+ "Set a sampler voice's Strudel sample controls: begin end gain speed unit(r|c|s) loop loopBegin loopEnd clip(legato) fit loopAt accelerate squiz cut, and (0.6.1) shift (semitones, length kept) formant (0 keeps the voice's formants) fadeInTime fadeTime (seconds), vel [lo,hi] (velocity layer) rr (round-robin group). null unsets one.",
1040
1246
  parameters: {
1041
1247
  type: "object",
1042
1248
  properties: {
@@ -1045,7 +1251,15 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1045
1251
  params: {
1046
1252
  type: "object",
1047
1253
  additionalProperties: {
1048
- type: ["number", "string", "boolean", "null"],
1254
+ anyOf: [
1255
+ { type: ["number", "string", "boolean", "null"] },
1256
+ {
1257
+ type: "array",
1258
+ items: { type: "integer", minimum: 0, maximum: 127 },
1259
+ minItems: 2,
1260
+ maxItems: 2,
1261
+ },
1262
+ ],
1049
1263
  },
1050
1264
  },
1051
1265
  },
@@ -1131,7 +1345,8 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1131
1345
  },
1132
1346
  {
1133
1347
  name: "set_effects",
1134
- 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.`,
1135
1350
  parameters: {
1136
1351
  type: "object",
1137
1352
  properties: {
@@ -1291,7 +1506,8 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1291
1506
  },
1292
1507
  {
1293
1508
  name: "add_drums",
1294
- 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.`,
1295
1511
  parameters: {
1296
1512
  type: "object",
1297
1513
  properties: {
@@ -1343,14 +1559,14 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1343
1559
  const track = context.score.tracks.find((t) => t.id === trackId)!;
1344
1560
  if (!isDrumInstrument(track.instrument))
1345
1561
  throw new ToolArgumentError(
1346
- `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`,
1347
1563
  );
1348
1564
  const tpb = context.score.ticksPerBeat;
1349
1565
  const loopBeats = loopTicksOf(context.score) / tpb;
1350
1566
  const voiceOf = (value: unknown, label: string) => {
1351
1567
  const voice =
1352
1568
  typeof value === "string" ? parseDrumVoice(value) : undefined;
1353
- if (!voice) throw new ToolArgumentError(`${label}: unknown drum voice`);
1569
+ if (!voice) throw new ToolArgumentError(`${label}: unknown drum`);
1354
1570
  return voice;
1355
1571
  };
1356
1572
  const hits: { voice: DrumVoice; beat: number; velocity: number }[] = [];
@@ -1505,7 +1721,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1505
1721
  typeof args.instrument === "string"
1506
1722
  ? args.instrument.toLowerCase()
1507
1723
  : "";
1508
- const preset = word === "piano" ? "grand" : word;
1724
+ const preset = pianoWrite(word)?.preset ?? word;
1509
1725
  if (isKeysPreset(preset)) {
1510
1726
  const patch = keysPresetPatch({}, preset);
1511
1727
  return {
@@ -1815,13 +2031,18 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1815
2031
  ...CHORD_TOOLS,
1816
2032
  ...EXPRESSION_TOOLS,
1817
2033
  ...TUNING_TOOLS,
2034
+ ...CALIBRATION_TOOLS,
1818
2035
  ...DRUM_TOOLS,
1819
2036
  ...TIME_TOOLS,
1820
2037
  ...SECTION_TOOLS,
1821
2038
  ...MEDIA_TOOLS,
1822
2039
  ...PACK_TOOLS,
1823
2040
  ...MASTER_TOOLS,
2041
+ ...STYLE_TOOLS,
1824
2042
  ...GRANULAR_TOOLS,
2043
+ ...RESAMPLE_TOOLS,
2044
+ // 0.7 Voice: one array per lane in voice-tools.ts.
2045
+ ...VOICE_TOOLS,
1825
2046
  // Looks tools up at call time, so it can plan any of the above.
1826
2047
  previewSoundTool((name) => findAgentTool(name)),
1827
2048
  ] satisfies AgentTool[]);
@@ -1918,14 +2139,16 @@ export function findAgentTool(
1918
2139
  export function chatTools(
1919
2140
  tools: readonly AgentTool[] = AGENT_TOOLS,
1920
2141
  ): ChatTool[] {
1921
- return tools.map((tool) => ({
1922
- type: "function",
1923
- function: {
1924
- name: tool.name,
1925
- description: tool.description,
1926
- parameters: tool.parameters,
1927
- },
1928
- }));
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
+ }));
1929
2152
  }
1930
2153
 
1931
2154
  function fxToolCommand(
@@ -1951,6 +2174,14 @@ function fxToolCommand(
1951
2174
  throw new ToolArgumentError("set_fx needs on, preset, or params");
1952
2175
  }
1953
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);
1954
2185
  const values: Record<string, number | string | boolean> = {};
1955
2186
  for (const [name, value] of Object.entries(params)) {
1956
2187
  const param = parseParamName(effect, name);
@@ -1969,19 +2200,54 @@ function fxToolCommand(
1969
2200
  return { type: "fx-set", effect, values };
1970
2201
  }
1971
2202
 
2203
+ /** f061-organ: set_keys drawbars/registers/stops/rotary as keys values. */
2204
+ function organToolValues(
2205
+ args: Record<string, unknown>,
2206
+ ): Record<string, string> {
2207
+ const out: Record<string, string> = {};
2208
+ for (const name of ["drawbars", "registers", "rotary"] as const) {
2209
+ const value = args[name];
2210
+ if (value === undefined) continue;
2211
+ if (typeof value !== "string")
2212
+ throw new ToolArgumentError(`keys ${name} must be a string`);
2213
+ out[name] = value;
2214
+ }
2215
+ if (args.stops !== undefined) {
2216
+ const words = Array.isArray(args.stops)
2217
+ ? args.stops
2218
+ : typeof args.stops === "string"
2219
+ ? args.stops.split(/[\s,+]+/)
2220
+ : undefined;
2221
+ if (!words || !words.every((word) => typeof word === "string"))
2222
+ throw new ToolArgumentError("keys stops must be stop names");
2223
+ out.stops = words.join(" ");
2224
+ }
2225
+ return out;
2226
+ }
2227
+
1972
2228
  function keysToolCommand(args: Record<string, unknown>): KeysCommand {
1973
2229
  if (args.reset === true) return { type: "keys-reset" };
2230
+ const organ = organToolValues(args);
2231
+ const organSet = Object.keys(organ).length > 0;
1974
2232
  if (args.preset !== undefined) {
1975
- if (typeof args.preset !== "string" || !isKeysPreset(args.preset))
2233
+ const preset =
2234
+ typeof args.preset === "string"
2235
+ ? (pianoWrite(args.preset)?.preset ?? args.preset)
2236
+ : undefined;
2237
+ if (preset === undefined || !isKeysPreset(preset))
1976
2238
  throw new ToolArgumentError(
1977
2239
  `keys presets: ${Object.keys(KEYS_PRESETS).join(", ")}`,
1978
2240
  );
1979
- return { type: "keys-preset", preset: args.preset };
2241
+ return organSet
2242
+ ? { type: "keys-preset", preset, values: organ }
2243
+ : { type: "keys-preset", preset };
1980
2244
  }
2245
+ if (args.params === undefined && organSet)
2246
+ return { type: "keys-set", values: organ };
1981
2247
  if (args.params === undefined)
1982
2248
  throw new ToolArgumentError("set_keys needs preset, reset, or params");
1983
2249
  const params = record(args.params, "params");
1984
- const values: Record<string, number | string | null> = {};
2250
+ const values: Record<string, number | string | null> = { ...organ };
1985
2251
  for (const [name, value] of Object.entries(params)) {
1986
2252
  const param = keysParamName(name);
1987
2253
  if (!param)
@@ -2112,6 +2378,8 @@ function instrumentName(value: unknown): {
2112
2378
  instrument: string;
2113
2379
  string?: { preset: string };
2114
2380
  modal?: TrackModal;
2381
+ wind?: TrackWind;
2382
+ vocoder?: TrackVocoder;
2115
2383
  } {
2116
2384
  const word = typeof value === "string" ? value.trim() : undefined;
2117
2385
  if (word === STRING_INSTRUMENT)
@@ -2124,6 +2392,8 @@ function instrumentName(value: unknown): {
2124
2392
  patch === undefined ||
2125
2393
  (!patch.string &&
2126
2394
  !patch.modal &&
2395
+ !patch.wind &&
2396
+ !patch.vocoder &&
2127
2397
  !(AVAILABLE_INSTRUMENTS as readonly string[]).includes(patch.instrument))
2128
2398
  )
2129
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"