@hraness/dawg 0.4.1 → 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 +99 -0
- package/DAWG.md +551 -33
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +41 -12
- package/core/expression.ts +1241 -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/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/resonators.ts +574 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +717 -62
- package/core/sdk/eval-child.ts +40 -3
- package/core/sdk/print.ts +614 -27
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2824 -45
- package/core/sections.ts +2072 -0
- package/core/strings.ts +845 -0
- package/core/synth.ts +11 -1
- package/core/tempo.ts +1318 -0
- package/core/tuning.ts +1180 -0
- package/guides/audition.md +26 -0
- package/guides/automation.md +26 -0
- package/guides/chords.md +28 -0
- package/guides/effects.md +29 -0
- package/guides/faders.md +29 -0
- package/guides/files.md +28 -0
- package/guides/getting-started.md +26 -0
- package/guides/index.ts +75 -0
- package/guides/keys.md +27 -0
- package/guides/media.md +27 -0
- package/guides/mix.md +23 -0
- package/guides/music.md +14 -0
- package/guides/notes.md +26 -0
- package/guides/performance.md +26 -0
- package/guides/play.md +24 -0
- package/guides/project.md +13 -0
- package/guides/providers.md +25 -0
- package/guides/rhythm.md +26 -0
- package/guides/sessions.md +21 -0
- package/guides/sound.md +15 -0
- package/guides/sounds.md +29 -0
- package/guides/tempo.md +27 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +9 -0
- package/src/agent/brief.ts +77 -3
- package/src/agent/chord-tools.ts +9 -1
- package/src/agent/expression-tools.ts +336 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +68 -6
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +27 -4
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +574 -16
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +9 -0
- package/src/audio/arrange.ts +489 -0
- package/src/audio/audition.ts +16 -3
- package/src/audio/click.ts +113 -1
- package/src/audio/clock.ts +71 -5
- 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/bus.ts +5 -4
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/dynamics.ts +2 -1
- package/src/audio/effects/filter.ts +5 -3
- package/src/audio/effects/modulation.ts +5 -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/effects/space.ts +92 -2
- package/src/audio/engine.ts +100 -21
- 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 +263 -17
- package/src/audio/loudness.ts +596 -0
- package/src/audio/master.ts +660 -0
- package/src/audio/measure-worker.ts +45 -0
- package/src/audio/measure.ts +110 -0
- package/src/audio/player.ts +11 -6
- package/src/audio/preview.ts +76 -14
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +269 -34
- 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/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +147 -0
- package/src/audio/wav.ts +377 -30
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +941 -0
- 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 +355 -27
- package/src/commands/keys.ts +353 -0
- package/src/commands/master.ts +361 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +967 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +919 -41
- package/src/project/check.ts +13 -0
- package/src/render.ts +139 -8
- package/src/session/daemon.ts +18 -8
- package/src/session/naming.ts +8 -1
- package/src/session/rebase.ts +21 -0
- package/src/tui/arrange-menu.ts +390 -0
- package/src/tui/audition.ts +38 -5
- package/src/tui/fader.ts +409 -0
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +967 -23
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +3 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +459 -43
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +63 -1
- package/tui/guide.ts +351 -0
- package/tui/highway.ts +87 -4
- package/tui/hits.ts +68 -0
- package/tui/input.ts +7 -0
- package/tui/keys.ts +72 -0
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,
|
|
@@ -7,6 +23,7 @@ import {
|
|
|
7
23
|
type ScoreOperation,
|
|
8
24
|
type TrackScore,
|
|
9
25
|
} from "../../core/score.ts";
|
|
26
|
+
import { loopTicksOf } from "../../core/tempo.ts";
|
|
10
27
|
import { AVAILABLE_INSTRUMENTS } from "../audio/wav.ts";
|
|
11
28
|
import {
|
|
12
29
|
SYNTH_PRESETS,
|
|
@@ -15,6 +32,24 @@ import {
|
|
|
15
32
|
synthParamName,
|
|
16
33
|
} from "../../core/synth.ts";
|
|
17
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";
|
|
18
53
|
import {
|
|
19
54
|
setSampleControls,
|
|
20
55
|
type SampleControlValue,
|
|
@@ -22,16 +57,26 @@ import {
|
|
|
22
57
|
import {
|
|
23
58
|
EFFECT_NAMES,
|
|
24
59
|
FX_PRESETS,
|
|
60
|
+
RIG_STAGES,
|
|
25
61
|
effectSpec,
|
|
26
62
|
type EffectName,
|
|
27
63
|
} from "../../core/fx.ts";
|
|
64
|
+
import {
|
|
65
|
+
RIG_PRESET_NAMES,
|
|
66
|
+
applyRigCommand,
|
|
67
|
+
parseRigCommand,
|
|
68
|
+
rigTrackFields,
|
|
69
|
+
rigWordPatch,
|
|
70
|
+
} from "../commands/rig.ts";
|
|
28
71
|
import {
|
|
29
72
|
applyFxCommand,
|
|
30
73
|
effectPatch,
|
|
31
74
|
effectValues,
|
|
32
75
|
parseEffectName,
|
|
33
76
|
parseParamName,
|
|
77
|
+
parseFxCommand,
|
|
34
78
|
type FxCommand,
|
|
79
|
+
type FxResult,
|
|
35
80
|
} from "../commands/fx.ts";
|
|
36
81
|
import type { ChatTool } from "./gateway.ts";
|
|
37
82
|
import { MEDIA_TOOLS } from "../media/tools.ts";
|
|
@@ -44,9 +89,16 @@ import {
|
|
|
44
89
|
import { PackError, type PackStore } from "../audio/packs.ts";
|
|
45
90
|
import { RHYTHM_TOOLS } from "./rhythm-tools.ts";
|
|
46
91
|
import { CHORD_TOOLS } from "./chord-tools.ts";
|
|
92
|
+
import { EXPRESSION_TOOLS } from "./expression-tools.ts";
|
|
93
|
+
import { TUNING_TOOLS } from "./tuning-tools.ts";
|
|
94
|
+
import { MASTER_TOOLS } from "./master-tools.ts";
|
|
47
95
|
import { DRUM_TOOLS } from "./drum-tools.ts";
|
|
96
|
+
import { TIME_TOOLS } from "./time-tools.ts";
|
|
97
|
+
import { SECTION_TOOLS } from "./section-tools.ts";
|
|
98
|
+
import { GRANULAR_TOOLS } from "./granular-tools.ts";
|
|
48
99
|
import type { MediaResult, MediaRunContext } from "../media/types.ts";
|
|
49
|
-
import { pitchToMidi } from "./ops.ts";
|
|
100
|
+
import { instrumentPatch, pitchToMidi } from "./ops.ts";
|
|
101
|
+
import { TUNING_LIMITS } from "../../core/tuning.ts";
|
|
50
102
|
import {
|
|
51
103
|
DRUM_VOICES,
|
|
52
104
|
drumVoicePitch,
|
|
@@ -79,6 +131,13 @@ import {
|
|
|
79
131
|
type SearchSpend,
|
|
80
132
|
} from "../web/search.ts";
|
|
81
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
|
+
|
|
82
141
|
/** What a validated tool call asks the host to do. */
|
|
83
142
|
export type ScorePlan = Readonly<{
|
|
84
143
|
kind: "score";
|
|
@@ -244,6 +303,12 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
244
303
|
description: "Length in beats",
|
|
245
304
|
},
|
|
246
305
|
velocity: { type: "number", minimum: 0, maximum: 1 },
|
|
306
|
+
cents: {
|
|
307
|
+
type: "number",
|
|
308
|
+
minimum: -TUNING_LIMITS.maxNoteCents,
|
|
309
|
+
maximum: TUNING_LIMITS.maxNoteCents,
|
|
310
|
+
description: "Detune from the tuned pitch (microtones)",
|
|
311
|
+
},
|
|
247
312
|
},
|
|
248
313
|
required: ["pitch", "start", "duration"],
|
|
249
314
|
additionalProperties: false,
|
|
@@ -262,6 +327,10 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
262
327
|
const start = number(note, "start", { min: 0 });
|
|
263
328
|
const duration = number(note, "duration", { min: 0, exclusive: true });
|
|
264
329
|
const velocity = optionalNumber(note, "velocity", { min: 0, max: 1 });
|
|
330
|
+
const cents = optionalNumber(note, "cents", {
|
|
331
|
+
min: -TUNING_LIMITS.maxNoteCents,
|
|
332
|
+
max: TUNING_LIMITS.maxNoteCents,
|
|
333
|
+
});
|
|
265
334
|
return {
|
|
266
335
|
type: "addNote",
|
|
267
336
|
note: {
|
|
@@ -271,6 +340,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
271
340
|
durationTicks: Math.max(1, Math.round(duration * tpb)),
|
|
272
341
|
pitch: pitch(note.pitch, `notes[${index}].pitch`),
|
|
273
342
|
velocity: velocity ?? 0.8,
|
|
343
|
+
...(cents ? { cents } : {}),
|
|
274
344
|
},
|
|
275
345
|
};
|
|
276
346
|
});
|
|
@@ -322,7 +392,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
322
392
|
{
|
|
323
393
|
name: "update_notes",
|
|
324
394
|
description:
|
|
325
|
-
"Move, resize, transpose,
|
|
395
|
+
"Move, resize, transpose, re-velocity or detune existing notes by id. Times are in beats.",
|
|
326
396
|
parameters: {
|
|
327
397
|
type: "object",
|
|
328
398
|
properties: {
|
|
@@ -343,6 +413,12 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
343
413
|
],
|
|
344
414
|
},
|
|
345
415
|
velocity: { type: "number", minimum: 0, maximum: 1 },
|
|
416
|
+
cents: {
|
|
417
|
+
type: "number",
|
|
418
|
+
minimum: -TUNING_LIMITS.maxNoteCents,
|
|
419
|
+
maximum: TUNING_LIMITS.maxNoteCents,
|
|
420
|
+
description: "Detune from the tuned pitch; 0 clears it",
|
|
421
|
+
},
|
|
346
422
|
},
|
|
347
423
|
required: ["noteId"],
|
|
348
424
|
additionalProperties: false,
|
|
@@ -378,6 +454,11 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
378
454
|
max: 1,
|
|
379
455
|
});
|
|
380
456
|
if (velocity !== undefined) patch.velocity = velocity;
|
|
457
|
+
const cents = optionalNumber(update, "cents", {
|
|
458
|
+
min: -TUNING_LIMITS.maxNoteCents,
|
|
459
|
+
max: TUNING_LIMITS.maxNoteCents,
|
|
460
|
+
});
|
|
461
|
+
if (cents !== undefined) patch.cents = cents;
|
|
381
462
|
if (Object.keys(patch).length === 0)
|
|
382
463
|
throw new ToolArgumentError(`updates[${index}] changes nothing`);
|
|
383
464
|
return { type: "updateNote", noteId, patch };
|
|
@@ -392,24 +473,47 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
392
473
|
},
|
|
393
474
|
{
|
|
394
475
|
name: "set_instrument",
|
|
395
|
-
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.",
|
|
396
478
|
parameters: {
|
|
397
479
|
type: "object",
|
|
398
480
|
properties: {
|
|
399
481
|
trackId: trackIdSchema,
|
|
400
|
-
instrument: { type: "string", enum:
|
|
482
|
+
instrument: { type: "string", enum: INSTRUMENT_ENUM },
|
|
401
483
|
},
|
|
402
484
|
required: ["instrument"],
|
|
403
485
|
additionalProperties: false,
|
|
404
486
|
},
|
|
405
487
|
plan(args, context) {
|
|
406
488
|
const trackId = targetTrack(args, context);
|
|
407
|
-
|
|
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);
|
|
408
507
|
return {
|
|
409
508
|
kind: "score",
|
|
410
|
-
operations: [
|
|
509
|
+
operations: [
|
|
510
|
+
{ type: "updateTrack", trackId, patch: { ...patch, ...rig } },
|
|
511
|
+
],
|
|
411
512
|
trackId,
|
|
412
|
-
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}`,
|
|
413
517
|
};
|
|
414
518
|
},
|
|
415
519
|
},
|
|
@@ -606,6 +710,92 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
606
710
|
};
|
|
607
711
|
},
|
|
608
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
|
+
},
|
|
609
799
|
{
|
|
610
800
|
name: "set_synth",
|
|
611
801
|
description:
|
|
@@ -651,6 +841,198 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
651
841
|
};
|
|
652
842
|
},
|
|
653
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
|
+
},
|
|
654
1036
|
{
|
|
655
1037
|
name: "set_sample",
|
|
656
1038
|
description:
|
|
@@ -698,6 +1080,55 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
698
1080
|
};
|
|
699
1081
|
},
|
|
700
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
|
+
},
|
|
701
1132
|
{
|
|
702
1133
|
name: "set_effects",
|
|
703
1134
|
description: `Shorthand: low-pass filter, delay (beats) and reverb (mix 0.15..0.35 is a room); null removes one. Prefer set_fx.`,
|
|
@@ -915,7 +1346,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
915
1346
|
`track ${trackId} is ${track.instrument}, not a drum kit; set_instrument kit or create a kit track`,
|
|
916
1347
|
);
|
|
917
1348
|
const tpb = context.score.ticksPerBeat;
|
|
918
|
-
const loopBeats = context.score
|
|
1349
|
+
const loopBeats = loopTicksOf(context.score) / tpb;
|
|
919
1350
|
const voiceOf = (value: unknown, label: string) => {
|
|
920
1351
|
const voice =
|
|
921
1352
|
typeof value === "string" ? parseDrumVoice(value) : undefined;
|
|
@@ -1053,7 +1484,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1053
1484
|
properties: {
|
|
1054
1485
|
id: { type: "string", pattern: ID_PATTERN.source, maxLength: 64 },
|
|
1055
1486
|
name: { type: "string", maxLength: SCORE_LIMITS.maxNameLength },
|
|
1056
|
-
instrument: { type: "string", enum:
|
|
1487
|
+
instrument: { type: "string", enum: INSTRUMENT_ENUM },
|
|
1057
1488
|
},
|
|
1058
1489
|
required: ["id", "instrument"],
|
|
1059
1490
|
additionalProperties: false,
|
|
@@ -1066,14 +1497,46 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1066
1497
|
const id = args.id;
|
|
1067
1498
|
if (context.score.tracks.some((track) => track.id === id))
|
|
1068
1499
|
throw new ToolArgumentError(`track ${id} already exists`);
|
|
1069
|
-
const instrument = instrumentName(args.instrument);
|
|
1070
1500
|
const name =
|
|
1071
1501
|
typeof args.name === "string" && args.name.trim().length > 0
|
|
1072
1502
|
? args.name.trim().slice(0, SCORE_LIMITS.maxNameLength)
|
|
1073
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;
|
|
1074
1526
|
return {
|
|
1075
1527
|
kind: "score",
|
|
1076
|
-
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
|
+
],
|
|
1077
1540
|
trackId: id,
|
|
1078
1541
|
summary: `+track ${id} (${instrument})`,
|
|
1079
1542
|
};
|
|
@@ -1350,9 +1813,15 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1350
1813
|
},
|
|
1351
1814
|
...RHYTHM_TOOLS,
|
|
1352
1815
|
...CHORD_TOOLS,
|
|
1816
|
+
...EXPRESSION_TOOLS,
|
|
1817
|
+
...TUNING_TOOLS,
|
|
1353
1818
|
...DRUM_TOOLS,
|
|
1819
|
+
...TIME_TOOLS,
|
|
1820
|
+
...SECTION_TOOLS,
|
|
1354
1821
|
...MEDIA_TOOLS,
|
|
1355
1822
|
...PACK_TOOLS,
|
|
1823
|
+
...MASTER_TOOLS,
|
|
1824
|
+
...GRANULAR_TOOLS,
|
|
1356
1825
|
// Looks tools up at call time, so it can plan any of the above.
|
|
1357
1826
|
previewSoundTool((name) => findAgentTool(name)),
|
|
1358
1827
|
] satisfies AgentTool[]);
|
|
@@ -1500,6 +1969,37 @@ function fxToolCommand(
|
|
|
1500
1969
|
return { type: "fx-set", effect, values };
|
|
1501
1970
|
}
|
|
1502
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. */
|
|
1503
2003
|
function synthToolCommand(args: Record<string, unknown>): SynthCommand {
|
|
1504
2004
|
if (args.reset === true) return { type: "synth-reset" };
|
|
1505
2005
|
if (args.zzfx !== undefined) {
|
|
@@ -1544,6 +2044,46 @@ function synthToolCommand(args: Record<string, unknown>): SynthCommand {
|
|
|
1544
2044
|
return { type: "synth-set", values };
|
|
1545
2045
|
}
|
|
1546
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
|
+
|
|
1547
2087
|
function targetTrack(args: Record<string, unknown>, context: ToolContext) {
|
|
1548
2088
|
const trackId = args.trackId ?? context.focusedTrackId;
|
|
1549
2089
|
if (typeof trackId !== "string" || trackId.length > SCORE_LIMITS.maxIdLength)
|
|
@@ -1563,15 +2103,33 @@ function knownNoteId(value: unknown, context: ToolContext, label: string) {
|
|
|
1563
2103
|
return value;
|
|
1564
2104
|
}
|
|
1565
2105
|
|
|
1566
|
-
|
|
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);
|
|
1567
2123
|
if (
|
|
1568
|
-
|
|
1569
|
-
!
|
|
2124
|
+
patch === undefined ||
|
|
2125
|
+
(!patch.string &&
|
|
2126
|
+
!patch.modal &&
|
|
2127
|
+
!(AVAILABLE_INSTRUMENTS as readonly string[]).includes(patch.instrument))
|
|
1570
2128
|
)
|
|
1571
2129
|
throw new ToolArgumentError(
|
|
1572
|
-
`instrument must be one of ${
|
|
2130
|
+
`instrument must be one of ${INSTRUMENT_ENUM.join(", ")}`,
|
|
1573
2131
|
);
|
|
1574
|
-
return
|
|
2132
|
+
return patch;
|
|
1575
2133
|
}
|
|
1576
2134
|
|
|
1577
2135
|
function pitch(value: unknown, label: string): number {
|