@hraness/dawg 0.6.0 → 0.6.1

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 (93) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/DAWG.md +338 -51
  3. package/README.md +4 -4
  4. package/core/chords.ts +492 -4
  5. package/core/diff.ts +5 -0
  6. package/core/expression.ts +143 -6
  7. package/core/fx.ts +258 -6
  8. package/core/granular.ts +91 -0
  9. package/core/instruments.ts +161 -0
  10. package/core/keys.ts +573 -4
  11. package/core/resonators.ts +291 -3
  12. package/core/score.ts +349 -3
  13. package/core/sdk/print.ts +78 -3
  14. package/core/sdk/v1.ts +1320 -36
  15. package/core/sections.ts +12 -4
  16. package/core/strings.ts +237 -2
  17. package/core/winds.ts +652 -0
  18. package/guides/automation.md +1 -0
  19. package/guides/chords.md +3 -1
  20. package/guides/effects.md +3 -2
  21. package/guides/media.md +1 -1
  22. package/guides/performance.md +3 -0
  23. package/guides/resample.md +26 -0
  24. package/guides/sounds.md +7 -6
  25. package/package.json +1 -1
  26. package/src/agent/agent.ts +8 -2
  27. package/src/agent/chord-tools.ts +153 -1
  28. package/src/agent/expression-tools.ts +91 -0
  29. package/src/agent/ops.ts +4 -1
  30. package/src/agent/preview-tool.ts +2 -1
  31. package/src/agent/resample-tool.ts +131 -0
  32. package/src/agent/tools.ts +200 -15
  33. package/src/agent/xcb-agent.ts +8 -2
  34. package/src/audio/arrange.ts +21 -0
  35. package/src/audio/dsp/envelope.ts +161 -0
  36. package/src/audio/dsp/shift.ts +256 -0
  37. package/src/audio/effects/chain.ts +18 -3
  38. package/src/audio/effects/common.ts +14 -0
  39. package/src/audio/effects/convolution.ts +106 -1
  40. package/src/audio/effects/gaze.ts +354 -0
  41. package/src/audio/engine.ts +46 -5
  42. package/src/audio/fit.ts +45 -0
  43. package/src/audio/granular.ts +352 -21
  44. package/src/audio/instrument-check.ts +9 -5
  45. package/src/audio/instruments.ts +32 -3
  46. package/src/audio/keys/electric.ts +420 -0
  47. package/src/audio/keys/engine.ts +143 -19
  48. package/src/audio/keys/organ.ts +1335 -0
  49. package/src/audio/keys/piano.ts +50 -6
  50. package/src/audio/keys/sympathetic.ts +127 -0
  51. package/src/audio/live-worker.ts +3 -1
  52. package/src/audio/live.ts +136 -9
  53. package/src/audio/preview.ts +15 -1
  54. package/src/audio/resample.ts +285 -0
  55. package/src/audio/resonators.ts +10 -2
  56. package/src/audio/sampler.ts +168 -21
  57. package/src/audio/samples.ts +26 -1
  58. package/src/audio/strings/body.ts +29 -16
  59. package/src/audio/strings/bow.ts +656 -0
  60. package/src/audio/strings/engine.ts +266 -20
  61. package/src/audio/strings/measure.test-helpers.ts +2 -0
  62. package/src/audio/wav.ts +119 -19
  63. package/src/audio/winds/engine.ts +302 -0
  64. package/src/audio/winds/filters.ts +153 -0
  65. package/src/audio/winds/pitch.ts +104 -0
  66. package/src/audio/winds/trim.ts +80 -0
  67. package/src/audio/winds/trims.ts +917 -0
  68. package/src/audio/winds/voice.ts +479 -0
  69. package/src/commands/expression.ts +103 -22
  70. package/src/commands/fx.ts +4 -1
  71. package/src/commands/granular.ts +7 -2
  72. package/src/commands/help.ts +91 -8
  73. package/src/commands/keys.ts +287 -11
  74. package/src/commands/modal.ts +67 -4
  75. package/src/commands/resample.ts +281 -0
  76. package/src/commands/rig.ts +5 -1
  77. package/src/commands/sample.ts +49 -2
  78. package/src/commands/shift.ts +119 -0
  79. package/src/commands/string.ts +28 -2
  80. package/src/commands/strum.ts +473 -0
  81. package/src/commands/wind.ts +244 -0
  82. package/src/main.ts +129 -7
  83. package/src/project/check.ts +5 -1
  84. package/src/render.ts +16 -0
  85. package/src/session/presence.ts +16 -3
  86. package/src/tui/audition.ts +1 -1
  87. package/src/tui/menu.ts +637 -35
  88. package/src/tui/modal-menu.ts +34 -7
  89. package/src/tui/performance-menu.ts +42 -0
  90. package/src/tui/play-chords.ts +104 -3
  91. package/src/tui/play-mode.ts +1 -0
  92. package/src/tui/play-session.ts +34 -9
  93. package/src/tui/wind-menu.ts +144 -0
@@ -12,13 +12,16 @@ Targets are `all` (default), `bar 3`, `bars 2-4` or note ids.
12
12
 
13
13
  - "make the bass slide like a 303" · "ghost the off-beat hats"
14
14
  - "pedal the piano every bar" · "humanize the drums a little"
15
+ - "play the piano una corda" · "hold the bass note with sostenuto"
15
16
 
16
17
  ## Type it yourself
17
18
 
18
19
  - `art staccato|legato|accent|tenuto|marcato|ghost [target]`
19
20
  - `glide 60ms legato` per track · `glide 60ms bar 2` per note
21
+ - bowed strings slur overlapping or `art legato` notes in one bow stroke
20
22
  - `bend scoop|fall|doit|-200 [target]` · `vibrato 5.5 30 0.2`
21
23
  - `pedal 0-3.5 4-7.5` (beats) · `pedal bars` · `pedal off`
24
+ - `pedal soft 0-8` una corda · `pedal sost 0-4` holds keys down at 0
22
25
  - `velcurve soft|hard|fixed 0.7` · `humanize 8 5 10 seed 3`
23
26
 
24
27
  ## Menu
@@ -0,0 +1,26 @@
1
+ ---
2
+ id: resample
3
+ title: Resample, grains and shift
4
+ parent: sound
5
+ order: 8
6
+ ---
7
+
8
+ Turn a part into a sample, grain it, and play the grains like an instrument.
9
+
10
+ ## Ask
11
+
12
+ - "resample the lead and make a grain cloud of bars 1-2"
13
+ - "shift the vocal sample up a fifth but keep its character"
14
+
15
+ ## Type it yourself
16
+
17
+ - `resample lead` · `resample drums bars 1-2 grain` · `resample master section chorus`
18
+ - `shift 7` same length · `shift 7 formant keep` · `fade out 0.5` · `fade in 0.05`
19
+ - `grain sync 1/16` · `grain quant scale` · `grain mono on` · `grain pedal on`
20
+ - `automate grain-pos points 0:0.1 8:0.9` sweeps the head
21
+
22
+ ## Menu
23
+
24
+ - Ctrl-K › Project › Resample: a track, an orbit or the mix
25
+ - Ctrl-K › Sound › granular: Sync, Quant, Mono, Pedal
26
+ - Ctrl-K › Sound › Sample: Shift, Formant, Fade in, Fade out
package/guides/sounds.md CHANGED
@@ -14,16 +14,17 @@ order: 1
14
14
 
15
15
  - `instrument sawtooth` · sine square triangle supersaw pulse
16
16
  - `synth preset pluck` · pad lead bass acid keys bell organ strings
17
- - `synth lpf 800 lpenv 3` any parameter · `synth adsr 0.01 0.2 0.5 0.3`
18
- - `synth lpf off` · `synth reset` · `synth` lists what is set
19
- - `piano felt` · grand ballad upright lofi · `keys hardness 0.3` · `keys`
17
+ - `synth lpf 800 lpenv 3` · `synth adsr 0.01 0.2 0.5 0.3` · `synth reset`
18
+ - `piano felt` · `epiano suitcase` · `wurli` · `clav` · `keys hardness 0.3`
19
+ - `tonewheel` gospel · `combo` vox · `pipe` flutes · `rotary fast` · `keys`
20
20
  - `wt list` tables · `wt pwm` load one · `wt 0.5` position
21
- - `string sitar` · nylon koto harp … · `string buzz 0.8` · `string presets`
21
+ - `string sitar` · `bowed violin` · cellos erhu koto … · `string presets`
22
+ - `wind sax` · flute trumpet … · `wind breath 0.8` · `modal gamelan` bronzes
22
23
  - `/pack list` · `/pack use <pack>/<sound> as kick` · `/pack info vcsl`
23
24
  - `/sample samples/kick.wav as kick` add a sampler voice
24
- - `/bpm 174` the sample's tempo (a bare `bpm 174` is the song's) · `fitmode beats` (drums) or `tones` (pads) · `len 8`
25
+ - `/bpm 174` sample tempo (`bpm` is the song's) · `fitmode beats` · `len 8`
25
26
 
26
27
  ## Menu
27
28
 
28
29
  - Ctrl-K › Sound: instrument, preset, envelope, filter, browse sounds
29
- - Ctrl-K › Sound › browse sounds › Strings: the plucked string presets
30
+ - Ctrl-K › Sound › browse sounds › Strings: plucked presets, Bowed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/dawg",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -203,15 +203,21 @@ export const AGENT_SYSTEM_PROMPT = [
203
203
  CHORD_PROCESS,
204
204
  "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
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.",
206
+ "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.",
207
+ "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; 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.",
208
+ "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 modelled piano.",
209
+ 'Organs: set_instrument tonewheel (hammond b3 gospel jazzorgan), combo (farfisa vox) or pipe (church flutes cornet reeds celeste) loads a modelled 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
210
  "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.",
211
+ "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.",
212
+ "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.",
208
213
  "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
214
  "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
215
  "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.",
211
216
  "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.",
217
+ "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
218
  "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
219
  "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.`,
220
+ `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
221
  "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
222
  "If a call is rejected, read the diagnostic and either fix the arguments or stop.",
217
223
  WORKSPACE_PROMPT,
@@ -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)
@@ -9,6 +9,7 @@ import {
9
9
  ExpressionValidationError,
10
10
  GLIDE_MODES,
11
11
  PEDAL_STATES,
12
+ SOSTENUTO_STATES,
12
13
  VELOCITY_CURVES,
13
14
  normalizeArticulation,
14
15
  normalizeBend,
@@ -20,12 +21,14 @@ import {
20
21
  normalizeVibrato,
21
22
  normalizeNoteHumanize,
22
23
  type NoteExpressionPatch,
24
+ type PedalState,
23
25
  } from "../../core/expression.ts";
24
26
  import {
25
27
  SCORE_LIMITS,
26
28
  type ScoreOperation,
27
29
  type TrackPatch,
28
30
  } from "../../core/score.ts";
31
+ import { softPedalNote } from "../../core/keys.ts";
29
32
  import { loopTicksOf } from "../../core/tempo.ts";
30
33
  import { BEND_SHAPES, barPedal } from "../commands/expression.ts";
31
34
  import type { AgentTool, ToolContext } from "./tools.ts";
@@ -333,4 +336,92 @@ export const EXPRESSION_TOOLS: readonly AgentTool[] = Object.freeze([
333
336
  };
334
337
  },
335
338
  },
339
+ {
340
+ // keys-electric (0.6.1): the modelled piano's soft and middle pedals.
341
+ name: "set_piano_pedals",
342
+ description:
343
+ 'Modelled piano soft (una corda: fewer strings, softer, darker) and sostenuto (holds only keys down when it presses) pedals. Each: [{beat, state}], "bars" (whole loop) or null. Sostenuto has no half.',
344
+ parameters: {
345
+ type: "object",
346
+ properties: {
347
+ trackId: { type: "string" },
348
+ soft: pedalSchema(PEDAL_STATES),
349
+ sostenuto: pedalSchema(SOSTENUTO_STATES),
350
+ },
351
+ additionalProperties: false,
352
+ },
353
+ plan(args, context) {
354
+ const trackId = trackOf(args, context);
355
+ const score = context.score;
356
+ const patch: Record<string, unknown> = {};
357
+ if (args.soft !== undefined)
358
+ patch.softPedal = pedalLane(args.soft, "soft", score, PEDAL_STATES);
359
+ if (args.sostenuto !== undefined)
360
+ patch.sostenuto = pedalLane(
361
+ args.sostenuto,
362
+ "sostenuto",
363
+ score,
364
+ SOSTENUTO_STATES,
365
+ );
366
+ if (Object.keys(patch).length === 0)
367
+ throw new ExpressionToolError("set soft, sostenuto or both");
368
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
369
+ const silent =
370
+ patch.softPedal && track ? softPedalNote(track) : undefined;
371
+ if (silent) throw new ExpressionToolError(`soft pedal · ${silent}`);
372
+ return {
373
+ kind: "score",
374
+ operations: [
375
+ { type: "updateTrack", trackId, patch: patch as TrackPatch },
376
+ ],
377
+ trackId,
378
+ summary: `${trackId} ${Object.entries(patch)
379
+ .map(([key, value]) => (value === null ? `${key} off` : key))
380
+ .join(", ")}`,
381
+ };
382
+ },
383
+ },
336
384
  ] satisfies AgentTool[]);
385
+
386
+ function pedalSchema(states: readonly string[]) {
387
+ return {
388
+ type: ["array", "string", "null"],
389
+ items: {
390
+ type: "object",
391
+ properties: {
392
+ beat: { type: "number" },
393
+ state: { type: "string", enum: states },
394
+ },
395
+ },
396
+ };
397
+ }
398
+
399
+ /** A soft or sostenuto lane from tool arguments: events, "bars" or null. */
400
+ function pedalLane(
401
+ value: unknown,
402
+ label: string,
403
+ score: ToolContext["score"],
404
+ states: readonly PedalState[],
405
+ ): unknown {
406
+ if (value === null) return null;
407
+ // Soft and sostenuto hold through the loop (no re-pedal at each bar).
408
+ if (value === "bars")
409
+ return [
410
+ { tick: 0, state: "down" },
411
+ { tick: loopTicksOf(score), state: "up" },
412
+ ];
413
+ if (!Array.isArray(value))
414
+ throw new ExpressionToolError(`${label} must be an array, "bars" or null`);
415
+ const events = value.map((item: unknown, index) => {
416
+ const event = (item ?? {}) as Record<string, unknown>;
417
+ const at = beat(event, "beat");
418
+ if (at === undefined)
419
+ throw new ExpressionToolError(`${label}[${index}].beat is required`);
420
+ return { tick: Math.round(at * score.ticksPerBeat), state: event.state };
421
+ });
422
+ return (
423
+ validated(() =>
424
+ normalizePedal(events, loopTicksOf(score), label, states),
425
+ ) ?? null
426
+ );
427
+ }
package/src/agent/ops.ts CHANGED
@@ -3,6 +3,7 @@ import { pitchToMidi } from "../../core/pitch.ts";
3
3
  import {
4
4
  instrumentPatchForWord,
5
5
  type TrackModal,
6
+ type TrackWind,
6
7
  } from "../../core/resonators.ts";
7
8
  import { SCORE_LIMITS } from "../../core/score.ts";
8
9
 
@@ -206,10 +207,12 @@ export function instrumentPatch(word: string): {
206
207
  instrument: string;
207
208
  string?: { preset: string };
208
209
  modal?: TrackModal;
210
+ wind?: TrackWind;
209
211
  } {
210
212
  const meaning = resolveInstrumentWord(word);
211
213
  if (!meaning) return { instrument: word };
212
- if (meaning.field === "modal") return instrumentPatchForWord(word);
214
+ if (meaning.field === "modal" || meaning.field === "wind")
215
+ return instrumentPatchForWord(word);
213
216
  if (meaning.field === "string" && meaning.preset)
214
217
  return {
215
218
  instrument: meaning.instrument,
@@ -40,6 +40,7 @@ export const PREVIEWABLE_TOOLS: readonly string[] = Object.freeze([
40
40
  "set_synth",
41
41
  "set_string",
42
42
  "set_modal",
43
+ "set_wind",
43
44
  "set_sample",
44
45
  "fit_sample",
45
46
  "set_effects",
@@ -159,7 +160,7 @@ export function previewSoundTool(
159
160
  return {
160
161
  name: "preview_sound",
161
162
  description:
162
- "Hear a track, or a candidate sound change, without committing it. changes: sound tool calls to try ({tool, args} for set_fx, set_rig, set_synth, set_string, set_modal, set_wavetable, set_instrument, set_sample, fit_sample, set_effects, set_mix, set_automation, set_drum_kit, use_sound), applied to a copy. Renders the track's notes over up to 4 bars (or a short phrase by role when it has none), solo or in context, and returns RMS/peak dBFS, spectral centroid and a one-line description for the current and the candidate sound. When the user's window is open it plays the snippet once. Then commit with the normal tools if it sounds right.",
163
+ "Hear a track, or a candidate sound change, without committing it. changes: sound tool calls to try ({tool, args} for set_fx, set_rig, set_synth, set_string, set_modal, set_wind, set_wavetable, set_instrument, set_sample, fit_sample, set_effects, set_mix, set_automation, set_drum_kit, use_sound), applied to a copy. Renders the track's notes over up to 4 bars (or a short phrase by role when it has none), solo or in context, and returns RMS/peak dBFS, spectral centroid and a one-line description for the current and the candidate sound. When the user's window is open it plays the snippet once. Then commit with the normal tools if it sounds right.",
163
164
  parameters: {
164
165
  type: "object",
165
166
  properties: {
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Agent tool `resample` (0.6.1): the `resample` prompt command for the
3
+ * model. Renders a track, an orbit or the master over the song, a section
4
+ * or a bar range to a pinned WAV under the project and adds a sampler (or,
5
+ * with grain, a granular) track that plays it, committed as ordinary score
6
+ * operations.
7
+ */
8
+ import { SCORE_LIMITS } from "../../core/score.ts";
9
+ import {
10
+ parseResampleCommand,
11
+ runResample,
12
+ type ResampleCommand,
13
+ } from "../commands/resample.ts";
14
+ import { hasSamplerTracks, SampleLibrary } from "../audio/samples.ts";
15
+ import type { AgentTool } from "./tools.ts";
16
+
17
+ class ResampleToolError extends Error {
18
+ constructor(message: string) {
19
+ super(message);
20
+ this.name = "ToolArgumentError";
21
+ }
22
+ }
23
+
24
+ /** The tool's arguments as the prompt command they stand for. */
25
+ export function resampleToolCommand(
26
+ args: Record<string, unknown>,
27
+ focusedTrackId: string,
28
+ ): ResampleCommand {
29
+ const words = ["resample"];
30
+ const source = args.source ?? "track";
31
+ if (source === "track") {
32
+ const trackId = args.trackId ?? focusedTrackId;
33
+ if (typeof trackId !== "string" || trackId.length === 0)
34
+ throw new ResampleToolError("trackId must be a track id");
35
+ words.push(trackId);
36
+ } else if (source === "orbit") {
37
+ if (!Number.isInteger(args.orbit))
38
+ throw new ResampleToolError("orbit must be an integer");
39
+ words.push("orbit", String(args.orbit));
40
+ } else if (source === "master") words.push("master");
41
+ else throw new ResampleToolError("source is track, orbit or master");
42
+ if (args.section !== undefined) {
43
+ if (typeof args.section !== "string" || args.section.length === 0)
44
+ throw new ResampleToolError("section must be a section name");
45
+ words.push("section", args.section);
46
+ } else if (args.bars !== undefined) {
47
+ const bars = args.bars;
48
+ if (
49
+ !Array.isArray(bars) ||
50
+ bars.length !== 2 ||
51
+ !bars.every((bar) => Number.isInteger(bar))
52
+ )
53
+ throw new ResampleToolError("bars must be [first, last], 1-based");
54
+ words.push("bars", `${bars[0]}-${bars[1]}`);
55
+ }
56
+ if (args.post === true) words.push("post");
57
+ if (args.grain === true) words.push("grain");
58
+ if (args.as !== undefined) {
59
+ if (typeof args.as !== "string")
60
+ throw new ResampleToolError("as must be a track id");
61
+ words.push("as", args.as);
62
+ }
63
+ const command = parseResampleCommand(words.join(" "));
64
+ if (!command) throw new ResampleToolError(`cannot ${words.join(" ")}`);
65
+ return command;
66
+ }
67
+
68
+ export const RESAMPLE_TOOLS: readonly AgentTool[] = Object.freeze([
69
+ {
70
+ name: "resample",
71
+ description:
72
+ "Bounce a track (default focused), an orbit (no master) or the master over the song, a section or bars [first,last] to a pinned WAV, adding a one-shot sampler track that plays it in place; grain adds a granular track instead; post keeps the master chain.",
73
+ parameters: {
74
+ type: "object",
75
+ properties: {
76
+ source: { type: "string", enum: ["track", "orbit", "master"] },
77
+ trackId: { type: "string", maxLength: SCORE_LIMITS.maxIdLength },
78
+ orbit: { type: "integer", minimum: 1, maximum: 16 },
79
+ section: { type: "string" },
80
+ bars: {
81
+ type: "array",
82
+ items: { type: "integer", minimum: 1 },
83
+ minItems: 2,
84
+ maxItems: 2,
85
+ },
86
+ grain: { type: "boolean" },
87
+ post: { type: "boolean" },
88
+ as: { type: "string", maxLength: SCORE_LIMITS.maxIdLength },
89
+ },
90
+ additionalProperties: false,
91
+ },
92
+ plan(args, context) {
93
+ const command = resampleToolCommand(args, context.focusedTrackId);
94
+ return {
95
+ kind: "prepare",
96
+ summary: "resample",
97
+ run: async (action) => {
98
+ const root = action.workspace?.root ?? process.cwd();
99
+ const samples = hasSamplerTracks(context.score)
100
+ ? await new SampleLibrary({
101
+ projectRoot: root,
102
+ ...(action.packs ? { packs: action.packs } : {}),
103
+ }).load(context.score)
104
+ : undefined;
105
+ const result = await runResample({
106
+ projectRoot: root,
107
+ score: context.score,
108
+ command,
109
+ ...(samples ? { samples } : {}),
110
+ });
111
+ if (!result.ok) throw new ResampleToolError(result.message);
112
+ const track = result.next.tracks.find(
113
+ (t) => t.id === result.trackId,
114
+ )!;
115
+ const notes = result.next.notes.filter(
116
+ (note) => note.trackId === result.trackId,
117
+ );
118
+ return {
119
+ kind: "score",
120
+ operations: [
121
+ { type: "addTrack", track },
122
+ ...notes.map((note) => ({ type: "addNote" as const, note })),
123
+ ],
124
+ summary: result.message,
125
+ trackId: result.trackId,
126
+ };
127
+ },
128
+ };
129
+ },
130
+ },
131
+ ]);