@hraness/dawg 0.5.0 → 0.6.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.
- package/CHANGELOG.md +29 -0
- package/DAWG.md +269 -13
- package/README.md +4 -4
- package/core/diff.ts +4 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/resonators.ts +574 -0
- package/core/score.ts +181 -6
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +169 -13
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +1114 -21
- package/core/sections.ts +34 -8
- package/core/strings.ts +845 -0
- package/guides/effects.md +3 -1
- package/guides/sounds.md +4 -0
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +7 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +39 -2
- package/src/agent/preview-tool.ts +6 -1
- package/src/agent/tools.ts +538 -14
- package/src/agent/xcb-agent.ts +7 -0
- package/src/audio/arrange.ts +21 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/engine.ts +10 -1
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +231 -12
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +2 -0
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +163 -17
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +78 -6
- package/src/commands/expression.ts +9 -2
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +74 -3
- package/src/commands/keys.ts +353 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/time.ts +4 -1
- package/src/main.ts +210 -8
- package/src/project/check.ts +13 -0
- package/src/render.ts +33 -4
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +332 -9
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/play-session.ts +45 -2
package/src/agent/tools.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
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";
|
|
1
17
|
import {
|
|
2
18
|
AUTOMATION_PARAMETERS,
|
|
3
19
|
automationPoints,
|
|
@@ -16,6 +32,24 @@ import {
|
|
|
16
32
|
synthParamName,
|
|
17
33
|
} from "../../core/synth.ts";
|
|
18
34
|
import { applySynthCommand, type SynthCommand } from "../commands/synth.ts";
|
|
35
|
+
import { applyStringCommand, type StringCommand } from "../commands/string.ts";
|
|
36
|
+
import {
|
|
37
|
+
STRING_PARAMS,
|
|
38
|
+
STRING_PRESET_NAMES,
|
|
39
|
+
stringParamName,
|
|
40
|
+
stringPresetName,
|
|
41
|
+
} from "../../core/strings.ts";
|
|
42
|
+
import {
|
|
43
|
+
applyKeysCommand,
|
|
44
|
+
keysPresetPatch,
|
|
45
|
+
type KeysCommand,
|
|
46
|
+
} from "../commands/keys.ts";
|
|
47
|
+
import {
|
|
48
|
+
KEYS_PRESETS,
|
|
49
|
+
KEYS_SIMPLE,
|
|
50
|
+
isKeysPreset,
|
|
51
|
+
keysParamName,
|
|
52
|
+
} from "../../core/keys.ts";
|
|
19
53
|
import {
|
|
20
54
|
setSampleControls,
|
|
21
55
|
type SampleControlValue,
|
|
@@ -23,16 +57,26 @@ import {
|
|
|
23
57
|
import {
|
|
24
58
|
EFFECT_NAMES,
|
|
25
59
|
FX_PRESETS,
|
|
60
|
+
RIG_STAGES,
|
|
26
61
|
effectSpec,
|
|
27
62
|
type EffectName,
|
|
28
63
|
} from "../../core/fx.ts";
|
|
64
|
+
import {
|
|
65
|
+
RIG_PRESET_NAMES,
|
|
66
|
+
applyRigCommand,
|
|
67
|
+
parseRigCommand,
|
|
68
|
+
rigTrackFields,
|
|
69
|
+
rigWordPatch,
|
|
70
|
+
} from "../commands/rig.ts";
|
|
29
71
|
import {
|
|
30
72
|
applyFxCommand,
|
|
31
73
|
effectPatch,
|
|
32
74
|
effectValues,
|
|
33
75
|
parseEffectName,
|
|
34
76
|
parseParamName,
|
|
77
|
+
parseFxCommand,
|
|
35
78
|
type FxCommand,
|
|
79
|
+
type FxResult,
|
|
36
80
|
} from "../commands/fx.ts";
|
|
37
81
|
import type { ChatTool } from "./gateway.ts";
|
|
38
82
|
import { MEDIA_TOOLS } from "../media/tools.ts";
|
|
@@ -51,8 +95,9 @@ import { MASTER_TOOLS } from "./master-tools.ts";
|
|
|
51
95
|
import { DRUM_TOOLS } from "./drum-tools.ts";
|
|
52
96
|
import { TIME_TOOLS } from "./time-tools.ts";
|
|
53
97
|
import { SECTION_TOOLS } from "./section-tools.ts";
|
|
98
|
+
import { GRANULAR_TOOLS } from "./granular-tools.ts";
|
|
54
99
|
import type { MediaResult, MediaRunContext } from "../media/types.ts";
|
|
55
|
-
import { pitchToMidi } from "./ops.ts";
|
|
100
|
+
import { instrumentPatch, pitchToMidi } from "./ops.ts";
|
|
56
101
|
import { TUNING_LIMITS } from "../../core/tuning.ts";
|
|
57
102
|
import {
|
|
58
103
|
DRUM_VOICES,
|
|
@@ -86,6 +131,13 @@ import {
|
|
|
86
131
|
type SearchSpend,
|
|
87
132
|
} from "../web/search.ts";
|
|
88
133
|
|
|
134
|
+
/** Instrument words the tools accept: legacy voices, `string`, 0.6 words. */
|
|
135
|
+
const INSTRUMENT_ENUM: string[] = [
|
|
136
|
+
...AVAILABLE_INSTRUMENTS,
|
|
137
|
+
STRING_INSTRUMENT,
|
|
138
|
+
...INSTRUMENT_WORDS.map((row) => row.word),
|
|
139
|
+
].filter((word, index, all) => all.indexOf(word) === index);
|
|
140
|
+
|
|
89
141
|
/** What a validated tool call asks the host to do. */
|
|
90
142
|
export type ScorePlan = Readonly<{
|
|
91
143
|
kind: "score";
|
|
@@ -421,24 +473,47 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
421
473
|
},
|
|
422
474
|
{
|
|
423
475
|
name: "set_instrument",
|
|
424
|
-
description:
|
|
476
|
+
description:
|
|
477
|
+
"Change a track's instrument voice. Mallets and bells (modal): modal (marimba) vibes xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani; set_modal shapes them.",
|
|
425
478
|
parameters: {
|
|
426
479
|
type: "object",
|
|
427
480
|
properties: {
|
|
428
481
|
trackId: trackIdSchema,
|
|
429
|
-
instrument: { type: "string", enum:
|
|
482
|
+
instrument: { type: "string", enum: INSTRUMENT_ENUM },
|
|
430
483
|
},
|
|
431
484
|
required: ["instrument"],
|
|
432
485
|
additionalProperties: false,
|
|
433
486
|
},
|
|
434
487
|
plan(args, context) {
|
|
435
488
|
const trackId = targetTrack(args, context);
|
|
436
|
-
|
|
489
|
+
// piano/grand/upright/felt…: a new write is the modelled piano.
|
|
490
|
+
const track = context.score.tracks.find((t) => t.id === trackId);
|
|
491
|
+
if (track && typeof args.instrument === "string") {
|
|
492
|
+
const word = args.instrument.toLowerCase();
|
|
493
|
+
const preset = word === "piano" ? "grand" : word;
|
|
494
|
+
if (isKeysPreset(preset)) {
|
|
495
|
+
const patch = keysPresetPatch(track, preset);
|
|
496
|
+
return {
|
|
497
|
+
kind: "score",
|
|
498
|
+
operations: [{ type: "updateTrack", trackId, patch }],
|
|
499
|
+
trackId,
|
|
500
|
+
summary: `${trackId} → ${patch.instrument} (${preset})`,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const patch = instrumentName(args.instrument);
|
|
505
|
+
// A guitar alias (`jangle`, `gtr-metal`…) also loads its rig.
|
|
506
|
+
const rig = rigWordPatch(String(args.instrument), track?.fx);
|
|
437
507
|
return {
|
|
438
508
|
kind: "score",
|
|
439
|
-
operations: [
|
|
509
|
+
operations: [
|
|
510
|
+
{ type: "updateTrack", trackId, patch: { ...patch, ...rig } },
|
|
511
|
+
],
|
|
440
512
|
trackId,
|
|
441
|
-
summary:
|
|
513
|
+
summary:
|
|
514
|
+
patch.instrument === "marimba" && !patch.modal
|
|
515
|
+
? `${trackId} → marimba (legacy tone) · set_modal preset marimba for the mallet engine`
|
|
516
|
+
: `${trackId} → ${patch.string?.preset ?? patch.modal?.preset ?? patch.instrument}`,
|
|
442
517
|
};
|
|
443
518
|
},
|
|
444
519
|
},
|
|
@@ -635,6 +710,92 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
635
710
|
};
|
|
636
711
|
},
|
|
637
712
|
},
|
|
713
|
+
{
|
|
714
|
+
name: "set_rig",
|
|
715
|
+
description:
|
|
716
|
+
"Guitar rig on a track (stomp → amp head with noise gate → speaker cab, before tremolo). rig loads a whole rig (clean crunch punk ragged lead metal fuzz octave funk wah bachata spring bassdrive reese jangle alt) or reset removes it; stomp/head/cab set stage params (stomp type fuzz|face|od|rat|octave gain tone level; head type clean|chime|crunch|lead|high|solid|bass gain bass mid treble presence master sag gate(dB); cab type 1x12|2x12|4x12|1x10|open|8x10|1x15|di mic), or null removes a stage. Heads are level-matched. `amp` is Strudel gain, not this.",
|
|
717
|
+
parameters: {
|
|
718
|
+
type: "object",
|
|
719
|
+
properties: {
|
|
720
|
+
trackId: trackIdSchema,
|
|
721
|
+
rig: { type: "string", enum: [...RIG_PRESET_NAMES, "reset"] },
|
|
722
|
+
...Object.fromEntries(
|
|
723
|
+
RIG_STAGES.map((stage) => [
|
|
724
|
+
stage,
|
|
725
|
+
{
|
|
726
|
+
type: ["object", "null"],
|
|
727
|
+
additionalProperties: { type: ["number", "string"] },
|
|
728
|
+
},
|
|
729
|
+
]),
|
|
730
|
+
),
|
|
731
|
+
},
|
|
732
|
+
additionalProperties: false,
|
|
733
|
+
},
|
|
734
|
+
plan(args, context) {
|
|
735
|
+
const trackId = targetTrack(args, context);
|
|
736
|
+
let score = context.score;
|
|
737
|
+
const messages: string[] = [];
|
|
738
|
+
const run = (result: FxResult) => {
|
|
739
|
+
if (!result.ok) throw new ToolArgumentError(result.message);
|
|
740
|
+
if (result.next) score = result.next;
|
|
741
|
+
messages.push(result.message);
|
|
742
|
+
};
|
|
743
|
+
if (args.rig !== undefined) {
|
|
744
|
+
if (typeof args.rig !== "string")
|
|
745
|
+
throw new ToolArgumentError("rig must be a rig name or reset");
|
|
746
|
+
const command = parseRigCommand(`rig ${args.rig}`);
|
|
747
|
+
if (!command || command.type === "rig-show")
|
|
748
|
+
throw new ToolArgumentError(
|
|
749
|
+
`rig must be one of ${RIG_PRESET_NAMES.join(", ")} or reset`,
|
|
750
|
+
);
|
|
751
|
+
run(applyRigCommand(score, trackId, command));
|
|
752
|
+
}
|
|
753
|
+
for (const stage of RIG_STAGES) {
|
|
754
|
+
const params = args[stage];
|
|
755
|
+
if (params === undefined) continue;
|
|
756
|
+
if (params === null) {
|
|
757
|
+
run(
|
|
758
|
+
applyFxCommand(score, trackId, { type: "fx-off", effect: stage }),
|
|
759
|
+
);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (typeof params !== "object" || Array.isArray(params))
|
|
763
|
+
throw new ToolArgumentError(`${stage} must be an object or null`);
|
|
764
|
+
const words = Object.entries(params as Record<string, unknown>)
|
|
765
|
+
.map(([key, value]) => `${key} ${String(value)}`)
|
|
766
|
+
.join(" ");
|
|
767
|
+
const command = words.length
|
|
768
|
+
? parseFxCommand(`fx ${stage} ${words}`)
|
|
769
|
+
: ({ type: "fx-on", effect: stage } as const);
|
|
770
|
+
if (!command)
|
|
771
|
+
throw new ToolArgumentError(
|
|
772
|
+
`${stage}: unknown parameter or value in ${words}`,
|
|
773
|
+
);
|
|
774
|
+
run(applyFxCommand(score, trackId, command));
|
|
775
|
+
}
|
|
776
|
+
if (messages.length === 0)
|
|
777
|
+
throw new ToolArgumentError("give rig, stomp, head or cab");
|
|
778
|
+
const next = score.tracks.find((t) => t.id === trackId)!;
|
|
779
|
+
const before = context.score.tracks.find((t) => t.id === trackId)!;
|
|
780
|
+
return {
|
|
781
|
+
kind: "score",
|
|
782
|
+
operations: [
|
|
783
|
+
{
|
|
784
|
+
type: "updateTrack",
|
|
785
|
+
trackId,
|
|
786
|
+
patch: {
|
|
787
|
+
fx: next.fx ?? null,
|
|
788
|
+
...(next.reverb !== before.reverb
|
|
789
|
+
? { reverb: next.reverb ?? null }
|
|
790
|
+
: {}),
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
],
|
|
794
|
+
trackId,
|
|
795
|
+
summary: `${trackId} ${messages.join(" · ")}`,
|
|
796
|
+
};
|
|
797
|
+
},
|
|
798
|
+
},
|
|
638
799
|
{
|
|
639
800
|
name: "set_synth",
|
|
640
801
|
description:
|
|
@@ -680,6 +841,198 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
680
841
|
};
|
|
681
842
|
},
|
|
682
843
|
},
|
|
844
|
+
{
|
|
845
|
+
name: "set_string",
|
|
846
|
+
description:
|
|
847
|
+
"Make a track a plucked string (physical model): preset picks the instrument; params override it (ring s, bright, damp, pos, mute, buzz = jawari, body, sym = sympathetic strings, stiff, exciter pick|finger|hammer|noise); null unsets one. reset keeps the preset and drops overrides; off returns the track to a plain pluck voice.",
|
|
848
|
+
parameters: {
|
|
849
|
+
type: "object",
|
|
850
|
+
properties: {
|
|
851
|
+
trackId: trackIdSchema,
|
|
852
|
+
preset: { type: "string", enum: [...STRING_PRESET_NAMES] },
|
|
853
|
+
reset: { type: "boolean" },
|
|
854
|
+
off: { type: "boolean" },
|
|
855
|
+
params: {
|
|
856
|
+
type: "object",
|
|
857
|
+
additionalProperties: { type: ["number", "string", "null"] },
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
additionalProperties: false,
|
|
861
|
+
},
|
|
862
|
+
plan(args, context) {
|
|
863
|
+
const trackId = targetTrack(args, context);
|
|
864
|
+
let score = context.score;
|
|
865
|
+
const messages: string[] = [];
|
|
866
|
+
for (const command of stringToolCommands(args)) {
|
|
867
|
+
const result = applyStringCommand(score, trackId, command);
|
|
868
|
+
if (!result.ok) throw new ToolArgumentError(result.message);
|
|
869
|
+
if (result.next) score = result.next;
|
|
870
|
+
messages.push(result.message);
|
|
871
|
+
}
|
|
872
|
+
const next = score.tracks.find((t) => t.id === trackId)!;
|
|
873
|
+
return {
|
|
874
|
+
kind: "score",
|
|
875
|
+
operations: [
|
|
876
|
+
{
|
|
877
|
+
type: "updateTrack",
|
|
878
|
+
trackId,
|
|
879
|
+
patch: { instrument: next.instrument, string: next.string ?? null },
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
trackId,
|
|
883
|
+
summary: `${trackId} ${messages.at(-1)}`,
|
|
884
|
+
};
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
name: "set_keys",
|
|
889
|
+
description: `Shape a modelled piano: preset (instrument, keys and its effects), params ${KEYS_SIMPLE.join(" ")}… (null unsets; DAWG.md lists all), or reset. Stored "piano" stays legacy; set_instrument piano writes grand.`,
|
|
890
|
+
parameters: {
|
|
891
|
+
type: "object",
|
|
892
|
+
properties: {
|
|
893
|
+
trackId: trackIdSchema,
|
|
894
|
+
preset: { type: "string", enum: Object.keys(KEYS_PRESETS) },
|
|
895
|
+
reset: { type: "boolean" },
|
|
896
|
+
params: {
|
|
897
|
+
type: "object",
|
|
898
|
+
additionalProperties: { type: ["number", "string", "null"] },
|
|
899
|
+
},
|
|
900
|
+
},
|
|
901
|
+
additionalProperties: false,
|
|
902
|
+
},
|
|
903
|
+
plan(args, context) {
|
|
904
|
+
const trackId = targetTrack(args, context);
|
|
905
|
+
const command = keysToolCommand(args);
|
|
906
|
+
const result = applyKeysCommand(context.score, trackId, command);
|
|
907
|
+
if (!result.ok || !result.next)
|
|
908
|
+
throw new ToolArgumentError(result.message);
|
|
909
|
+
const before = context.score.tracks.find((t) => t.id === trackId)!;
|
|
910
|
+
const next = result.next.tracks.find((t) => t.id === trackId)!;
|
|
911
|
+
return {
|
|
912
|
+
kind: "score",
|
|
913
|
+
operations: [
|
|
914
|
+
{
|
|
915
|
+
type: "updateTrack",
|
|
916
|
+
trackId,
|
|
917
|
+
patch: {
|
|
918
|
+
instrument: next.instrument,
|
|
919
|
+
keys: next.keys ?? null,
|
|
920
|
+
...(next.filter !== before.filter
|
|
921
|
+
? { filter: next.filter ?? null }
|
|
922
|
+
: {}),
|
|
923
|
+
...(next.fx !== before.fx ? { fx: next.fx ?? null } : {}),
|
|
924
|
+
...(next.reverb !== before.reverb
|
|
925
|
+
? { reverb: next.reverb ?? null }
|
|
926
|
+
: {}),
|
|
927
|
+
},
|
|
928
|
+
},
|
|
929
|
+
],
|
|
930
|
+
trackId,
|
|
931
|
+
summary: `${trackId} ${result.message}`,
|
|
932
|
+
};
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
name: "set_modal",
|
|
937
|
+
description:
|
|
938
|
+
"Mallets and bells on the modal engine: preset (marimba vibes xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani) switches the voice and keeps overrides; mallet yarn|cord|rubber|plastic|brass; params sets MODAL_PARAMS (hardness position ring tilt release damp motor motordepth ombak buzz click strikebend strikedecay gain), null returns one to the preset; reset clears overrides. Turns the track into instrument modal.",
|
|
939
|
+
parameters: {
|
|
940
|
+
type: "object",
|
|
941
|
+
properties: {
|
|
942
|
+
trackId: trackIdSchema,
|
|
943
|
+
preset: {
|
|
944
|
+
type: "string",
|
|
945
|
+
enum: [...MODAL_PRESET_NAMES, ...Object.keys(MODAL_ALIASES)],
|
|
946
|
+
},
|
|
947
|
+
mallet: { type: "string", enum: [...MODAL_MALLET_NAMES] },
|
|
948
|
+
reset: { type: "boolean" },
|
|
949
|
+
params: {
|
|
950
|
+
type: "object",
|
|
951
|
+
additionalProperties: { type: ["number", "string", "null"] },
|
|
952
|
+
},
|
|
953
|
+
},
|
|
954
|
+
additionalProperties: false,
|
|
955
|
+
},
|
|
956
|
+
plan(args, context) {
|
|
957
|
+
const trackId = targetTrack(args, context);
|
|
958
|
+
const commands: ModalCommand[] = [];
|
|
959
|
+
if (args.reset === true) commands.push({ type: "modal-reset" });
|
|
960
|
+
if (args.preset !== undefined) {
|
|
961
|
+
const preset =
|
|
962
|
+
typeof args.preset === "string"
|
|
963
|
+
? modalPresetFor(args.preset)
|
|
964
|
+
: undefined;
|
|
965
|
+
if (!preset)
|
|
966
|
+
throw new ToolArgumentError(
|
|
967
|
+
`preset must be one of ${MODAL_PRESET_NAMES.join(", ")}`,
|
|
968
|
+
);
|
|
969
|
+
commands.push({ type: "modal-preset", preset });
|
|
970
|
+
}
|
|
971
|
+
const values: Record<string, number | string | null> = {};
|
|
972
|
+
if (args.mallet !== undefined) {
|
|
973
|
+
if (typeof args.mallet !== "string")
|
|
974
|
+
throw new ToolArgumentError("mallet must be a string");
|
|
975
|
+
values.mallet = args.mallet;
|
|
976
|
+
}
|
|
977
|
+
if (args.params !== undefined) {
|
|
978
|
+
if (
|
|
979
|
+
typeof args.params !== "object" ||
|
|
980
|
+
args.params === null ||
|
|
981
|
+
Array.isArray(args.params)
|
|
982
|
+
)
|
|
983
|
+
throw new ToolArgumentError("params must be an object");
|
|
984
|
+
for (const [key, value] of Object.entries(args.params)) {
|
|
985
|
+
const name = modalParamName(key);
|
|
986
|
+
if (!name)
|
|
987
|
+
throw new ToolArgumentError(
|
|
988
|
+
`modal has no parameter ${key} (${Object.keys(MODAL_PARAMS).join(" ")})`,
|
|
989
|
+
);
|
|
990
|
+
if (
|
|
991
|
+
value !== null &&
|
|
992
|
+
typeof value !== "number" &&
|
|
993
|
+
typeof value !== "string"
|
|
994
|
+
)
|
|
995
|
+
throw new ToolArgumentError(`${key} must be a number or string`);
|
|
996
|
+
values[name] = value;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (Object.keys(values).length > 0)
|
|
1000
|
+
commands.push({ type: "modal-set", values });
|
|
1001
|
+
if (commands.length === 0) {
|
|
1002
|
+
// An empty call turns the track modal and keeps a modal track's preset.
|
|
1003
|
+
const current = context.score.tracks.find((t) => t.id === trackId);
|
|
1004
|
+
commands.push({
|
|
1005
|
+
type: "modal-preset",
|
|
1006
|
+
preset:
|
|
1007
|
+
current?.instrument === "modal" && current.modal?.preset
|
|
1008
|
+
? current.modal.preset
|
|
1009
|
+
: "marimba",
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
let score = context.score;
|
|
1013
|
+
const messages: string[] = [];
|
|
1014
|
+
for (const command of commands) {
|
|
1015
|
+
const result = applyModalCommand(score, trackId, command);
|
|
1016
|
+
if (!result.ok || !result.next)
|
|
1017
|
+
throw new ToolArgumentError(result.message);
|
|
1018
|
+
score = result.next;
|
|
1019
|
+
messages.push(result.message);
|
|
1020
|
+
}
|
|
1021
|
+
const next = score.tracks.find((t) => t.id === trackId)!;
|
|
1022
|
+
return {
|
|
1023
|
+
kind: "score",
|
|
1024
|
+
operations: [
|
|
1025
|
+
{
|
|
1026
|
+
type: "updateTrack",
|
|
1027
|
+
trackId,
|
|
1028
|
+
patch: { instrument: next.instrument, modal: next.modal ?? null },
|
|
1029
|
+
},
|
|
1030
|
+
],
|
|
1031
|
+
trackId,
|
|
1032
|
+
summary: `${trackId} ${messages.at(-1)}`,
|
|
1033
|
+
};
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
683
1036
|
{
|
|
684
1037
|
name: "set_sample",
|
|
685
1038
|
description:
|
|
@@ -727,6 +1080,55 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
727
1080
|
};
|
|
728
1081
|
},
|
|
729
1082
|
},
|
|
1083
|
+
{
|
|
1084
|
+
name: "fit_sample",
|
|
1085
|
+
description:
|
|
1086
|
+
"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.",
|
|
1087
|
+
parameters: {
|
|
1088
|
+
type: "object",
|
|
1089
|
+
properties: {
|
|
1090
|
+
trackId: trackIdSchema,
|
|
1091
|
+
voice: { type: "string" },
|
|
1092
|
+
bpm: { type: ["number", "null"], minimum: 20, maximum: 400 },
|
|
1093
|
+
fitmode: {
|
|
1094
|
+
anyOf: [
|
|
1095
|
+
{ type: "string", enum: ["repitch", "beats", "tones"] },
|
|
1096
|
+
{ type: "null" },
|
|
1097
|
+
],
|
|
1098
|
+
},
|
|
1099
|
+
len: { type: ["number", "null"], exclusiveMinimum: 0, maximum: 1024 },
|
|
1100
|
+
},
|
|
1101
|
+
required: ["voice"],
|
|
1102
|
+
additionalProperties: false,
|
|
1103
|
+
},
|
|
1104
|
+
plan(args, context) {
|
|
1105
|
+
const trackId = targetTrack(args, context);
|
|
1106
|
+
if (typeof args.voice !== "string")
|
|
1107
|
+
throw new ToolArgumentError("fit_sample needs voice");
|
|
1108
|
+
const values: Record<string, SampleControlValue> = {};
|
|
1109
|
+
for (const key of ["bpm", "len", "fitmode"] as const)
|
|
1110
|
+
if (args[key] !== undefined)
|
|
1111
|
+
values[key] = args[key] as SampleControlValue;
|
|
1112
|
+
if (Object.keys(values).length === 0)
|
|
1113
|
+
throw new ToolArgumentError("fit_sample needs bpm, len or fitmode");
|
|
1114
|
+
const result = setSampleControls(
|
|
1115
|
+
context.score,
|
|
1116
|
+
trackId,
|
|
1117
|
+
args.voice,
|
|
1118
|
+
values,
|
|
1119
|
+
);
|
|
1120
|
+
if (!result.ok) throw new ToolArgumentError(result.message);
|
|
1121
|
+
const next = result.next.tracks.find((t) => t.id === trackId)!;
|
|
1122
|
+
return {
|
|
1123
|
+
kind: "score",
|
|
1124
|
+
operations: [
|
|
1125
|
+
{ type: "updateTrack", trackId, patch: { sampler: next.sampler } },
|
|
1126
|
+
],
|
|
1127
|
+
trackId,
|
|
1128
|
+
summary: `${trackId} ${result.message.replace(/^sample/, "fit")}`,
|
|
1129
|
+
};
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
730
1132
|
{
|
|
731
1133
|
name: "set_effects",
|
|
732
1134
|
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 +1484,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1082
1484
|
properties: {
|
|
1083
1485
|
id: { type: "string", pattern: ID_PATTERN.source, maxLength: 64 },
|
|
1084
1486
|
name: { type: "string", maxLength: SCORE_LIMITS.maxNameLength },
|
|
1085
|
-
instrument: { type: "string", enum:
|
|
1487
|
+
instrument: { type: "string", enum: INSTRUMENT_ENUM },
|
|
1086
1488
|
},
|
|
1087
1489
|
required: ["id", "instrument"],
|
|
1088
1490
|
additionalProperties: false,
|
|
@@ -1095,14 +1497,46 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1095
1497
|
const id = args.id;
|
|
1096
1498
|
if (context.score.tracks.some((track) => track.id === id))
|
|
1097
1499
|
throw new ToolArgumentError(`track ${id} already exists`);
|
|
1098
|
-
const instrument = instrumentName(args.instrument);
|
|
1099
1500
|
const name =
|
|
1100
1501
|
typeof args.name === "string" && args.name.trim().length > 0
|
|
1101
1502
|
? args.name.trim().slice(0, SCORE_LIMITS.maxNameLength)
|
|
1102
1503
|
: id;
|
|
1504
|
+
const word =
|
|
1505
|
+
typeof args.instrument === "string"
|
|
1506
|
+
? args.instrument.toLowerCase()
|
|
1507
|
+
: "";
|
|
1508
|
+
const preset = word === "piano" ? "grand" : word;
|
|
1509
|
+
if (isKeysPreset(preset)) {
|
|
1510
|
+
const patch = keysPresetPatch({}, preset);
|
|
1511
|
+
return {
|
|
1512
|
+
kind: "score",
|
|
1513
|
+
operations: [
|
|
1514
|
+
{
|
|
1515
|
+
type: "addTrack",
|
|
1516
|
+
track: { id, name, ...patch, instrument: patch.instrument! },
|
|
1517
|
+
},
|
|
1518
|
+
],
|
|
1519
|
+
trackId: id,
|
|
1520
|
+
summary: `+track ${id} (${patch.instrument} ${preset})`,
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
const patch = instrumentName(args.instrument);
|
|
1524
|
+
const instrument =
|
|
1525
|
+
patch.string?.preset ?? patch.modal?.preset ?? patch.instrument;
|
|
1103
1526
|
return {
|
|
1104
1527
|
kind: "score",
|
|
1105
|
-
operations: [
|
|
1528
|
+
operations: [
|
|
1529
|
+
{
|
|
1530
|
+
type: "addTrack",
|
|
1531
|
+
track: {
|
|
1532
|
+
id,
|
|
1533
|
+
name,
|
|
1534
|
+
...patch,
|
|
1535
|
+
// A guitar alias also starts with its rig.
|
|
1536
|
+
...rigTrackFields(String(args.instrument)),
|
|
1537
|
+
},
|
|
1538
|
+
},
|
|
1539
|
+
],
|
|
1106
1540
|
trackId: id,
|
|
1107
1541
|
summary: `+track ${id} (${instrument})`,
|
|
1108
1542
|
};
|
|
@@ -1387,6 +1821,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1387
1821
|
...MEDIA_TOOLS,
|
|
1388
1822
|
...PACK_TOOLS,
|
|
1389
1823
|
...MASTER_TOOLS,
|
|
1824
|
+
...GRANULAR_TOOLS,
|
|
1390
1825
|
// Looks tools up at call time, so it can plan any of the above.
|
|
1391
1826
|
previewSoundTool((name) => findAgentTool(name)),
|
|
1392
1827
|
] satisfies AgentTool[]);
|
|
@@ -1534,6 +1969,37 @@ function fxToolCommand(
|
|
|
1534
1969
|
return { type: "fx-set", effect, values };
|
|
1535
1970
|
}
|
|
1536
1971
|
|
|
1972
|
+
function keysToolCommand(args: Record<string, unknown>): KeysCommand {
|
|
1973
|
+
if (args.reset === true) return { type: "keys-reset" };
|
|
1974
|
+
if (args.preset !== undefined) {
|
|
1975
|
+
if (typeof args.preset !== "string" || !isKeysPreset(args.preset))
|
|
1976
|
+
throw new ToolArgumentError(
|
|
1977
|
+
`keys presets: ${Object.keys(KEYS_PRESETS).join(", ")}`,
|
|
1978
|
+
);
|
|
1979
|
+
return { type: "keys-preset", preset: args.preset };
|
|
1980
|
+
}
|
|
1981
|
+
if (args.params === undefined)
|
|
1982
|
+
throw new ToolArgumentError("set_keys needs preset, reset, or params");
|
|
1983
|
+
const params = record(args.params, "params");
|
|
1984
|
+
const values: Record<string, number | string | null> = {};
|
|
1985
|
+
for (const [name, value] of Object.entries(params)) {
|
|
1986
|
+
const param = keysParamName(name);
|
|
1987
|
+
if (!param)
|
|
1988
|
+
throw new ToolArgumentError(
|
|
1989
|
+
`keys has no parameter ${name} (DAWG.md lists them under Keys)`,
|
|
1990
|
+
);
|
|
1991
|
+
if (
|
|
1992
|
+
value !== null &&
|
|
1993
|
+
typeof value !== "number" &&
|
|
1994
|
+
typeof value !== "string"
|
|
1995
|
+
)
|
|
1996
|
+
throw new ToolArgumentError(`keys ${param} takes a number or a word`);
|
|
1997
|
+
values[param] = value;
|
|
1998
|
+
}
|
|
1999
|
+
return { type: "keys-set", values };
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
/** Instrument words the agent may pick: the voice bank plus the pianos. */
|
|
1537
2003
|
function synthToolCommand(args: Record<string, unknown>): SynthCommand {
|
|
1538
2004
|
if (args.reset === true) return { type: "synth-reset" };
|
|
1539
2005
|
if (args.zzfx !== undefined) {
|
|
@@ -1578,6 +2044,46 @@ function synthToolCommand(args: Record<string, unknown>): SynthCommand {
|
|
|
1578
2044
|
return { type: "synth-set", values };
|
|
1579
2045
|
}
|
|
1580
2046
|
|
|
2047
|
+
function stringToolCommands(args: Record<string, unknown>): StringCommand[] {
|
|
2048
|
+
if (args.off === true) return [{ type: "string-off" }];
|
|
2049
|
+
const commands: StringCommand[] = [];
|
|
2050
|
+
if (args.preset !== undefined) {
|
|
2051
|
+
const preset =
|
|
2052
|
+
typeof args.preset === "string"
|
|
2053
|
+
? stringPresetName(args.preset)
|
|
2054
|
+
: undefined;
|
|
2055
|
+
if (!preset)
|
|
2056
|
+
throw new ToolArgumentError(
|
|
2057
|
+
`string presets: ${STRING_PRESET_NAMES.join(", ")}`,
|
|
2058
|
+
);
|
|
2059
|
+
commands.push({ type: "string-preset", preset });
|
|
2060
|
+
}
|
|
2061
|
+
if (args.reset === true) commands.push({ type: "string-reset" });
|
|
2062
|
+
if (args.params !== undefined) {
|
|
2063
|
+
const values: Record<string, number | string | null> = {};
|
|
2064
|
+
for (const [name, value] of Object.entries(record(args.params, "params"))) {
|
|
2065
|
+
const param = stringParamName(name);
|
|
2066
|
+
if (!param)
|
|
2067
|
+
throw new ToolArgumentError(
|
|
2068
|
+
`string has no parameter ${name}; params: ${Object.keys(STRING_PARAMS).join(", ")}`,
|
|
2069
|
+
);
|
|
2070
|
+
if (
|
|
2071
|
+
value !== null &&
|
|
2072
|
+
typeof value !== "number" &&
|
|
2073
|
+
typeof value !== "string"
|
|
2074
|
+
)
|
|
2075
|
+
throw new ToolArgumentError(`string ${name} must be a value`);
|
|
2076
|
+
values[param] = value;
|
|
2077
|
+
}
|
|
2078
|
+
commands.push({ type: "string-set", values });
|
|
2079
|
+
}
|
|
2080
|
+
if (commands.length === 0)
|
|
2081
|
+
throw new ToolArgumentError(
|
|
2082
|
+
"set_string needs preset, params, reset or off",
|
|
2083
|
+
);
|
|
2084
|
+
return commands;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1581
2087
|
function targetTrack(args: Record<string, unknown>, context: ToolContext) {
|
|
1582
2088
|
const trackId = args.trackId ?? context.focusedTrackId;
|
|
1583
2089
|
if (typeof trackId !== "string" || trackId.length > SCORE_LIMITS.maxIdLength)
|
|
@@ -1597,15 +2103,33 @@ function knownNoteId(value: unknown, context: ToolContext, label: string) {
|
|
|
1597
2103
|
return value;
|
|
1598
2104
|
}
|
|
1599
2105
|
|
|
1600
|
-
|
|
2106
|
+
/**
|
|
2107
|
+
* The track patch an instrument word means: legacy words store themselves,
|
|
2108
|
+
* `string` and the string resolver words (nylon, koto, harp…) also write
|
|
2109
|
+
* the `Track.string` preset the engine needs.
|
|
2110
|
+
*/
|
|
2111
|
+
function instrumentName(value: unknown): {
|
|
2112
|
+
instrument: string;
|
|
2113
|
+
string?: { preset: string };
|
|
2114
|
+
modal?: TrackModal;
|
|
2115
|
+
} {
|
|
2116
|
+
const word = typeof value === "string" ? value.trim() : undefined;
|
|
2117
|
+
if (word === STRING_INSTRUMENT)
|
|
2118
|
+
return {
|
|
2119
|
+
instrument: STRING_INSTRUMENT,
|
|
2120
|
+
string: { preset: DEFAULT_STRING_PRESET },
|
|
2121
|
+
};
|
|
2122
|
+
const patch = word === undefined ? undefined : instrumentPatch(word);
|
|
1601
2123
|
if (
|
|
1602
|
-
|
|
1603
|
-
!
|
|
2124
|
+
patch === undefined ||
|
|
2125
|
+
(!patch.string &&
|
|
2126
|
+
!patch.modal &&
|
|
2127
|
+
!(AVAILABLE_INSTRUMENTS as readonly string[]).includes(patch.instrument))
|
|
1604
2128
|
)
|
|
1605
2129
|
throw new ToolArgumentError(
|
|
1606
|
-
`instrument must be one of ${
|
|
2130
|
+
`instrument must be one of ${INSTRUMENT_ENUM.join(", ")}`,
|
|
1607
2131
|
);
|
|
1608
|
-
return
|
|
2132
|
+
return patch;
|
|
1609
2133
|
}
|
|
1610
2134
|
|
|
1611
2135
|
function pitch(value: unknown, label: string): number {
|