@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,3 +1,4 @@
1
+ import type { CommandHost } from "./command-agent.ts";
1
2
  import { GRANULAR_PARAMS } from "../../core/granular.ts";
2
3
  import {
3
4
  applyScoreOperation,
@@ -38,7 +39,9 @@ import { projectOutline, type ProjectOutline } from "./workspace.ts";
38
39
  export const AGENT_LIMITS = Object.freeze({
39
40
  maxSteps: 8,
40
41
  maxToolCalls: 32,
41
- maxResponseBytes: 256 * 1024,
42
+ // Raw SSE bytes, framing included: each streamed token costs ~150-200
43
+ // bytes, so reasoning models need room beyond the visible reply.
44
+ maxResponseBytes: 1024 * 1024,
42
45
  maxToolArgumentBytes: 32 * 1024,
43
46
  timeoutMs: 90_000,
44
47
  maxTextChars: 4_000,
@@ -71,6 +74,20 @@ export type AgentEvent =
71
74
  costUsd?: number;
72
75
  }
73
76
  | { type: "text-delta"; delta: string }
77
+ /**
78
+ * Command mode: the command line the model is writing, as it streams
79
+ * (empty once the line is done), for the prompt bar's ghost text.
80
+ */
81
+ | { type: "command-typing"; text: string }
82
+ /** Command mode: one complete line ran through the typed-command path. */
83
+ | {
84
+ type: "command";
85
+ line: string;
86
+ ok: boolean;
87
+ message: string;
88
+ baseRevision: number;
89
+ resultRevision: number;
90
+ }
74
91
  /** Provider progress worth a status line (a retry), not model output. */
75
92
  | { type: "activity"; message: string }
76
93
  | { type: "tool-start"; callId: string; name: string; step: number }
@@ -165,6 +182,11 @@ export type AgentHost = Readonly<{
165
182
  packs?: PackStore;
166
183
  /** How preview_sound renders and plays (the TUI plays it once). */
167
184
  preview?: PreviewHost;
185
+ /**
186
+ * Show-me command mode: the agent writes prompt commands that run through
187
+ * the user's own command path. Absent keeps the JSON tool loop.
188
+ */
189
+ commands?: CommandHost;
168
190
  }>;
169
191
 
170
192
  export type AgentTurnOptions = Readonly<{
@@ -172,6 +194,8 @@ export type AgentTurnOptions = Readonly<{
172
194
  /** An alias (`opus-5.5`) or an exact `vendor/model` ID. */
173
195
  model: GatewayModel | string;
174
196
  client: GatewayClient;
197
+ /** A model ID to try once when `model` fails before its first byte. */
198
+ fallbackModel?: string;
175
199
  host: AgentHost;
176
200
  onEvent?: (event: AgentEvent) => void;
177
201
  signal?: AbortSignal;
@@ -193,30 +217,79 @@ export const WORKSPACE_PROMPT = [
193
217
  export const MEDIA_PROMPT =
194
218
  "Media tools (download_audio, split_stems, analyze_audio, transcribe_notes, import_sample, make_wavetable, transcribe_lyrics) work on files under tracks/<slug>/downloads/ and report project-relative paths; the brief's project tree lists what is already there (read_file reports a wav's type and size), so never download the same video twice. They can run for minutes, so call them one at a time and chain on their outputs (download → stems → analyze → notes). make_wavetable turns a download, stem or sample into tracks/<slug>/wavetables/<name>.wav and describes its sweep; play it with set_wavetable table <that path>.";
195
219
 
220
+ /**
221
+ * Lets a model end the turn in the same response as its last tool calls,
222
+ * which saves the summary-only round trip (about a third of a typical turn).
223
+ */
224
+ export const DONE_PROMPT =
225
+ 'When you are done, reply with one short sentence describing the musical change. For a tempo, mix, effect or sound change, you may instead put that sentence first, beginning "Done:", before the calls.';
226
+
227
+ /**
228
+ * Tools that write musical content (notes, rhythms, chords, structure).
229
+ * The eval shows models skipping their self-check when they end the turn
230
+ * early on these, so a step containing one always gets a reply step, even
231
+ * after "Done:". Parameter edits (tempo, mix, fx, instruments) and project
232
+ * file edits, which passed every eval run either way, end early.
233
+ */
234
+ export const CONTENT_TOOLS: ReadonlySet<string> = new Set([
235
+ "add_notes",
236
+ "update_notes",
237
+ "remove_notes",
238
+ "add_drums",
239
+ "set_rhythm",
240
+ "apply_drum_pattern",
241
+ "write_chords",
242
+ "strum_chords",
243
+ "set_automation",
244
+ "add_transition",
245
+ "edit_section",
246
+ "set_form",
247
+ "extend_loop",
248
+ "edit_clip",
249
+ "place_clip",
250
+ ]);
251
+
252
+ /** `Done: added a kick` → `added a kick`; anything else → undefined. */
253
+ export function doneSummary(text: string): string | undefined {
254
+ const match = /^\s*\**done\**\s*[:\u2014\u2013-]\s*([\s\S]*)$/i.exec(text);
255
+ if (!match) return undefined;
256
+ const rest = match[1]!.trim();
257
+ return rest ? rest.charAt(0).toUpperCase() + rest.slice(1) : "Done.";
258
+ }
259
+
196
260
  export const AGENT_SYSTEM_PROMPT = [
197
261
  "You are dawg, a loop composer inside a terminal music workstation.",
198
262
  "Edit the score only by calling the provided tools; every call is validated and applied immediately, and its result tells you the new revision.",
199
- "Times are in beats from the loop start (0-based). Keep notes inside loopBeats unless you extend the loop first.",
263
+ 'Times are in beats from the loop start (0-based), while musicians count from 1: in 4/4 "beats 2 and 4" are beats 1 and 3 of each bar here, "the and of 1" is 0.5, and bar n starts at (n-1)×beats per bar. Keep notes inside loopBeats unless you extend the loop first.',
200
264
  "Prefer a few well-formed calls (one add_notes call per track part) over many tiny ones.",
201
- 'For drums, create a track with instrument "kit" and prefer set_rhythm (Euclidean rows: pulses over steps, rotate, repeats for rolls, accent, probability, swing) so the beat stays editable as parameters; for a genre groove start from apply_drum_pattern (list_drum_patterns) and pick a sound with set_drum_kit; use add_drums only for one-off fills. Drum pitches select voices, so do not use add_notes for beats.',
265
+ 'For drums, create a track with instrument "kit" and prefer set_rhythm (Euclidean rows: pulses over steps, rotate, repeats for rolls, accent, probability, swing) so the beat stays editable as parameters; for a style groove start from apply_drum_pattern (list_drum_patterns) and pick a sound with set_drum_kit. Drum pitches select voices, so do not use add_notes for beats.',
202
266
  "Song structure: list_sections shows named sections (bars count from 1); edit_section marks, adds, duplicates, moves, renames, deletes, mutes, varies (transpose/gain) and loops them; set_form sets the play order with repeats (intro verse chorus*2 outro); add_transition generates builds (riser, snare roll, filter sweep, uplifter), drops (cut plus impact) and drum fills at section boundaries.",
203
267
  CHORD_PROCESS,
204
268
  "Synth voices: set_synth shapes a synth track with Strudel params (ADSR, lpf/lpenv filter envelopes, fm/fmh, supersaw unison/detune, vib, penv, noise; z_* ZzFX sounds take slide/pitchJump/lfo/zcrush…) or a preset (pad lead pluck bass keys bell…).",
205
- "Plucked strings: set_string makes a track a physical-model string (nylon steel electric jangle ebass slap upright motown sitar tanpura harpsichord lute oud setar tar santur dulcimer koto harp banjo tres requinto) and overrides params (ring bright damp pos mute buzz body sym). The bare words sitar, ebass, pluck keep their older voices; use set_string for the modelled ones.",
206
- "Pianos: set_instrument piano (or grand upright felt honkytonk prepared, ballad, lofi) loads a modelled piano with real hammers, stretched inharmonic strings, dampers and the sustain pedal; set_keys tweaks it (hardness touch decay release felt stretch body…, automate as keys-<param> lanes). A track already stored as instrument piano keeps the legacy tone until you set it again.",
269
+ "Plucked strings: set_string makes a track a physical-model string (nylon steel electric jangle ebass slap upright motown sitar tanpura harpsichord lute oud setar tar santur dulcimer koto harp banjo tres requinto) and overrides params (ring bright damp pos mute buzz body sym). The bare words sitar, ebass, pluck keep their older voices; use set_string for the modeled ones.",
270
+ "Bowed strings: set_string preset violin|viola|cello|contrabass|fiddle|erhu|kamancheh|violins|violas|cellos|contrabasses|pizz|trem plays a bowed physical model (bow params pressure speed attack vib vibmod vibdelay tremhz sord dyn). Overlapping or legato notes slur in one bow. The bare words cello, contrabass and strings keep their older voices; use set_string (or the words violin, viola, fiddle, erhu, violins, cellos) for the bowed ones. Sampler voices take vel [lo, hi] velocity layers and rr round-robin groups via set_sample.",
271
+ "Pianos: set_instrument piano (or grand upright felt honkytonk prepared, ballad, lofi) loads a modeled piano with real hammers, stretched inharmonic strings, dampers and the sustain pedal; set_keys tweaks it (hardness touch decay release felt stretch body…, automate as keys-<param> lanes; sym 0.5 adds sympathetic bloom under the sustain pedal for pedalled Romantic and Impressionist playing). A track already stored as instrument piano keeps the legacy tone until you set it again.",
272
+ "Electric keys: set_instrument epiano (rhodes; suitcase stereo vibe, dyno bright 80s), wurli, clav (funkclav); set_keys tweaks bark bell tone vibe (epiano), trem (wurli), pickup neck|bridge|both|out and mute (clav). Soft and sostenuto pedals: set_piano_pedals on any modeled piano.",
273
+ 'Organs: set_instrument tonewheel (hammond b3 gospel jazzorgan), combo (farfisa vox) or pipe (church flutes cornet reeds celeste) loads a modeled organ; instrument organ stays the legacy sine. set_keys drawbars "888800008" (16\' to 1\'), registers "08880" (combo), stops ["principal8","octave4"] or plenum|flutes|cornet|reeds|strings|full (pipe), rotary slow|fast|stop, params perc 3rd percdecay percvol click scanner c3 drive chiff wind trem (each family takes only its own rows); automate keys-rotary and keys-drive lanes.',
207
274
  "Mallets and bells: set_instrument vibes (or marimba via modal, xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani) plays the modal engine; set_modal changes mallet (yarn…brass), hardness, position, ring, damp, motor/motordepth (vibes tremolo), buzz, click. Plain instrument marimba stays the legacy tone.",
275
+ "Gamelan: set_modal preset saron demung slenthem gangsa gender bonang kenong kethuk kempul (gong too) with tuning slendro or pelog; for ombak (the shimmer) put the same part on two gangsa tracks and set_modal pair <other track> on one: it sounds ombak Hz (default 6) above its partner. Frame drums: daf bodhran tabla.",
276
+ "Autotune (0.7): autotune_vocal corrects pitch on a track's clips and sampler voices; presets hard robot warble trap pop natural gentle guided locked (pop by default). Scale targets use the song or track key and tuning, so maqam, raga and n-EDO work; to chord follows the chords; to notes with from <track> follows a guide melody. Preview it, and use analyze_pitch to find the key first when unsure.",
277
+ "Winds and brass: set_instrument flute recorder whistle ney shakuhachi panpipe suling bansuri clarinet bassclarinet oboe bassoon sax altosax barisax trumpet harmon plunger trombone tuba horn plays the wind engine; set_wind changes breath, bright, mute (open straight cup harmon plunger), players (2..8 spreads a section over chord tones), growl, vib. Lines slur unless re-tongued (gaps or articulations). Plain instrument wind stays the legacy tone.",
278
+ "Voices (0.7): set_instrument aah ooh choir chorale (or set_sing preset oohchoir airy glass lament soprano basso drone) plays the built-in singing voice, a synthetic glottal source through formants (never a cloned voice); set_sing params voice (auto soprano alto tenor bass), vowel (a e i o u or a>o), voices 2..8 (an ensemble), breath, vib, jitter, bright; set_vowels puts vowels on notes. Throat singing: khoomei, sygyt (whistle) and kargyraa (sub growl) hold a drone (set_sing params drone D2) and each note picks a harmonic of it.",
279
+ "Vocoder: a vocoder is a carrier track whose vocoder.src is the modulator (usually the user's vocal). make one with create_track (instrument vocoder), then set_vocoder with src set to the vocal, and mute the vocal; talkbox suits funk and soul, choir and glass suit hyperpop, robot and smear suit electronic or ambient. Vocode only the user's own or licensed audio, never an identifiable third party's speech unless the user confirms they hold the rights.",
208
280
  "Sampler voices: set_sample sets Strudel sample controls per voice (begin end speed unit loop loopBegin loopEnd clip/legato fit loopAt accelerate squiz cut); splice = slices with fit.",
209
281
  "Fitting samples (0.6): fit_sample sets a voice's own bpm (or len in beats) so it follows the song's tempo map; fitmode beats for drums and speech (hits stay sharp), tones for pads and vocals (pitch kept), repitch (default) like tape.",
210
- "Effects (set_fx; set_effects and set_automation): the brief's effects list is the fixed chain order with each effect's simple params. set_fx turns an effect on with good defaults, loads a preset or sets params by dawg or Strudel name. Lanes: volume, pan, filter, resonance, delay-feedback, delay-mix, wt and <effect>-<param> (e.g. autofilter-cutoff). Sidechain: put pads/bass on `orbit` 2 and give the kick `duck` (orbit 2, preset pump); a ducker never ducks itself.",
282
+ "Effects (set_fx and set_automation): the brief's effects list is the fixed chain order with each effect's simple params. set_fx turns an effect on with good defaults, loads a preset or sets params by dawg or Strudel name. Lanes: volume, pan, filter, resonance, delay-feedback, delay-mix, wt and <effect>-<param> (e.g. autofilter-cutoff). Sidechain: put pads/bass on `orbit` 2 and give the kick `duck` (orbit 2, preset pump); a ducker never ducks itself.",
211
283
  "Guitar rig (set_rig): rig loads a whole rig (clean crunch punk ragged lead metal fuzz octave funk wah bachata spring bassdrive reese jangle alt) on a guitar or bass track; stomp, head and cab tweak the pedal, amp (level-matched, gate in dB) and cabinet. Tracks named jangle punk funk ragged gtr-lead gtr-metal bachata start with a guitar voice and that rig. `amp` is Strudel gain; the guitar amp is head.",
284
+ "Shoegaze: rig shoegaze|glide|dreampop|swell|ebow (or a track named shoegaze); set_fx wobble (tremolo-arm bend: depth cents, rate, drift), bloom (feedback swell on the top note), swell (volume swell: time), double (second take: time, drift, width); reverb ir builtin:reverse|gate|spring. Strumming: set_guitar (tune dadgad, capo, hand, ring), strum_chords (chords or the track's own, strokes folk|pop|punk… or D-DU-UDU, speed ms), write_chords perform guitar.",
212
285
  "Previewing: preview_sound renders a track, or candidate sound tool calls (changes: [{tool, args}]), without committing, returns loudness, brightness and a comparison, and plays it once in the user's window. Use it when choosing between sounds (tables, presets, effect amounts), say how it sounds, then commit with the normal tools.",
213
286
  "Wavetable synth: set_wavetable picks a table (built-ins offline, Strudel wt_ sets fetched once) and scans it by position wt.",
214
- `Granular: set_granular makes a track a grain cloud (presets cloud hold sparkle swarm stutter microloop backwards dust) of a synth source (src synth:pad, the default; synth:<name>@<note>) or one of its own sampler voices, and overrides params (${Object.keys(GRANULAR_PARAMS).join(" ")}; repeat and hold latch the head for beat repeats, begin and end pick a region, root takes a MIDI number or note name); off returns to the previous voice; the same seed gives the same grains.`,
287
+ `Granular: set_granular makes a track a grain cloud (presets cloud hold sparkle swarm stutter microloop backwards dust) of a synth source (src synth:pad, the default; synth:<name>@<note>) or one of its own sampler voices, and overrides params (${Object.keys(GRANULAR_PARAMS).join(" ")}; repeat and hold latch the head for beat repeats, begin and end pick a region, root takes a MIDI number or note name); off returns to the previous voice; the same seed gives the same grains. Grain play: sync "1/16" puts grain onsets on the tempo grid, quant scale|chord snaps grain pitches, mono true glides legato notes through one cloud, pedal true freezes the head while the sustain pedal is down; set_automation grain-<param> lanes (grain-pos, grain-scan, grain-pitch …) move a parameter over beats. Resample: the resample tool renders a track, orbit or the mix (a section or bars) to a pinned WAV and adds a sampler track, or with grain a granular track, that plays it; set_sample shift (semitones, length kept, formant 0 keeps a voice's character) and fadeInTime/fadeTime shape a sampler voice.`,
215
288
  "Mastering: set_master sets the song master and loudness target; measure_mix reports LUFS, true peak, balance and correlation. Master only when asked for loudness or a finished sound, and measure before and after.",
216
289
  "If a call is rejected, read the diagnostic and either fix the arguments or stop.",
217
290
  WORKSPACE_PROMPT,
218
291
  MEDIA_PROMPT,
219
- "When you are done, reply with one short sentence describing the musical change.",
292
+ DONE_PROMPT,
220
293
  ].join(" ");
221
294
 
222
295
  /**
@@ -277,6 +350,7 @@ export async function runAgentTurn(
277
350
  return result;
278
351
  };
279
352
  const currentRevision = () => options.host.snapshot().revision;
353
+ const turnStartRevision = currentRevision();
280
354
 
281
355
  try {
282
356
  for (let step = 1; step <= limits.maxSteps; step += 1) {
@@ -294,9 +368,17 @@ export async function runAgentTurn(
294
368
  });
295
369
  // The brief already describes the current score, so tool results from
296
370
  // older steps only need to say what happened, not repeat it in full.
371
+ // It sits before the user request, so once this turn has edited the
372
+ // score it says so: otherwise a model reads the edited notes as the
373
+ // starting point and applies the same edit again (a transpose that
374
+ // repeats until the step limit).
375
+ const briefMessage = (json: string, revision: number) =>
376
+ revision === turnStartRevision
377
+ ? `Composition brief (JSON): ${json}`
378
+ : `Composition brief (JSON, the current score at revision ${revision}: it already includes every edit your tool calls made this turn, starting from revision ${turnStartRevision}; do not repeat them): ${json}`;
297
379
  const request: ChatMessage[] = [
298
380
  messages[0]!,
299
- { role: "system", content: `Composition brief (JSON): ${brief}` },
381
+ { role: "system", content: briefMessage(brief, snapshot.revision) },
300
382
  ...messages.slice(1).map((message, offset) => {
301
383
  const producedAt = toolResultStep.get(offset + 1);
302
384
  return message.role === "tool" &&
@@ -324,6 +406,9 @@ export async function runAgentTurn(
324
406
  messages: request,
325
407
  tools: chatToolList,
326
408
  maxResponseBytes: remaining,
409
+ ...(options.fallbackModel
410
+ ? { fallbackModelId: options.fallbackModel }
411
+ : {}),
327
412
  },
328
413
  signal,
329
414
  )) {
@@ -376,7 +461,7 @@ export async function runAgentTurn(
376
461
  }
377
462
  }
378
463
  bytesUsed += streamBytes;
379
- if (text.trim()) finalText = text.trim();
464
+ if (text.trim()) finalText = doneSummary(text) ?? text.trim();
380
465
 
381
466
  const calls = [...pending.entries()]
382
467
  .sort(([left], [right]) => left - right)
@@ -405,6 +490,11 @@ export async function runAgentTurn(
405
490
  tool_calls: assistantCalls,
406
491
  });
407
492
  let overBudget = false;
493
+ let stepMutated = 0;
494
+ let stepRejected = 0;
495
+ const stepWritesContent = calls.some((call) =>
496
+ CONTENT_TOOLS.has(call.name),
497
+ );
408
498
  for (const call of calls) {
409
499
  if (signal.aborted) throw signal.reason;
410
500
  let content: string;
@@ -439,10 +529,12 @@ export async function runAgentTurn(
439
529
  });
440
530
  if (outcome.ok) {
441
531
  applied += outcome.mutated ? 1 : 0;
532
+ stepMutated += outcome.mutated ? 1 : 0;
442
533
  if (outcome.explanation) finalText = outcome.explanation;
443
534
  emit(outcome.event);
444
535
  } else {
445
536
  rejected += 1;
537
+ stepRejected += 1;
446
538
  emit({
447
539
  type: "tool-rejected",
448
540
  callId: call.id,
@@ -455,6 +547,25 @@ export async function runAgentTurn(
455
547
  toolResultStep.set(messages.length, step);
456
548
  messages.push({ role: "tool", tool_call_id: call.id, content });
457
549
  }
550
+ // "Done:" in the same response as the calls ends the turn here, but
551
+ // only when something changed and nothing was rejected: a rejection
552
+ // still goes back to the model so it can fix the arguments.
553
+ const done = doneSummary(text);
554
+ if (
555
+ done !== undefined &&
556
+ stepMutated > 0 &&
557
+ stepRejected === 0 &&
558
+ !stepWritesContent
559
+ ) {
560
+ return finish({
561
+ type: "done",
562
+ reason: "stop",
563
+ text: done.slice(0, AGENT_LIMITS.maxTextChars),
564
+ applied,
565
+ rejected,
566
+ revision: currentRevision(),
567
+ });
568
+ }
458
569
  if (overBudget || toolCalls >= limits.maxToolCalls) {
459
570
  return finish({
460
571
  type: "done",
@@ -831,7 +942,7 @@ export function classifyAgentError(
831
942
  signal: AbortSignal,
832
943
  userSignal: AbortSignal | undefined,
833
944
  ): { code: AgentErrorCode; message: string } {
834
- if (userSignal?.aborted) return { code: "aborted", message: "cancelled" };
945
+ if (userSignal?.aborted) return { code: "aborted", message: "canceled" };
835
946
  if (
836
947
  error instanceof AgentTimeoutError ||
837
948
  signal.reason instanceof AgentTimeoutError
@@ -851,7 +962,14 @@ export function classifyAgentError(
851
962
  if (error instanceof Error && error.name === "XcbError")
852
963
  return { code: "provider", message: errorMessage(error) };
853
964
  if (error instanceof Error && error.name === "AbortError")
854
- return { code: "aborted", message: "cancelled" };
965
+ return { code: "aborted", message: "canceled" };
966
+ // A transport timeout (AbortSignal.timeout's DOMException) that escaped the
967
+ // client is still the provider's failure, not an internal one.
968
+ if (error instanceof Error && error.name === "TimeoutError")
969
+ return {
970
+ code: "provider",
971
+ message: `model request timed out: ${errorMessage(error)}`,
972
+ };
855
973
  return { code: "internal", message: errorMessage(error) };
856
974
  }
857
975
 
@@ -891,7 +1009,7 @@ export function describeAgentEvent(event: AgentEvent): string | undefined {
891
1009
  );
892
1010
  case "error":
893
1011
  return event.code === "aborted"
894
- ? `cancelled · kept rev ${event.revision}`
1012
+ ? `canceled · kept rev ${event.revision}`
895
1013
  : `agent ${event.code}: ${event.message}`;
896
1014
  }
897
1015
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Agent tool `set_calibration`: runs the same `/calibration` command the
3
+ * prompt and the menu do (src/commands/calibration.ts).
4
+ */
5
+
6
+ import { CALIBRATION_LATEST } from "../../core/score.ts";
7
+ import {
8
+ applyCalibrationCommand,
9
+ parseCalibrationCommand,
10
+ } from "../commands/calibration.ts";
11
+ import { ToolArgumentError } from "./tool-error.ts";
12
+ import type { AgentTool } from "./tools.ts";
13
+
14
+ const SET_CALIBRATION: AgentTool = {
15
+ name: "set_calibration",
16
+ description: `Set the song's sound calibration: ${CALIBRATION_LATEST} (latest) chokes the open hat, tunes GM toms, adds crash, ride and cowbell, levels keys presets and steadies lip brass; 0 keeps the legacy 0.4 to 0.6.1 sound byte-identical. New songs start at the latest.`,
17
+ parameters: {
18
+ type: "object",
19
+ properties: {
20
+ calibration: { type: "integer", minimum: 0, maximum: CALIBRATION_LATEST },
21
+ },
22
+ required: ["calibration"],
23
+ additionalProperties: false,
24
+ },
25
+ plan(args, context) {
26
+ const value = args.calibration;
27
+ if (
28
+ typeof value !== "number" ||
29
+ !Number.isInteger(value) ||
30
+ value < 0 ||
31
+ value > CALIBRATION_LATEST
32
+ )
33
+ throw new ToolArgumentError(
34
+ `calibration must be an integer 0..${CALIBRATION_LATEST}`,
35
+ );
36
+ const result = applyCalibrationCommand(
37
+ context.score,
38
+ parseCalibrationCommand(`calibration ${value}`)!,
39
+ );
40
+ if (!result.ok) throw new ToolArgumentError(result.message);
41
+ return {
42
+ kind: "score",
43
+ operations: [
44
+ { type: "setCalibration", calibration: value === 0 ? null : value },
45
+ ],
46
+ summary: result.message,
47
+ };
48
+ },
49
+ };
50
+
51
+ export const CALIBRATION_TOOLS: readonly AgentTool[] = Object.freeze([
52
+ SET_CALIBRATION,
53
+ ]);
@@ -12,8 +12,12 @@ import {
12
12
  generateProgression,
13
13
  keyName,
14
14
  parseKey,
15
+ DEFAULT_STROKE_SPEED,
16
+ GUITAR_TUNING_NAMES,
15
17
  PERFORM_MODES,
16
18
  PROGRESSION_PRESETS,
19
+ STROKE_PATTERN_NAMES,
20
+ strokeGrid,
17
21
  PROGRESSION_STYLES,
18
22
  renderProgression,
19
23
  resolveChord,
@@ -27,6 +31,12 @@ import { midiToPitch } from "../../core/pitch.ts";
27
31
  import { resolveTuning, snapToTuning } from "../../core/tuning.ts";
28
32
  import { SCORE_LIMITS, type ScoreOperation } from "../../core/score.ts";
29
33
  import type { AgentTool, ToolContext } from "./tools.ts";
34
+ import {
35
+ applyGuitarCommand,
36
+ parseGuitarCommand,
37
+ planStrum,
38
+ StrumError,
39
+ } from "../commands/strum.ts";
30
40
 
31
41
  /** Most notes one write_chords call may add (arpeggios multiply fast). */
32
42
  export const MAX_CHORD_NOTES = 512;
@@ -193,7 +203,9 @@ export const CHORD_TOOLS: readonly AgentTool[] = Object.freeze([
193
203
  perform: { type: "string", enum: [...PERFORM_MODES] },
194
204
  rate: { ...num, description: "arp step, beats" },
195
205
  octaves: int,
196
- strum: num,
206
+ strum: { ...num, description: "strum/strum-up gap, beats" },
207
+ strokes: { ...str, description: "perform guitar: see strum_chords" },
208
+ speed: { ...num, description: "perform guitar: ms per stroke" },
197
209
  velocity: num,
198
210
  pattern: { type: "string", enum: CHORD_PATTERNS.map((p) => p.name) },
199
211
  bass: bool,
@@ -225,6 +237,15 @@ export const CHORD_TOOLS: readonly AgentTool[] = Object.freeze([
225
237
  const velocity = finite(args, "velocity", 0, 1);
226
238
  const rate = finite(args, "rate", 0.0625, 4);
227
239
  const strum = finite(args, "strum", 0, 1);
240
+ const speedMs = finite(args, "speed", 0, 200);
241
+ const strokes = args.strokes;
242
+ if (
243
+ strokes !== undefined &&
244
+ (typeof strokes !== "string" || !strokeGrid(strokes))
245
+ )
246
+ throw new ChordToolError(
247
+ `strokes must be ${STROKE_PATTERN_NAMES.join(", ")} or a grid of D U d u x - .`,
248
+ );
228
249
  const octaves = integer(args, "octaves", 1, 4);
229
250
  const pattern = args.pattern;
230
251
  if (
@@ -259,6 +280,16 @@ export const CHORD_TOOLS: readonly AgentTool[] = Object.freeze([
259
280
  ...(velocity !== undefined ? { velocity } : {}),
260
281
  seed: integer(args, "seed", 0, 2 ** 31) ?? 0,
261
282
  ...(typeof pattern === "string" ? { pattern } : {}),
283
+ ...(mode === "guitar"
284
+ ? {
285
+ ...(typeof strokes === "string" ? { strokes } : {}),
286
+ speed: (speedMs ?? DEFAULT_STROKE_SPEED * 1000) / 1000,
287
+ tempo: context.score.tempoBpm,
288
+ ...(guitarOf(context, trackId)
289
+ ? { guitar: guitarOf(context, trackId) }
290
+ : {}),
291
+ }
292
+ : {}),
262
293
  },
263
294
  });
264
295
  const total = rendered.notes.length + rendered.bass.length;
@@ -308,8 +339,129 @@ export const CHORD_TOOLS: readonly AgentTool[] = Object.freeze([
308
339
  };
309
340
  },
310
341
  },
342
+ {
343
+ name: "strum_chords",
344
+ description: `Strum chords as fretted guitar notes (set_guitar). chords omitted strums the track's block chords. strokes ${STROKE_PATTERN_NAMES.join("|")} or a DUdux-. grid; speed ms/stroke (22); step grid beats; each beats/chord (a bar).`,
345
+ parameters: {
346
+ type: "object",
347
+ properties: {
348
+ trackId: str,
349
+ chords: chordsSchema,
350
+ strokes: str,
351
+ speed: num,
352
+ step: num,
353
+ each: num,
354
+ at: num,
355
+ velocity: num,
356
+ seed: int,
357
+ },
358
+ additionalProperties: false,
359
+ },
360
+ plan(args, context) {
361
+ const trackId = trackOf(args.trackId, context, "trackId");
362
+ const chords = args.chords;
363
+ if (
364
+ chords !== undefined &&
365
+ (!Array.isArray(chords) ||
366
+ chords.length > MAX_CHORDS ||
367
+ !chords.every((c) => typeof c === "string"))
368
+ )
369
+ throw new ChordToolError(`chords must be up to ${MAX_CHORDS} strings`);
370
+ if (args.strokes !== undefined && typeof args.strokes !== "string")
371
+ throw new ChordToolError("strokes must be a string");
372
+ const speedMs = finite(args, "speed", 0, 200);
373
+ const step = finite(args, "step", 1 / 32, 4);
374
+ const each = finite(args, "each", 1e-3, 64);
375
+ const at = finite(args, "at", 0, Infinity);
376
+ const velocity = finite(args, "velocity", 0, 1);
377
+ const seed = integer(args, "seed", 0, 2 ** 31);
378
+ try {
379
+ const plan = planStrum(
380
+ context.score,
381
+ trackId,
382
+ {
383
+ chords: (chords as string[] | undefined) ?? [],
384
+ ...(typeof args.strokes === "string"
385
+ ? { strokes: args.strokes }
386
+ : {}),
387
+ ...(speedMs !== undefined ? { speedMs } : {}),
388
+ ...(step !== undefined ? { step } : {}),
389
+ ...(each !== undefined ? { each } : {}),
390
+ ...(at !== undefined ? { at } : {}),
391
+ ...(velocity !== undefined ? { velocity } : {}),
392
+ ...(seed !== undefined ? { seed } : {}),
393
+ },
394
+ (index) => context.newNoteId(trackId, index),
395
+ MAX_CHORD_NOTES,
396
+ );
397
+ return {
398
+ kind: "score",
399
+ operations: plan.operations,
400
+ trackId,
401
+ summary: `${trackId} strum ${plan.names.join(" ")}`.slice(0, 160),
402
+ };
403
+ } catch (error) {
404
+ if (error instanceof StrumError)
405
+ throw new ChordToolError(error.message);
406
+ throw error;
407
+ }
408
+ },
409
+ },
410
+ {
411
+ name: "set_guitar",
412
+ description: `Guitar fretting for strum: tune ${GUITAR_TUNING_NAMES.join("|")} or notes low-high ("D A D G A D"), capo 0..12, hand 3..6 frets, ring 0..1, position 0..12, reset.`,
413
+ parameters: {
414
+ type: "object",
415
+ properties: {
416
+ trackId: str,
417
+ tune: str,
418
+ capo: int,
419
+ hand: int,
420
+ ring: num,
421
+ position: int,
422
+ reset: bool,
423
+ },
424
+ additionalProperties: false,
425
+ },
426
+ plan(args, context) {
427
+ const trackId = trackOf(args.trackId, context, "trackId");
428
+ let score = context.score;
429
+ const messages: string[] = [];
430
+ const run = (command: string) => {
431
+ const parsed = parseGuitarCommand(command);
432
+ const result = parsed
433
+ ? applyGuitarCommand(score, trackId, parsed)
434
+ : { ok: false, message: `cannot parse ${command}` };
435
+ if (!result.ok) throw new ChordToolError(result.message);
436
+ if ("next" in result && result.next) score = result.next;
437
+ messages.push(result.message);
438
+ };
439
+ if (args.reset === true) run("guitar reset");
440
+ if (args.tune !== undefined) run(`guitar tune ${String(args.tune)}`);
441
+ for (const field of ["capo", "hand", "ring", "position"] as const)
442
+ if (args[field] !== undefined)
443
+ run(`guitar ${field} ${String(args[field])}`);
444
+ if (messages.length === 0)
445
+ throw new ChordToolError(
446
+ "give tune, capo, hand, ring, position or reset",
447
+ );
448
+ const guitar = score.tracks.find((t) => t.id === trackId)?.guitar;
449
+ return {
450
+ kind: "score",
451
+ operations: [
452
+ { type: "updateTrack", trackId, patch: { guitar: guitar ?? null } },
453
+ ],
454
+ trackId,
455
+ summary: messages.at(-1)!,
456
+ };
457
+ },
458
+ },
311
459
  ]);
312
460
 
461
+ function guitarOf(context: ToolContext, trackId: string) {
462
+ return context.score.tracks.find((track) => track.id === trackId)?.guitar;
463
+ }
464
+
313
465
  function trackOf(value: unknown, context: ToolContext, label: string): string {
314
466
  const trackId = value ?? context.focusedTrackId;
315
467
  if (typeof trackId !== "string" || trackId.length > SCORE_LIMITS.maxIdLength)