@hraness/dawg 0.5.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 (124) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/DAWG.md +592 -49
  3. package/README.md +4 -4
  4. package/core/chords.ts +492 -4
  5. package/core/diff.ts +9 -0
  6. package/core/expression.ts +143 -6
  7. package/core/fx.ts +689 -2
  8. package/core/granular.ts +619 -0
  9. package/core/instruments.ts +442 -0
  10. package/core/keys.ts +955 -0
  11. package/core/resonators.ts +862 -0
  12. package/core/score.ts +529 -8
  13. package/core/sdk/eval-child.ts +2 -0
  14. package/core/sdk/print.ts +247 -16
  15. package/core/sdk/sync-instruments.ts +58 -0
  16. package/core/sdk/v1.ts +2417 -40
  17. package/core/sections.ts +44 -10
  18. package/core/strings.ts +1080 -0
  19. package/core/winds.ts +652 -0
  20. package/guides/automation.md +1 -0
  21. package/guides/chords.md +3 -1
  22. package/guides/effects.md +5 -2
  23. package/guides/media.md +1 -1
  24. package/guides/performance.md +3 -0
  25. package/guides/resample.md +26 -0
  26. package/guides/sounds.md +7 -2
  27. package/guides/tempo.md +1 -0
  28. package/package.json +1 -1
  29. package/src/agent/agent.ts +13 -0
  30. package/src/agent/chord-tools.ts +153 -1
  31. package/src/agent/expression-tools.ts +91 -0
  32. package/src/agent/granular-tools.ts +138 -0
  33. package/src/agent/models.ts +4 -4
  34. package/src/agent/ops.ts +42 -2
  35. package/src/agent/preview-tool.ts +7 -1
  36. package/src/agent/resample-tool.ts +131 -0
  37. package/src/agent/tools.ts +726 -17
  38. package/src/agent/xcb-agent.ts +13 -0
  39. package/src/audio/arrange.ts +42 -3
  40. package/src/audio/dsp/bank.ts +233 -0
  41. package/src/audio/dsp/envelope.ts +161 -0
  42. package/src/audio/dsp/fft.ts +6 -0
  43. package/src/audio/dsp/filters.ts +57 -0
  44. package/src/audio/dsp/interp.ts +112 -0
  45. package/src/audio/dsp/modal.ts +684 -0
  46. package/src/audio/dsp/onset.ts +197 -0
  47. package/src/audio/dsp/oversample.ts +202 -0
  48. package/src/audio/dsp/rng.ts +37 -0
  49. package/src/audio/dsp/shape.ts +81 -0
  50. package/src/audio/dsp/shift.ts +256 -0
  51. package/src/audio/dsp/stft.ts +73 -0
  52. package/src/audio/dsp/window.ts +77 -0
  53. package/src/audio/effects/chain.ts +23 -3
  54. package/src/audio/effects/common.ts +14 -0
  55. package/src/audio/effects/convolution.ts +106 -1
  56. package/src/audio/effects/gaze.ts +354 -0
  57. package/src/audio/effects/rig/cab.ts +99 -0
  58. package/src/audio/effects/rig/filters.ts +152 -0
  59. package/src/audio/effects/rig/gate.ts +39 -0
  60. package/src/audio/effects/rig/head.ts +487 -0
  61. package/src/audio/effects/rig/index.ts +170 -0
  62. package/src/audio/effects/rig/section.ts +78 -0
  63. package/src/audio/effects/rig/stomp.ts +238 -0
  64. package/src/audio/engine.ts +56 -6
  65. package/src/audio/fit.ts +447 -0
  66. package/src/audio/granular.ts +995 -0
  67. package/src/audio/instrument-check.ts +137 -0
  68. package/src/audio/instruments.ts +140 -0
  69. package/src/audio/keys/dsp.ts +323 -0
  70. package/src/audio/keys/electric.ts +420 -0
  71. package/src/audio/keys/engine.ts +485 -0
  72. package/src/audio/keys/organ.ts +1335 -0
  73. package/src/audio/keys/piano.ts +476 -0
  74. package/src/audio/keys/sympathetic.ts +127 -0
  75. package/src/audio/live-worker.ts +56 -0
  76. package/src/audio/live.ts +361 -15
  77. package/src/audio/loudness.ts +65 -20
  78. package/src/audio/preview.ts +17 -1
  79. package/src/audio/resample.ts +285 -0
  80. package/src/audio/resonators.ts +295 -0
  81. package/src/audio/sampler.ts +328 -35
  82. package/src/audio/samples.ts +56 -8
  83. package/src/audio/strings/body.ts +263 -0
  84. package/src/audio/strings/bow.ts +656 -0
  85. package/src/audio/strings/engine.ts +615 -0
  86. package/src/audio/strings/loop.ts +119 -0
  87. package/src/audio/strings/measure.test-helpers.ts +200 -0
  88. package/src/audio/strings/pluck.ts +354 -0
  89. package/src/audio/warp.ts +61 -0
  90. package/src/audio/wav.ts +191 -19
  91. package/src/audio/winds/engine.ts +302 -0
  92. package/src/audio/winds/filters.ts +153 -0
  93. package/src/audio/winds/pitch.ts +104 -0
  94. package/src/audio/winds/trim.ts +80 -0
  95. package/src/audio/winds/trims.ts +917 -0
  96. package/src/audio/winds/voice.ts +479 -0
  97. package/src/commands/expression.ts +111 -23
  98. package/src/commands/fit.ts +135 -0
  99. package/src/commands/fx.ts +35 -1
  100. package/src/commands/granular.ts +432 -0
  101. package/src/commands/help.ts +159 -5
  102. package/src/commands/keys.ts +629 -0
  103. package/src/commands/modal.ts +310 -0
  104. package/src/commands/resample.ts +281 -0
  105. package/src/commands/rig.ts +264 -0
  106. package/src/commands/sample.ts +52 -2
  107. package/src/commands/shift.ts +119 -0
  108. package/src/commands/string.ts +201 -0
  109. package/src/commands/strum.ts +473 -0
  110. package/src/commands/time.ts +4 -1
  111. package/src/commands/wind.ts +244 -0
  112. package/src/main.ts +339 -15
  113. package/src/project/check.ts +17 -0
  114. package/src/render.ts +49 -4
  115. package/src/session/presence.ts +16 -3
  116. package/src/tui/audition.ts +2 -2
  117. package/src/tui/granular-menu.ts +278 -0
  118. package/src/tui/menu.ts +934 -9
  119. package/src/tui/modal-menu.ts +145 -0
  120. package/src/tui/performance-menu.ts +42 -0
  121. package/src/tui/play-chords.ts +104 -3
  122. package/src/tui/play-mode.ts +1 -0
  123. package/src/tui/play-session.ts +70 -2
  124. package/src/tui/wind-menu.ts +144 -0
@@ -1,3 +1,28 @@
1
+ import { INSTRUMENT_WORDS } from "../../core/instruments.ts";
2
+ import {
3
+ DEFAULT_STRING_PRESET,
4
+ STRING_INSTRUMENT,
5
+ } from "../../core/strings.ts";
6
+ import {
7
+ instrumentPatchForWord,
8
+ MODAL_ALIASES,
9
+ MODAL_MALLET_NAMES,
10
+ MODAL_PARAMS,
11
+ MODAL_PRESET_NAMES,
12
+ modalParamName,
13
+ modalPresetFor,
14
+ type TrackModal,
15
+ } from "../../core/resonators.ts";
16
+ import { applyModalCommand, type ModalCommand } from "../commands/modal.ts";
17
+ import { applyWindCommand, type WindCommand } from "../commands/wind.ts";
18
+ import {
19
+ type TrackWind,
20
+ WIND_ALIASES,
21
+ WIND_PARAMS,
22
+ WIND_PRESET_NAMES,
23
+ windParamName,
24
+ windPresetFor,
25
+ } from "../../core/winds.ts";
1
26
  import {
2
27
  AUTOMATION_PARAMETERS,
3
28
  automationPoints,
@@ -16,6 +41,26 @@ import {
16
41
  synthParamName,
17
42
  } from "../../core/synth.ts";
18
43
  import { applySynthCommand, type SynthCommand } from "../commands/synth.ts";
44
+ import { applyStringCommand, type StringCommand } from "../commands/string.ts";
45
+ import {
46
+ STRING_PARAMS,
47
+ STRING_PRESET_NAMES,
48
+ stringParamName,
49
+ stringPresetName,
50
+ } from "../../core/strings.ts";
51
+ import {
52
+ applyKeysCommand,
53
+ keysPresetPatch,
54
+ type KeysCommand,
55
+ } from "../commands/keys.ts";
56
+ import {
57
+ KEYS_PRESETS,
58
+ ORGAN_ALIASES,
59
+ KEYS_SIMPLE,
60
+ isKeysPreset,
61
+ keysParamName,
62
+ pianoWrite,
63
+ } from "../../core/keys.ts";
19
64
  import {
20
65
  setSampleControls,
21
66
  type SampleControlValue,
@@ -23,16 +68,26 @@ import {
23
68
  import {
24
69
  EFFECT_NAMES,
25
70
  FX_PRESETS,
71
+ RIG_STAGES,
26
72
  effectSpec,
27
73
  type EffectName,
28
74
  } from "../../core/fx.ts";
75
+ import {
76
+ RIG_PRESET_NAMES,
77
+ applyRigCommand,
78
+ parseRigCommand,
79
+ rigTrackFields,
80
+ rigWordPatch,
81
+ } from "../commands/rig.ts";
29
82
  import {
30
83
  applyFxCommand,
31
84
  effectPatch,
32
85
  effectValues,
33
86
  parseEffectName,
34
87
  parseParamName,
88
+ parseFxCommand,
35
89
  type FxCommand,
90
+ type FxResult,
36
91
  } from "../commands/fx.ts";
37
92
  import type { ChatTool } from "./gateway.ts";
38
93
  import { MEDIA_TOOLS } from "../media/tools.ts";
@@ -51,8 +106,10 @@ import { MASTER_TOOLS } from "./master-tools.ts";
51
106
  import { DRUM_TOOLS } from "./drum-tools.ts";
52
107
  import { TIME_TOOLS } from "./time-tools.ts";
53
108
  import { SECTION_TOOLS } from "./section-tools.ts";
109
+ import { GRANULAR_TOOLS } from "./granular-tools.ts";
110
+ import { RESAMPLE_TOOLS } from "./resample-tool.ts";
54
111
  import type { MediaResult, MediaRunContext } from "../media/types.ts";
55
- import { pitchToMidi } from "./ops.ts";
112
+ import { instrumentPatch, pitchToMidi } from "./ops.ts";
56
113
  import { TUNING_LIMITS } from "../../core/tuning.ts";
57
114
  import {
58
115
  DRUM_VOICES,
@@ -86,6 +143,13 @@ import {
86
143
  type SearchSpend,
87
144
  } from "../web/search.ts";
88
145
 
146
+ /** Instrument words the tools accept: legacy voices, `string`, 0.6 words. */
147
+ const INSTRUMENT_ENUM: string[] = [
148
+ ...AVAILABLE_INSTRUMENTS,
149
+ STRING_INSTRUMENT,
150
+ ...INSTRUMENT_WORDS.map((row) => row.word),
151
+ ].filter((word, index, all) => all.indexOf(word) === index);
152
+
89
153
  /** What a validated tool call asks the host to do. */
90
154
  export type ScorePlan = Readonly<{
91
155
  kind: "score";
@@ -421,24 +485,48 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
421
485
  },
422
486
  {
423
487
  name: "set_instrument",
424
- description: "Change a track's instrument voice.",
488
+ description:
489
+ "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.",
425
490
  parameters: {
426
491
  type: "object",
427
492
  properties: {
428
493
  trackId: trackIdSchema,
429
- instrument: { type: "string", enum: [...AVAILABLE_INSTRUMENTS] },
494
+ instrument: { type: "string", enum: INSTRUMENT_ENUM },
430
495
  },
431
496
  required: ["instrument"],
432
497
  additionalProperties: false,
433
498
  },
434
499
  plan(args, context) {
435
500
  const trackId = targetTrack(args, context);
436
- const instrument = instrumentName(args.instrument);
501
+ // piano/grand/upright/felt…: a new write is the modelled piano.
502
+ const track = context.score.tracks.find((t) => t.id === trackId);
503
+ if (track && typeof args.instrument === "string") {
504
+ const word = args.instrument.toLowerCase();
505
+ // `rhodes`, `wurlitzer`, `clavinet` name their electric presets.
506
+ const preset = pianoWrite(word)?.preset ?? word;
507
+ if (isKeysPreset(preset)) {
508
+ const patch = keysPresetPatch(track, preset);
509
+ return {
510
+ kind: "score",
511
+ operations: [{ type: "updateTrack", trackId, patch }],
512
+ trackId,
513
+ summary: `${trackId} → ${patch.instrument} (${preset})`,
514
+ };
515
+ }
516
+ }
517
+ const patch = instrumentName(args.instrument);
518
+ // A guitar alias (`jangle`, `gtr-metal`…) also loads its rig.
519
+ const rig = rigWordPatch(String(args.instrument), track?.fx);
437
520
  return {
438
521
  kind: "score",
439
- operations: [{ type: "updateTrack", trackId, patch: { instrument } }],
522
+ operations: [
523
+ { type: "updateTrack", trackId, patch: { ...patch, ...rig } },
524
+ ],
440
525
  trackId,
441
- summary: `${trackId} → ${instrument}`,
526
+ summary:
527
+ patch.instrument === "marimba" && !patch.modal
528
+ ? `${trackId} → marimba (legacy tone) · set_modal preset marimba for the mallet engine`
529
+ : `${trackId} → ${patch.string?.preset ?? patch.modal?.preset ?? patch.wind?.preset ?? patch.instrument}`,
442
530
  };
443
531
  },
444
532
  },
@@ -512,7 +600,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
512
600
  parameter: {
513
601
  type: "string",
514
602
  description:
515
- "volume, pan, filter, resonance, delay-feedback, delay-mix, or <effect>-<param>",
603
+ "volume, pan, filter, resonance, delay-feedback, delay-mix, <effect>-<param>, or grain-<param> on a granular track",
516
604
  },
517
605
  mode: { type: "string", enum: ["replace", "merge"] },
518
606
  points: {
@@ -635,6 +723,92 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
635
723
  };
636
724
  },
637
725
  },
726
+ {
727
+ name: "set_rig",
728
+ description:
729
+ "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.",
730
+ parameters: {
731
+ type: "object",
732
+ properties: {
733
+ trackId: trackIdSchema,
734
+ rig: { type: "string", enum: [...RIG_PRESET_NAMES, "reset"] },
735
+ ...Object.fromEntries(
736
+ RIG_STAGES.map((stage) => [
737
+ stage,
738
+ {
739
+ type: ["object", "null"],
740
+ additionalProperties: { type: ["number", "string"] },
741
+ },
742
+ ]),
743
+ ),
744
+ },
745
+ additionalProperties: false,
746
+ },
747
+ plan(args, context) {
748
+ const trackId = targetTrack(args, context);
749
+ let score = context.score;
750
+ const messages: string[] = [];
751
+ const run = (result: FxResult) => {
752
+ if (!result.ok) throw new ToolArgumentError(result.message);
753
+ if (result.next) score = result.next;
754
+ messages.push(result.message);
755
+ };
756
+ if (args.rig !== undefined) {
757
+ if (typeof args.rig !== "string")
758
+ throw new ToolArgumentError("rig must be a rig name or reset");
759
+ const command = parseRigCommand(`rig ${args.rig}`);
760
+ if (!command || command.type === "rig-show")
761
+ throw new ToolArgumentError(
762
+ `rig must be one of ${RIG_PRESET_NAMES.join(", ")} or reset`,
763
+ );
764
+ run(applyRigCommand(score, trackId, command));
765
+ }
766
+ for (const stage of RIG_STAGES) {
767
+ const params = args[stage];
768
+ if (params === undefined) continue;
769
+ if (params === null) {
770
+ run(
771
+ applyFxCommand(score, trackId, { type: "fx-off", effect: stage }),
772
+ );
773
+ continue;
774
+ }
775
+ if (typeof params !== "object" || Array.isArray(params))
776
+ throw new ToolArgumentError(`${stage} must be an object or null`);
777
+ const words = Object.entries(params as Record<string, unknown>)
778
+ .map(([key, value]) => `${key} ${String(value)}`)
779
+ .join(" ");
780
+ const command = words.length
781
+ ? parseFxCommand(`fx ${stage} ${words}`)
782
+ : ({ type: "fx-on", effect: stage } as const);
783
+ if (!command)
784
+ throw new ToolArgumentError(
785
+ `${stage}: unknown parameter or value in ${words}`,
786
+ );
787
+ run(applyFxCommand(score, trackId, command));
788
+ }
789
+ if (messages.length === 0)
790
+ throw new ToolArgumentError("give rig, stomp, head or cab");
791
+ const next = score.tracks.find((t) => t.id === trackId)!;
792
+ const before = context.score.tracks.find((t) => t.id === trackId)!;
793
+ return {
794
+ kind: "score",
795
+ operations: [
796
+ {
797
+ type: "updateTrack",
798
+ trackId,
799
+ patch: {
800
+ fx: next.fx ?? null,
801
+ ...(next.reverb !== before.reverb
802
+ ? { reverb: next.reverb ?? null }
803
+ : {}),
804
+ },
805
+ },
806
+ ],
807
+ trackId,
808
+ summary: `${trackId} ${messages.join(" · ")}`,
809
+ };
810
+ },
811
+ },
638
812
  {
639
813
  name: "set_synth",
640
814
  description:
@@ -680,10 +854,328 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
680
854
  };
681
855
  },
682
856
  },
857
+ {
858
+ name: "set_string",
859
+ description:
860
+ "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.",
861
+ parameters: {
862
+ type: "object",
863
+ properties: {
864
+ trackId: trackIdSchema,
865
+ preset: { type: "string", enum: [...STRING_PRESET_NAMES] },
866
+ reset: { type: "boolean" },
867
+ off: { type: "boolean" },
868
+ params: {
869
+ type: "object",
870
+ additionalProperties: { type: ["number", "string", "null"] },
871
+ },
872
+ },
873
+ additionalProperties: false,
874
+ },
875
+ plan(args, context) {
876
+ const trackId = targetTrack(args, context);
877
+ let score = context.score;
878
+ const messages: string[] = [];
879
+ for (const command of stringToolCommands(args)) {
880
+ const result = applyStringCommand(score, trackId, command);
881
+ if (!result.ok) throw new ToolArgumentError(result.message);
882
+ if (result.next) score = result.next;
883
+ messages.push(result.message);
884
+ }
885
+ const next = score.tracks.find((t) => t.id === trackId)!;
886
+ return {
887
+ kind: "score",
888
+ operations: [
889
+ {
890
+ type: "updateTrack",
891
+ trackId,
892
+ patch: { instrument: next.instrument, string: next.string ?? null },
893
+ },
894
+ ],
895
+ trackId,
896
+ summary: `${trackId} ${messages.at(-1)}`,
897
+ };
898
+ },
899
+ },
900
+ {
901
+ name: "set_keys",
902
+ description: `Shape a modelled piano, electric keys (epiano wurli clav) or organ: preset (instrument, keys and its effects), params ${KEYS_SIMPLE.join(" ")}… (pianos: sym 0.5 adds sympathetic bloom under the sustain pedal, good for Chopin and Debussy; epiano: bark bell tone vibe; wurli: trem; clav: pickup mute; null unsets; DAWG.md lists all), or reset. Stored "piano" stays legacy; set_instrument piano writes grand. Organs (not "organ", a legacy synth): presets tonewheel combo pipe… (aliases hammond b3 farfisa church); drawbars "888800008", registers "08880", stops (plenum, flute8…), rotary; params perc percdecay percvol click scanner drive (tonewheel), voice vib vibmod drive (combo), chiff wind trem (pipe). A row the family does not read is refused.`,
903
+ parameters: {
904
+ type: "object",
905
+ properties: {
906
+ trackId: trackIdSchema,
907
+ preset: {
908
+ type: "string",
909
+ enum: [...Object.keys(KEYS_PRESETS), ...Object.keys(ORGAN_ALIASES)],
910
+ },
911
+ reset: { type: "boolean" },
912
+ params: {
913
+ type: "object",
914
+ additionalProperties: { type: ["number", "string", "null"] },
915
+ },
916
+ // f061-organ
917
+ drawbars: { type: "string", pattern: "^[0-8]{9}$" },
918
+ registers: { type: "string", pattern: "^[0-8]{5}$" },
919
+ stops: { type: ["array", "string"], items: { type: "string" } },
920
+ rotary: { type: "string", enum: ["slow", "fast", "stop"] },
921
+ },
922
+ additionalProperties: false,
923
+ },
924
+ plan(args, context) {
925
+ const trackId = targetTrack(args, context);
926
+ const command = keysToolCommand(args);
927
+ const result = applyKeysCommand(context.score, trackId, command);
928
+ if (!result.ok || !result.next)
929
+ throw new ToolArgumentError(result.message);
930
+ const before = context.score.tracks.find((t) => t.id === trackId)!;
931
+ const next = result.next.tracks.find((t) => t.id === trackId)!;
932
+ return {
933
+ kind: "score",
934
+ operations: [
935
+ {
936
+ type: "updateTrack",
937
+ trackId,
938
+ patch: {
939
+ instrument: next.instrument,
940
+ keys: next.keys ?? null,
941
+ ...(next.filter !== before.filter
942
+ ? { filter: next.filter ?? null }
943
+ : {}),
944
+ ...(next.fx !== before.fx ? { fx: next.fx ?? null } : {}),
945
+ ...(next.reverb !== before.reverb
946
+ ? { reverb: next.reverb ?? null }
947
+ : {}),
948
+ },
949
+ },
950
+ ],
951
+ trackId,
952
+ summary: `${trackId} ${result.message}`,
953
+ };
954
+ },
955
+ },
956
+ {
957
+ name: "set_modal",
958
+ description:
959
+ "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.",
960
+ parameters: {
961
+ type: "object",
962
+ properties: {
963
+ trackId: trackIdSchema,
964
+ preset: {
965
+ type: "string",
966
+ enum: [...MODAL_PRESET_NAMES, ...Object.keys(MODAL_ALIASES)],
967
+ },
968
+ mallet: { type: "string", enum: [...MODAL_MALLET_NAMES] },
969
+ pair: { type: ["string", "null"] },
970
+ reset: { type: "boolean" },
971
+ params: {
972
+ type: "object",
973
+ additionalProperties: { type: ["number", "string", "null"] },
974
+ },
975
+ },
976
+ additionalProperties: false,
977
+ },
978
+ plan(args, context) {
979
+ const trackId = targetTrack(args, context);
980
+ const commands: ModalCommand[] = [];
981
+ if (args.reset === true) commands.push({ type: "modal-reset" });
982
+ if (args.preset !== undefined) {
983
+ const preset =
984
+ typeof args.preset === "string"
985
+ ? modalPresetFor(args.preset)
986
+ : undefined;
987
+ if (!preset)
988
+ throw new ToolArgumentError(
989
+ `preset must be one of ${MODAL_PRESET_NAMES.join(", ")}`,
990
+ );
991
+ commands.push({ type: "modal-preset", preset });
992
+ }
993
+ const values: Record<string, number | string | null> = {};
994
+ if (args.mallet !== undefined) {
995
+ if (typeof args.mallet !== "string")
996
+ throw new ToolArgumentError("mallet must be a string");
997
+ values.mallet = args.mallet;
998
+ }
999
+ if (args.pair !== undefined) {
1000
+ if (args.pair !== null && typeof args.pair !== "string")
1001
+ throw new ToolArgumentError("pair must be a track id or null");
1002
+ values.pair = args.pair;
1003
+ }
1004
+ if (args.params !== undefined) {
1005
+ if (
1006
+ typeof args.params !== "object" ||
1007
+ args.params === null ||
1008
+ Array.isArray(args.params)
1009
+ )
1010
+ throw new ToolArgumentError("params must be an object");
1011
+ for (const [key, value] of Object.entries(args.params)) {
1012
+ const name = modalParamName(key);
1013
+ if (!name)
1014
+ throw new ToolArgumentError(
1015
+ `modal has no parameter ${key} (${Object.keys(MODAL_PARAMS).join(" ")})`,
1016
+ );
1017
+ if (
1018
+ value !== null &&
1019
+ typeof value !== "number" &&
1020
+ typeof value !== "string"
1021
+ )
1022
+ throw new ToolArgumentError(`${key} must be a number or string`);
1023
+ values[name] = value;
1024
+ }
1025
+ }
1026
+ if (Object.keys(values).length > 0)
1027
+ commands.push({ type: "modal-set", values });
1028
+ if (commands.length === 0) {
1029
+ // An empty call turns the track modal and keeps a modal track's preset.
1030
+ const current = context.score.tracks.find((t) => t.id === trackId);
1031
+ commands.push({
1032
+ type: "modal-preset",
1033
+ preset:
1034
+ current?.instrument === "modal" && current.modal?.preset
1035
+ ? current.modal.preset
1036
+ : "marimba",
1037
+ });
1038
+ }
1039
+ let score = context.score;
1040
+ const messages: string[] = [];
1041
+ for (const command of commands) {
1042
+ const result = applyModalCommand(score, trackId, command);
1043
+ if (!result.ok || !result.next)
1044
+ throw new ToolArgumentError(result.message);
1045
+ score = result.next;
1046
+ messages.push(result.message);
1047
+ }
1048
+ const next = score.tracks.find((t) => t.id === trackId)!;
1049
+ // A pair also sets the partner (pengumbang) to ombak 0.
1050
+ const partners = score.tracks.filter(
1051
+ (t) =>
1052
+ t.id !== trackId &&
1053
+ t.modal !== context.score.tracks.find((o) => o.id === t.id)?.modal,
1054
+ );
1055
+ return {
1056
+ kind: "score",
1057
+ operations: [
1058
+ ...partners.map((t) => ({
1059
+ type: "updateTrack" as const,
1060
+ trackId: t.id,
1061
+ patch: { modal: t.modal ?? null },
1062
+ })),
1063
+ {
1064
+ type: "updateTrack",
1065
+ trackId,
1066
+ patch: { instrument: next.instrument, modal: next.modal ?? null },
1067
+ },
1068
+ ],
1069
+ trackId,
1070
+ summary: `${trackId} ${messages.at(-1)}`,
1071
+ };
1072
+ },
1073
+ },
1074
+ {
1075
+ name: "set_wind",
1076
+ description:
1077
+ "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.",
1078
+ parameters: {
1079
+ type: "object",
1080
+ properties: {
1081
+ trackId: trackIdSchema,
1082
+ preset: {
1083
+ type: "string",
1084
+ enum: [...WIND_PRESET_NAMES, ...Object.keys(WIND_ALIASES)],
1085
+ },
1086
+ reset: { type: "boolean" },
1087
+ off: { type: "boolean" },
1088
+ params: {
1089
+ type: "object",
1090
+ additionalProperties: {
1091
+ type: ["number", "string", "boolean", "null"],
1092
+ },
1093
+ },
1094
+ },
1095
+ additionalProperties: false,
1096
+ },
1097
+ plan(args, context) {
1098
+ const trackId = targetTrack(args, context);
1099
+ const commands: WindCommand[] = [];
1100
+ if (args.off === true) commands.push({ type: "wind-off" });
1101
+ if (args.reset === true) commands.push({ type: "wind-reset" });
1102
+ if (args.preset !== undefined) {
1103
+ const preset =
1104
+ typeof args.preset === "string"
1105
+ ? windPresetFor(args.preset)
1106
+ : undefined;
1107
+ if (!preset)
1108
+ throw new ToolArgumentError(
1109
+ `preset must be one of ${WIND_PRESET_NAMES.join(", ")}`,
1110
+ );
1111
+ commands.push({ type: "wind-preset", preset });
1112
+ }
1113
+ const values: Record<string, number | string | boolean | null> = {};
1114
+ if (args.params !== undefined) {
1115
+ if (
1116
+ typeof args.params !== "object" ||
1117
+ args.params === null ||
1118
+ Array.isArray(args.params)
1119
+ )
1120
+ throw new ToolArgumentError("params must be an object");
1121
+ for (const [key, value] of Object.entries(args.params)) {
1122
+ const name = windParamName(key);
1123
+ if (!name)
1124
+ throw new ToolArgumentError(
1125
+ `wind has no parameter ${key} (${Object.keys(WIND_PARAMS).join(" ")})`,
1126
+ );
1127
+ if (
1128
+ value !== null &&
1129
+ typeof value !== "number" &&
1130
+ typeof value !== "string" &&
1131
+ typeof value !== "boolean"
1132
+ )
1133
+ throw new ToolArgumentError(
1134
+ `${key} must be a number, string or boolean`,
1135
+ );
1136
+ values[name] = value;
1137
+ }
1138
+ }
1139
+ if (Object.keys(values).length > 0)
1140
+ commands.push({ type: "wind-set", values });
1141
+ if (commands.length === 0) {
1142
+ // An empty call turns the track into the wind engine.
1143
+ const current = context.score.tracks.find((t) => t.id === trackId);
1144
+ commands.push({
1145
+ type: "wind-preset",
1146
+ preset:
1147
+ current?.instrument === "wind" && current.wind?.preset
1148
+ ? current.wind.preset
1149
+ : "flute",
1150
+ });
1151
+ }
1152
+ let score = context.score;
1153
+ const messages: string[] = [];
1154
+ for (const command of commands) {
1155
+ const result = applyWindCommand(score, trackId, command);
1156
+ if (!result.ok) throw new ToolArgumentError(result.message);
1157
+ if (result.next) score = result.next;
1158
+ messages.push(result.message);
1159
+ }
1160
+ const next = score.tracks.find((t) => t.id === trackId)!;
1161
+ return {
1162
+ kind: "score",
1163
+ operations: [
1164
+ {
1165
+ type: "updateTrack",
1166
+ trackId,
1167
+ patch: { instrument: next.instrument, wind: next.wind ?? null },
1168
+ },
1169
+ ],
1170
+ trackId,
1171
+ summary: `${trackId} ${messages.at(-1)}`,
1172
+ };
1173
+ },
1174
+ },
683
1175
  {
684
1176
  name: "set_sample",
685
1177
  description:
686
- "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.",
1178
+ "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.",
687
1179
  parameters: {
688
1180
  type: "object",
689
1181
  properties: {
@@ -692,7 +1184,15 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
692
1184
  params: {
693
1185
  type: "object",
694
1186
  additionalProperties: {
695
- type: ["number", "string", "boolean", "null"],
1187
+ anyOf: [
1188
+ { type: ["number", "string", "boolean", "null"] },
1189
+ {
1190
+ type: "array",
1191
+ items: { type: "integer", minimum: 0, maximum: 127 },
1192
+ minItems: 2,
1193
+ maxItems: 2,
1194
+ },
1195
+ ],
696
1196
  },
697
1197
  },
698
1198
  },
@@ -727,6 +1227,55 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
727
1227
  };
728
1228
  },
729
1229
  },
1230
+ {
1231
+ name: "fit_sample",
1232
+ description:
1233
+ "Fit a sampler voice to the song's tempo map (ramps included): bpm is the sample's own tempo (20..400), len its length in beats; fitmode repitch (tape: pitch moves with speed, default) | beats (drums, speech: onset slices placed on time, unstretched) | tones (pads, vocals: phase-vocoder stretch, pitch kept). fit on > bpm > len. null unsets one. Song tempo is set_tempo, not this.",
1234
+ parameters: {
1235
+ type: "object",
1236
+ properties: {
1237
+ trackId: trackIdSchema,
1238
+ voice: { type: "string" },
1239
+ bpm: { type: ["number", "null"], minimum: 20, maximum: 400 },
1240
+ fitmode: {
1241
+ anyOf: [
1242
+ { type: "string", enum: ["repitch", "beats", "tones"] },
1243
+ { type: "null" },
1244
+ ],
1245
+ },
1246
+ len: { type: ["number", "null"], exclusiveMinimum: 0, maximum: 1024 },
1247
+ },
1248
+ required: ["voice"],
1249
+ additionalProperties: false,
1250
+ },
1251
+ plan(args, context) {
1252
+ const trackId = targetTrack(args, context);
1253
+ if (typeof args.voice !== "string")
1254
+ throw new ToolArgumentError("fit_sample needs voice");
1255
+ const values: Record<string, SampleControlValue> = {};
1256
+ for (const key of ["bpm", "len", "fitmode"] as const)
1257
+ if (args[key] !== undefined)
1258
+ values[key] = args[key] as SampleControlValue;
1259
+ if (Object.keys(values).length === 0)
1260
+ throw new ToolArgumentError("fit_sample needs bpm, len or fitmode");
1261
+ const result = setSampleControls(
1262
+ context.score,
1263
+ trackId,
1264
+ args.voice,
1265
+ values,
1266
+ );
1267
+ if (!result.ok) throw new ToolArgumentError(result.message);
1268
+ const next = result.next.tracks.find((t) => t.id === trackId)!;
1269
+ return {
1270
+ kind: "score",
1271
+ operations: [
1272
+ { type: "updateTrack", trackId, patch: { sampler: next.sampler } },
1273
+ ],
1274
+ trackId,
1275
+ summary: `${trackId} ${result.message.replace(/^sample/, "fit")}`,
1276
+ };
1277
+ },
1278
+ },
730
1279
  {
731
1280
  name: "set_effects",
732
1281
  description: `Shorthand: low-pass filter, delay (beats) and reverb (mix 0.15..0.35 is a room); null removes one. Prefer set_fx.`,
@@ -1082,7 +1631,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1082
1631
  properties: {
1083
1632
  id: { type: "string", pattern: ID_PATTERN.source, maxLength: 64 },
1084
1633
  name: { type: "string", maxLength: SCORE_LIMITS.maxNameLength },
1085
- instrument: { type: "string", enum: [...AVAILABLE_INSTRUMENTS] },
1634
+ instrument: { type: "string", enum: INSTRUMENT_ENUM },
1086
1635
  },
1087
1636
  required: ["id", "instrument"],
1088
1637
  additionalProperties: false,
@@ -1095,14 +1644,46 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1095
1644
  const id = args.id;
1096
1645
  if (context.score.tracks.some((track) => track.id === id))
1097
1646
  throw new ToolArgumentError(`track ${id} already exists`);
1098
- const instrument = instrumentName(args.instrument);
1099
1647
  const name =
1100
1648
  typeof args.name === "string" && args.name.trim().length > 0
1101
1649
  ? args.name.trim().slice(0, SCORE_LIMITS.maxNameLength)
1102
1650
  : id;
1651
+ const word =
1652
+ typeof args.instrument === "string"
1653
+ ? args.instrument.toLowerCase()
1654
+ : "";
1655
+ const preset = pianoWrite(word)?.preset ?? word;
1656
+ if (isKeysPreset(preset)) {
1657
+ const patch = keysPresetPatch({}, preset);
1658
+ return {
1659
+ kind: "score",
1660
+ operations: [
1661
+ {
1662
+ type: "addTrack",
1663
+ track: { id, name, ...patch, instrument: patch.instrument! },
1664
+ },
1665
+ ],
1666
+ trackId: id,
1667
+ summary: `+track ${id} (${patch.instrument} ${preset})`,
1668
+ };
1669
+ }
1670
+ const patch = instrumentName(args.instrument);
1671
+ const instrument =
1672
+ patch.string?.preset ?? patch.modal?.preset ?? patch.instrument;
1103
1673
  return {
1104
1674
  kind: "score",
1105
- operations: [{ type: "addTrack", track: { id, name, instrument } }],
1675
+ operations: [
1676
+ {
1677
+ type: "addTrack",
1678
+ track: {
1679
+ id,
1680
+ name,
1681
+ ...patch,
1682
+ // A guitar alias also starts with its rig.
1683
+ ...rigTrackFields(String(args.instrument)),
1684
+ },
1685
+ },
1686
+ ],
1106
1687
  trackId: id,
1107
1688
  summary: `+track ${id} (${instrument})`,
1108
1689
  };
@@ -1387,6 +1968,8 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1387
1968
  ...MEDIA_TOOLS,
1388
1969
  ...PACK_TOOLS,
1389
1970
  ...MASTER_TOOLS,
1971
+ ...GRANULAR_TOOLS,
1972
+ ...RESAMPLE_TOOLS,
1390
1973
  // Looks tools up at call time, so it can plan any of the above.
1391
1974
  previewSoundTool((name) => findAgentTool(name)),
1392
1975
  ] satisfies AgentTool[]);
@@ -1534,6 +2117,72 @@ function fxToolCommand(
1534
2117
  return { type: "fx-set", effect, values };
1535
2118
  }
1536
2119
 
2120
+ /** f061-organ: set_keys drawbars/registers/stops/rotary as keys values. */
2121
+ function organToolValues(
2122
+ args: Record<string, unknown>,
2123
+ ): Record<string, string> {
2124
+ const out: Record<string, string> = {};
2125
+ for (const name of ["drawbars", "registers", "rotary"] as const) {
2126
+ const value = args[name];
2127
+ if (value === undefined) continue;
2128
+ if (typeof value !== "string")
2129
+ throw new ToolArgumentError(`keys ${name} must be a string`);
2130
+ out[name] = value;
2131
+ }
2132
+ if (args.stops !== undefined) {
2133
+ const words = Array.isArray(args.stops)
2134
+ ? args.stops
2135
+ : typeof args.stops === "string"
2136
+ ? args.stops.split(/[\s,+]+/)
2137
+ : undefined;
2138
+ if (!words || !words.every((word) => typeof word === "string"))
2139
+ throw new ToolArgumentError("keys stops must be stop names");
2140
+ out.stops = words.join(" ");
2141
+ }
2142
+ return out;
2143
+ }
2144
+
2145
+ function keysToolCommand(args: Record<string, unknown>): KeysCommand {
2146
+ if (args.reset === true) return { type: "keys-reset" };
2147
+ const organ = organToolValues(args);
2148
+ const organSet = Object.keys(organ).length > 0;
2149
+ if (args.preset !== undefined) {
2150
+ const preset =
2151
+ typeof args.preset === "string"
2152
+ ? (pianoWrite(args.preset)?.preset ?? args.preset)
2153
+ : undefined;
2154
+ if (preset === undefined || !isKeysPreset(preset))
2155
+ throw new ToolArgumentError(
2156
+ `keys presets: ${Object.keys(KEYS_PRESETS).join(", ")}`,
2157
+ );
2158
+ return organSet
2159
+ ? { type: "keys-preset", preset, values: organ }
2160
+ : { type: "keys-preset", preset };
2161
+ }
2162
+ if (args.params === undefined && organSet)
2163
+ return { type: "keys-set", values: organ };
2164
+ if (args.params === undefined)
2165
+ throw new ToolArgumentError("set_keys needs preset, reset, or params");
2166
+ const params = record(args.params, "params");
2167
+ const values: Record<string, number | string | null> = { ...organ };
2168
+ for (const [name, value] of Object.entries(params)) {
2169
+ const param = keysParamName(name);
2170
+ if (!param)
2171
+ throw new ToolArgumentError(
2172
+ `keys has no parameter ${name} (DAWG.md lists them under Keys)`,
2173
+ );
2174
+ if (
2175
+ value !== null &&
2176
+ typeof value !== "number" &&
2177
+ typeof value !== "string"
2178
+ )
2179
+ throw new ToolArgumentError(`keys ${param} takes a number or a word`);
2180
+ values[param] = value;
2181
+ }
2182
+ return { type: "keys-set", values };
2183
+ }
2184
+
2185
+ /** Instrument words the agent may pick: the voice bank plus the pianos. */
1537
2186
  function synthToolCommand(args: Record<string, unknown>): SynthCommand {
1538
2187
  if (args.reset === true) return { type: "synth-reset" };
1539
2188
  if (args.zzfx !== undefined) {
@@ -1578,6 +2227,46 @@ function synthToolCommand(args: Record<string, unknown>): SynthCommand {
1578
2227
  return { type: "synth-set", values };
1579
2228
  }
1580
2229
 
2230
+ function stringToolCommands(args: Record<string, unknown>): StringCommand[] {
2231
+ if (args.off === true) return [{ type: "string-off" }];
2232
+ const commands: StringCommand[] = [];
2233
+ if (args.preset !== undefined) {
2234
+ const preset =
2235
+ typeof args.preset === "string"
2236
+ ? stringPresetName(args.preset)
2237
+ : undefined;
2238
+ if (!preset)
2239
+ throw new ToolArgumentError(
2240
+ `string presets: ${STRING_PRESET_NAMES.join(", ")}`,
2241
+ );
2242
+ commands.push({ type: "string-preset", preset });
2243
+ }
2244
+ if (args.reset === true) commands.push({ type: "string-reset" });
2245
+ if (args.params !== undefined) {
2246
+ const values: Record<string, number | string | null> = {};
2247
+ for (const [name, value] of Object.entries(record(args.params, "params"))) {
2248
+ const param = stringParamName(name);
2249
+ if (!param)
2250
+ throw new ToolArgumentError(
2251
+ `string has no parameter ${name}; params: ${Object.keys(STRING_PARAMS).join(", ")}`,
2252
+ );
2253
+ if (
2254
+ value !== null &&
2255
+ typeof value !== "number" &&
2256
+ typeof value !== "string"
2257
+ )
2258
+ throw new ToolArgumentError(`string ${name} must be a value`);
2259
+ values[param] = value;
2260
+ }
2261
+ commands.push({ type: "string-set", values });
2262
+ }
2263
+ if (commands.length === 0)
2264
+ throw new ToolArgumentError(
2265
+ "set_string needs preset, params, reset or off",
2266
+ );
2267
+ return commands;
2268
+ }
2269
+
1581
2270
  function targetTrack(args: Record<string, unknown>, context: ToolContext) {
1582
2271
  const trackId = args.trackId ?? context.focusedTrackId;
1583
2272
  if (typeof trackId !== "string" || trackId.length > SCORE_LIMITS.maxIdLength)
@@ -1597,15 +2286,35 @@ function knownNoteId(value: unknown, context: ToolContext, label: string) {
1597
2286
  return value;
1598
2287
  }
1599
2288
 
1600
- function instrumentName(value: unknown): string {
2289
+ /**
2290
+ * The track patch an instrument word means: legacy words store themselves,
2291
+ * `string` and the string resolver words (nylon, koto, harp…) also write
2292
+ * the `Track.string` preset the engine needs.
2293
+ */
2294
+ function instrumentName(value: unknown): {
2295
+ instrument: string;
2296
+ string?: { preset: string };
2297
+ modal?: TrackModal;
2298
+ wind?: TrackWind;
2299
+ } {
2300
+ const word = typeof value === "string" ? value.trim() : undefined;
2301
+ if (word === STRING_INSTRUMENT)
2302
+ return {
2303
+ instrument: STRING_INSTRUMENT,
2304
+ string: { preset: DEFAULT_STRING_PRESET },
2305
+ };
2306
+ const patch = word === undefined ? undefined : instrumentPatch(word);
1601
2307
  if (
1602
- typeof value !== "string" ||
1603
- !(AVAILABLE_INSTRUMENTS as readonly string[]).includes(value)
2308
+ patch === undefined ||
2309
+ (!patch.string &&
2310
+ !patch.modal &&
2311
+ !patch.wind &&
2312
+ !(AVAILABLE_INSTRUMENTS as readonly string[]).includes(patch.instrument))
1604
2313
  )
1605
2314
  throw new ToolArgumentError(
1606
- `instrument must be one of ${AVAILABLE_INSTRUMENTS.join(", ")}`,
2315
+ `instrument must be one of ${INSTRUMENT_ENUM.join(", ")}`,
1607
2316
  );
1608
- return value;
2317
+ return patch;
1609
2318
  }
1610
2319
 
1611
2320
  function pitch(value: unknown, label: string): number {