@hraness/dawg 0.2.0 → 0.4.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 +201 -0
- package/DAWG.md +762 -11
- package/README.md +79 -30
- package/core/chords.ts +1724 -0
- package/core/diff.ts +261 -0
- package/core/euclid.ts +670 -0
- package/core/fx.ts +1065 -0
- package/core/key.ts +43 -0
- package/core/kits.ts +320 -0
- package/core/params.ts +111 -0
- package/core/pitch.ts +60 -0
- package/core/rhythm.ts +287 -0
- package/core/score.ts +1054 -14
- package/core/sdk/eval-child.ts +125 -0
- package/core/sdk/eval.ts +260 -0
- package/core/sdk/print.ts +675 -0
- package/core/sdk/sync-chords.ts +54 -0
- package/core/sdk/v1.ts +4159 -0
- package/core/slug.ts +19 -0
- package/core/synth.ts +1001 -0
- package/package.json +8 -5
- package/src/agent/agent.ts +317 -16
- package/src/agent/brief.ts +50 -5
- package/src/agent/chord-tools.ts +357 -0
- package/src/agent/drum-tools.ts +135 -0
- package/src/agent/gateway.ts +215 -41
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +3 -18
- package/src/agent/pack-tools.ts +369 -0
- package/src/agent/planner.ts +55 -2
- package/src/agent/preview-tool.ts +270 -0
- package/src/agent/provider.ts +234 -106
- package/src/agent/rhythm-tools.ts +145 -0
- package/src/agent/sse.ts +31 -7
- package/src/agent/tools.ts +705 -14
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +35 -14
- package/src/agent/xcb.ts +233 -21
- package/src/audio/audition.ts +93 -0
- package/src/audio/cache.ts +160 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/effects/bus.ts +147 -0
- package/src/audio/effects/chain.ts +99 -0
- package/src/audio/effects/common.ts +251 -0
- package/src/audio/effects/convolution.ts +301 -0
- package/src/audio/effects/drive.ts +142 -0
- package/src/audio/effects/duck.ts +122 -0
- package/src/audio/effects/dynamics.ts +121 -0
- package/src/audio/effects/filter.ts +319 -0
- package/src/audio/effects/modulation.ts +174 -0
- package/src/audio/effects/space.ts +337 -0
- package/src/audio/engine.ts +430 -33
- package/src/audio/kits.ts +200 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/player.ts +19 -4
- package/src/audio/preview.ts +470 -0
- package/src/audio/random.ts +15 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +420 -0
- package/src/audio/samples.ts +1025 -0
- package/src/audio/synth/oscillators.ts +268 -0
- package/src/audio/synth/voice.ts +555 -0
- package/src/audio/synth/zzfx.ts +137 -0
- package/src/audio/wav.ts +550 -334
- package/src/audio/wavetable-maker.ts +717 -0
- package/src/audio/wavetable.ts +624 -0
- package/src/auth/cli.ts +146 -27
- package/src/auth/credentials.ts +167 -41
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +885 -128
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +25 -2
- package/src/auth/tui.ts +60 -43
- package/src/commands/drums.ts +299 -0
- package/src/commands/edit.ts +192 -0
- package/src/commands/fx.ts +360 -0
- package/src/commands/help.ts +535 -0
- package/src/commands/history.ts +32 -19
- package/src/commands/music.ts +34 -15
- package/src/commands/pack.ts +423 -0
- package/src/commands/rhythm.ts +230 -0
- package/src/commands/sample.ts +462 -0
- package/src/commands/synth.ts +229 -0
- package/src/commands/wavetable.ts +370 -0
- package/src/main.ts +2136 -170
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +186 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +397 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/media/wavetable.ts +202 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +49 -6
- package/src/session/attach.ts +3 -4
- package/src/session/daemon.ts +25 -8
- package/src/session/delta.ts +249 -0
- package/src/session/naming.ts +6 -38
- package/src/session/port.ts +26 -6
- package/src/session/rebase.ts +38 -8
- package/src/session/store.ts +116 -21
- package/src/tui/audition.ts +501 -0
- package/src/tui/euclid.ts +472 -0
- package/src/tui/menu.ts +2125 -0
- package/src/tui/play-chords.ts +538 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +984 -0
- package/src/tui/sketch.ts +108 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +684 -0
- package/tui/activity.ts +42 -3
- package/tui/app.ts +398 -33
- package/tui/drums.ts +44 -0
- package/tui/grammar.ts +281 -0
- package/tui/highway.ts +24 -2
- package/tui/layers.ts +14 -2
- package/tui/play-strip.ts +188 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tools for chords: `suggest_progression` (read-only) and
|
|
3
|
+
* `write_chords` (score). Both run core/chords.ts, so the agent gets the
|
|
4
|
+
* same key-mode harmony, voice leading and performance play mode uses.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
BASS_MODES,
|
|
9
|
+
CHORD_PATTERNS,
|
|
10
|
+
findChordPattern,
|
|
11
|
+
type BassMode,
|
|
12
|
+
generateProgression,
|
|
13
|
+
keyName,
|
|
14
|
+
parseKey,
|
|
15
|
+
PERFORM_MODES,
|
|
16
|
+
PROGRESSION_PRESETS,
|
|
17
|
+
PROGRESSION_STYLES,
|
|
18
|
+
renderProgression,
|
|
19
|
+
resolveChord,
|
|
20
|
+
SPREADS,
|
|
21
|
+
type Chord,
|
|
22
|
+
type Key,
|
|
23
|
+
type PerformMode,
|
|
24
|
+
type Spread,
|
|
25
|
+
} from "../../core/chords.ts";
|
|
26
|
+
import { midiToPitch } from "../../core/pitch.ts";
|
|
27
|
+
import { SCORE_LIMITS, type ScoreOperation } from "../../core/score.ts";
|
|
28
|
+
import type { AgentTool, ToolContext } from "./tools.ts";
|
|
29
|
+
|
|
30
|
+
/** Most notes one write_chords call may add (arpeggios multiply fast). */
|
|
31
|
+
export const MAX_CHORD_NOTES = 512;
|
|
32
|
+
const MAX_CHORDS = 32;
|
|
33
|
+
|
|
34
|
+
class ChordToolError extends Error {
|
|
35
|
+
constructor(message: string) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "ToolArgumentError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const STYLE_NAMES = [
|
|
42
|
+
...PROGRESSION_STYLES,
|
|
43
|
+
...PROGRESSION_PRESETS.map((preset) => preset.name),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const str = { type: "string" } as const;
|
|
47
|
+
const int = { type: "integer" } as const;
|
|
48
|
+
const num = { type: "number" } as const;
|
|
49
|
+
const bool = { type: "boolean" } as const;
|
|
50
|
+
const keySchema = {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: '"C major", "a dorian"; default song key',
|
|
53
|
+
} as const;
|
|
54
|
+
const chordsSchema = {
|
|
55
|
+
type: "array",
|
|
56
|
+
items: str,
|
|
57
|
+
description: '"ii7", "bVII", "V/V" or "Cm7", "F/A"',
|
|
58
|
+
} as const;
|
|
59
|
+
const voicingSchema = {
|
|
60
|
+
inversion: { ...int, description: "-12..12" },
|
|
61
|
+
spread: { type: "string", enum: [...SPREADS] },
|
|
62
|
+
} as const;
|
|
63
|
+
const progressionSchema = {
|
|
64
|
+
key: keySchema,
|
|
65
|
+
chords: {
|
|
66
|
+
...chordsSchema,
|
|
67
|
+
description: `${chordsSchema.description}; omit to generate`,
|
|
68
|
+
},
|
|
69
|
+
length: int,
|
|
70
|
+
style: str,
|
|
71
|
+
seed: int,
|
|
72
|
+
sevenths: bool,
|
|
73
|
+
...voicingSchema,
|
|
74
|
+
} as const;
|
|
75
|
+
|
|
76
|
+
type Progression = Readonly<{ key: Key; chords: Chord[] }>;
|
|
77
|
+
|
|
78
|
+
function progressionOf(
|
|
79
|
+
args: Record<string, unknown>,
|
|
80
|
+
context: ToolContext,
|
|
81
|
+
): Progression {
|
|
82
|
+
const keyText =
|
|
83
|
+
args.key === undefined ? (context.score.key ?? "C major") : args.key;
|
|
84
|
+
const key = parseKey(typeof keyText === "string" ? keyText : undefined);
|
|
85
|
+
if (!key)
|
|
86
|
+
throw new ChordToolError(
|
|
87
|
+
`key must look like "C major", "a minor" or "F# dorian"`,
|
|
88
|
+
);
|
|
89
|
+
if (args.chords !== undefined) {
|
|
90
|
+
if (
|
|
91
|
+
!Array.isArray(args.chords) ||
|
|
92
|
+
args.chords.length === 0 ||
|
|
93
|
+
args.chords.length > MAX_CHORDS
|
|
94
|
+
)
|
|
95
|
+
throw new ChordToolError(`chords must list 1..${MAX_CHORDS} chords`);
|
|
96
|
+
const chords = args.chords.map((value, index) => {
|
|
97
|
+
const chord =
|
|
98
|
+
typeof value === "string" ? resolveChord(key, value) : undefined;
|
|
99
|
+
if (!chord)
|
|
100
|
+
throw new ChordToolError(
|
|
101
|
+
`chords[${index}]: not a roman numeral or chord symbol`,
|
|
102
|
+
);
|
|
103
|
+
return chord;
|
|
104
|
+
});
|
|
105
|
+
return { key, chords };
|
|
106
|
+
}
|
|
107
|
+
const length = integer(args, "length", 1, MAX_CHORDS) ?? 4;
|
|
108
|
+
const style = args.style;
|
|
109
|
+
if (
|
|
110
|
+
style !== undefined &&
|
|
111
|
+
(typeof style !== "string" || !STYLE_NAMES.includes(style))
|
|
112
|
+
)
|
|
113
|
+
throw new ChordToolError(`style must be one of ${STYLE_NAMES.join(", ")}`);
|
|
114
|
+
const sevenths = optionalBoolean(args, "sevenths");
|
|
115
|
+
return {
|
|
116
|
+
key,
|
|
117
|
+
chords: generateProgression({
|
|
118
|
+
key,
|
|
119
|
+
length,
|
|
120
|
+
...(style !== undefined ? { style } : {}),
|
|
121
|
+
seed: integer(args, "seed", 0, 2 ** 31) ?? 1,
|
|
122
|
+
...(sevenths !== undefined ? { sevenths } : {}),
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function voicingOf(args: Record<string, unknown>): {
|
|
128
|
+
inversion?: number;
|
|
129
|
+
spread?: Spread;
|
|
130
|
+
} {
|
|
131
|
+
const inversion = integer(args, "inversion", -12, 12);
|
|
132
|
+
const spread = args.spread;
|
|
133
|
+
if (spread !== undefined && !(SPREADS as readonly unknown[]).includes(spread))
|
|
134
|
+
throw new ChordToolError(`spread must be one of ${SPREADS.join(", ")}`);
|
|
135
|
+
return {
|
|
136
|
+
...(inversion !== undefined ? { inversion } : {}),
|
|
137
|
+
...(spread !== undefined ? { spread: spread as Spread } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const CHORD_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
142
|
+
{
|
|
143
|
+
name: "suggest_progression",
|
|
144
|
+
description: `Read-only: voice-led diatonic chords (name, numeral, voicing, bass). style ${PROGRESSION_STYLES.join("|")} walks a seeded harmony graph; or a preset ${PROGRESSION_PRESETS.map((p) => p.name).join("|")}. length ≤${MAX_CHORDS}.`,
|
|
145
|
+
parameters: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: progressionSchema,
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
},
|
|
150
|
+
plan(args, context) {
|
|
151
|
+
const { key, chords } = progressionOf(args, context);
|
|
152
|
+
const rendered = renderProgression({
|
|
153
|
+
key,
|
|
154
|
+
chords,
|
|
155
|
+
...voicingOf(args),
|
|
156
|
+
bass: true,
|
|
157
|
+
});
|
|
158
|
+
const result = {
|
|
159
|
+
key: keyName(key),
|
|
160
|
+
chords: rendered.voiced.map((chord) => ({
|
|
161
|
+
name: chord.name,
|
|
162
|
+
roman: chord.roman,
|
|
163
|
+
voicing: chord.pitches.map((pitch) => midiToPitch(pitch)),
|
|
164
|
+
...(chord.bass !== undefined
|
|
165
|
+
? { bass: midiToPitch(chord.bass) }
|
|
166
|
+
: {}),
|
|
167
|
+
})),
|
|
168
|
+
};
|
|
169
|
+
const names = rendered.voiced.map((chord) => chord.name).join(" ");
|
|
170
|
+
return {
|
|
171
|
+
kind: "action",
|
|
172
|
+
summary: `${keyName(key)}: ${names}`.slice(0, 160),
|
|
173
|
+
run: async () => ({
|
|
174
|
+
content: JSON.stringify(result),
|
|
175
|
+
summary: `${keyName(key)}: ${names}`.slice(0, 160),
|
|
176
|
+
}),
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "write_chords",
|
|
182
|
+
description: `Write voice-led chords to a track (one bar each by default), with optional bass here or on bassTrackId. perform pattern uses pattern ${CHORD_PATTERNS.map((p) => p.name).join("|")}. bassMode ${BASS_MODES.join("|")} (solo: bass only). ≤${MAX_CHORD_NOTES} notes.`,
|
|
183
|
+
parameters: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: {
|
|
186
|
+
trackId: str,
|
|
187
|
+
key: keySchema,
|
|
188
|
+
chords: chordsSchema,
|
|
189
|
+
...voicingSchema,
|
|
190
|
+
start: num,
|
|
191
|
+
beatsPerChord: num,
|
|
192
|
+
perform: { type: "string", enum: [...PERFORM_MODES] },
|
|
193
|
+
rate: { ...num, description: "arp step, beats" },
|
|
194
|
+
octaves: int,
|
|
195
|
+
strum: num,
|
|
196
|
+
velocity: num,
|
|
197
|
+
pattern: { type: "string", enum: CHORD_PATTERNS.map((p) => p.name) },
|
|
198
|
+
bass: bool,
|
|
199
|
+
bassMode: { type: "string", enum: [...BASS_MODES] },
|
|
200
|
+
bassTrackId: str,
|
|
201
|
+
},
|
|
202
|
+
required: ["chords"],
|
|
203
|
+
additionalProperties: false,
|
|
204
|
+
},
|
|
205
|
+
plan(args, context) {
|
|
206
|
+
const trackId = trackOf(args.trackId, context, "trackId");
|
|
207
|
+
if (args.chords === undefined)
|
|
208
|
+
throw new ChordToolError(
|
|
209
|
+
"write_chords needs chords (try suggest_progression)",
|
|
210
|
+
);
|
|
211
|
+
const bassTrackId =
|
|
212
|
+
args.bassTrackId === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: trackOf(args.bassTrackId, context, "bassTrackId");
|
|
215
|
+
const { key, chords } = progressionOf(args, context);
|
|
216
|
+
const mode = args.perform;
|
|
217
|
+
if (
|
|
218
|
+
mode !== undefined &&
|
|
219
|
+
!(PERFORM_MODES as readonly unknown[]).includes(mode)
|
|
220
|
+
)
|
|
221
|
+
throw new ChordToolError(
|
|
222
|
+
`perform must be one of ${PERFORM_MODES.join(", ")}`,
|
|
223
|
+
);
|
|
224
|
+
const velocity = finite(args, "velocity", 0, 1);
|
|
225
|
+
const rate = finite(args, "rate", 0.0625, 4);
|
|
226
|
+
const strum = finite(args, "strum", 0, 1);
|
|
227
|
+
const octaves = integer(args, "octaves", 1, 4);
|
|
228
|
+
const pattern = args.pattern;
|
|
229
|
+
if (
|
|
230
|
+
pattern !== undefined &&
|
|
231
|
+
(typeof pattern !== "string" || !findChordPattern(pattern))
|
|
232
|
+
)
|
|
233
|
+
throw new ChordToolError(
|
|
234
|
+
`pattern must be one of ${CHORD_PATTERNS.map((p) => p.name).join(", ")}`,
|
|
235
|
+
);
|
|
236
|
+
const bassMode = args.bassMode;
|
|
237
|
+
if (
|
|
238
|
+
bassMode !== undefined &&
|
|
239
|
+
!(BASS_MODES as readonly unknown[]).includes(bassMode)
|
|
240
|
+
)
|
|
241
|
+
throw new ChordToolError(
|
|
242
|
+
`bassMode must be one of ${BASS_MODES.join(", ")}`,
|
|
243
|
+
);
|
|
244
|
+
const rendered = renderProgression({
|
|
245
|
+
key,
|
|
246
|
+
chords,
|
|
247
|
+
start: finite(args, "start", 0, Infinity) ?? 0,
|
|
248
|
+
beatsPerChord:
|
|
249
|
+
finite(args, "beatsPerChord", 1e-3, 64) ?? context.score.beatsPerBar,
|
|
250
|
+
...voicingOf(args),
|
|
251
|
+
bass: args.bass === true || bassTrackId !== undefined,
|
|
252
|
+
...(bassMode !== undefined ? { bassMode: bassMode as BassMode } : {}),
|
|
253
|
+
perform: {
|
|
254
|
+
mode: (mode as PerformMode | undefined) ?? "block",
|
|
255
|
+
...(rate !== undefined ? { rate } : {}),
|
|
256
|
+
...(strum !== undefined ? { strum } : {}),
|
|
257
|
+
...(octaves !== undefined ? { octaves } : {}),
|
|
258
|
+
...(velocity !== undefined ? { velocity } : {}),
|
|
259
|
+
seed: integer(args, "seed", 0, 2 ** 31) ?? 0,
|
|
260
|
+
...(typeof pattern === "string" ? { pattern } : {}),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
const total = rendered.notes.length + rendered.bass.length;
|
|
264
|
+
if (total > MAX_CHORD_NOTES)
|
|
265
|
+
throw new ChordToolError(
|
|
266
|
+
`${total} notes exceed ${MAX_CHORD_NOTES}; use fewer chords, a slower rate or fewer octaves`,
|
|
267
|
+
);
|
|
268
|
+
const tpb = context.score.ticksPerBeat;
|
|
269
|
+
const operations: ScoreOperation[] = [];
|
|
270
|
+
const add = (
|
|
271
|
+
notes: readonly {
|
|
272
|
+
pitch: number;
|
|
273
|
+
start: number;
|
|
274
|
+
length: number;
|
|
275
|
+
velocity: number;
|
|
276
|
+
}[],
|
|
277
|
+
target: string,
|
|
278
|
+
) => {
|
|
279
|
+
for (const note of notes)
|
|
280
|
+
operations.push({
|
|
281
|
+
type: "addNote",
|
|
282
|
+
note: {
|
|
283
|
+
id: context.newNoteId(target, operations.length),
|
|
284
|
+
trackId: target,
|
|
285
|
+
startTick: Math.round(note.start * tpb),
|
|
286
|
+
durationTicks: Math.max(1, Math.round(note.length * tpb)),
|
|
287
|
+
pitch: note.pitch,
|
|
288
|
+
velocity: note.velocity,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
add(rendered.notes, trackId);
|
|
293
|
+
add(rendered.bass, bassTrackId ?? trackId);
|
|
294
|
+
const names = rendered.voiced.map((chord) => chord.name).join(" ");
|
|
295
|
+
return {
|
|
296
|
+
kind: "score",
|
|
297
|
+
operations,
|
|
298
|
+
trackId,
|
|
299
|
+
summary: `${trackId} chords ${names}`.slice(0, 160),
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
function trackOf(value: unknown, context: ToolContext, label: string): string {
|
|
306
|
+
const trackId = value ?? context.focusedTrackId;
|
|
307
|
+
if (typeof trackId !== "string" || trackId.length > SCORE_LIMITS.maxIdLength)
|
|
308
|
+
throw new ChordToolError(`${label} must be a short string`);
|
|
309
|
+
if (!context.score.tracks.some((track) => track.id === trackId))
|
|
310
|
+
throw new ChordToolError(
|
|
311
|
+
`unknown track ${trackId}; create it with create_track first`,
|
|
312
|
+
);
|
|
313
|
+
return trackId;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function finite(
|
|
317
|
+
args: Record<string, unknown>,
|
|
318
|
+
name: string,
|
|
319
|
+
min: number,
|
|
320
|
+
max: number,
|
|
321
|
+
): number | undefined {
|
|
322
|
+
const value = args[name];
|
|
323
|
+
if (value === undefined) return undefined;
|
|
324
|
+
if (
|
|
325
|
+
typeof value !== "number" ||
|
|
326
|
+
!Number.isFinite(value) ||
|
|
327
|
+
value < min ||
|
|
328
|
+
value > max
|
|
329
|
+
)
|
|
330
|
+
throw new ChordToolError(
|
|
331
|
+
`${name} must be a number in ${min}..${max === Infinity ? "∞" : max}`,
|
|
332
|
+
);
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function integer(
|
|
337
|
+
args: Record<string, unknown>,
|
|
338
|
+
name: string,
|
|
339
|
+
min: number,
|
|
340
|
+
max: number,
|
|
341
|
+
): number | undefined {
|
|
342
|
+
const value = finite(args, name, min, max);
|
|
343
|
+
if (value !== undefined && !Number.isInteger(value))
|
|
344
|
+
throw new ChordToolError(`${name} must be an integer`);
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function optionalBoolean(
|
|
349
|
+
args: Record<string, unknown>,
|
|
350
|
+
name: string,
|
|
351
|
+
): boolean | undefined {
|
|
352
|
+
const value = args[name];
|
|
353
|
+
if (value === undefined) return undefined;
|
|
354
|
+
if (typeof value !== "boolean")
|
|
355
|
+
throw new ChordToolError(`${name} must be true or false`);
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tools for the drum pattern library and the synthesized kits:
|
|
3
|
+
* `list_drum_patterns`, `apply_drum_pattern`, `set_drum_kit`. Patterns land
|
|
4
|
+
* as rhythm rows (`set_rhythm` edits them afterwards), so a groove stays a
|
|
5
|
+
* few parameters per voice instead of dozens of notes.
|
|
6
|
+
*/
|
|
7
|
+
import { diffScores } from "../../core/diff.ts";
|
|
8
|
+
import { SYNTH_KITS } from "../../core/kits.ts";
|
|
9
|
+
import { SCORE_LIMITS } from "../../core/score.ts";
|
|
10
|
+
import {
|
|
11
|
+
applyDrumPattern,
|
|
12
|
+
applySynthKit,
|
|
13
|
+
DRUM_PATTERNS,
|
|
14
|
+
patternLine,
|
|
15
|
+
} from "../commands/drums.ts";
|
|
16
|
+
import type { AgentTool, ToolContext } from "./tools.ts";
|
|
17
|
+
import { ToolArgumentError } from "./tools.ts";
|
|
18
|
+
|
|
19
|
+
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
20
|
+
|
|
21
|
+
function trackIdOf(args: Record<string, unknown>, context: ToolContext) {
|
|
22
|
+
const trackId = args.trackId ?? context.focusedTrackId;
|
|
23
|
+
if (typeof trackId !== "string" || !ID_PATTERN.test(trackId))
|
|
24
|
+
throw new ToolArgumentError(
|
|
25
|
+
"trackId must be 1-64 letters, digits, dot, dash, or underscore",
|
|
26
|
+
);
|
|
27
|
+
return trackId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const trackId = {
|
|
31
|
+
type: "string",
|
|
32
|
+
maxLength: SCORE_LIMITS.maxIdLength,
|
|
33
|
+
description:
|
|
34
|
+
"Drum track id (created when missing). Defaults to the focused track.",
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export const DRUM_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
38
|
+
{
|
|
39
|
+
name: "list_drum_patterns",
|
|
40
|
+
description:
|
|
41
|
+
"List the built-in drum patterns (name · tempo range · genre tags · voices). Filter by a word such as house, trap or 2-step.",
|
|
42
|
+
parameters: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: { filter: { type: "string", maxLength: 40 } },
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
plan(args) {
|
|
48
|
+
const needle =
|
|
49
|
+
typeof args.filter === "string" ? args.filter.trim().toLowerCase() : "";
|
|
50
|
+
const lines = DRUM_PATTERNS.filter(
|
|
51
|
+
(entry) =>
|
|
52
|
+
!needle ||
|
|
53
|
+
entry.name.includes(needle) ||
|
|
54
|
+
entry.tags.some((tag) => tag.includes(needle)),
|
|
55
|
+
).map(patternLine);
|
|
56
|
+
return {
|
|
57
|
+
kind: "action",
|
|
58
|
+
summary: needle ? `patterns ${needle}` : "list patterns",
|
|
59
|
+
run: async () => ({
|
|
60
|
+
content: JSON.stringify({
|
|
61
|
+
ok: true,
|
|
62
|
+
patterns: lines,
|
|
63
|
+
hint: "apply_drum_pattern puts one on a drum track; set_rhythm then edits its rows",
|
|
64
|
+
}),
|
|
65
|
+
summary: `${lines.length} patterns`,
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "apply_drum_pattern",
|
|
72
|
+
description:
|
|
73
|
+
"Replace a drum track's notes with a library pattern as Euclidean/grid rhythm rows (one per voice). Moves the tempo into the pattern's range unless tempo is keep.",
|
|
74
|
+
parameters: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
name: { type: "string", maxLength: 40 },
|
|
78
|
+
trackId,
|
|
79
|
+
tempo: { type: "string", enum: ["auto", "keep", "set"] },
|
|
80
|
+
},
|
|
81
|
+
required: ["name"],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
plan(args, context) {
|
|
85
|
+
if (typeof args.name !== "string" || !args.name.trim())
|
|
86
|
+
throw new ToolArgumentError("name must be a pattern name");
|
|
87
|
+
const id = trackIdOf(args, context);
|
|
88
|
+
const tempo =
|
|
89
|
+
args.tempo === "keep" || args.tempo === "set" ? args.tempo : "auto";
|
|
90
|
+
const result = applyDrumPattern(
|
|
91
|
+
context.score,
|
|
92
|
+
id,
|
|
93
|
+
args.name.trim().toLowerCase(),
|
|
94
|
+
tempo,
|
|
95
|
+
);
|
|
96
|
+
if (!result.ok || !result.next)
|
|
97
|
+
throw new ToolArgumentError(result.message);
|
|
98
|
+
return {
|
|
99
|
+
kind: "score",
|
|
100
|
+
operations: diffScores(context.score, result.next),
|
|
101
|
+
trackId: id,
|
|
102
|
+
summary: result.message,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "set_drum_kit",
|
|
108
|
+
description: `Choose the synthesized drum kit of a drum track: ${SYNTH_KITS.map((kit) => kit.name).join(", ")}, or default. Offline and instant; sample kits (909, 808, linn, …) go through use_sound.`,
|
|
109
|
+
parameters: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
kit: {
|
|
113
|
+
type: "string",
|
|
114
|
+
enum: ["default", ...SYNTH_KITS.map((kit) => kit.name)],
|
|
115
|
+
},
|
|
116
|
+
trackId,
|
|
117
|
+
},
|
|
118
|
+
required: ["kit"],
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
121
|
+
plan(args, context) {
|
|
122
|
+
if (typeof args.kit !== "string")
|
|
123
|
+
throw new ToolArgumentError("kit must be a synth kit name");
|
|
124
|
+
const id = trackIdOf(args, context);
|
|
125
|
+
const result = applySynthKit(context.score, id, args.kit);
|
|
126
|
+
if (!result.ok) throw new ToolArgumentError(result.message);
|
|
127
|
+
return {
|
|
128
|
+
kind: "score",
|
|
129
|
+
operations: result.next ? diffScores(context.score, result.next) : [],
|
|
130
|
+
trackId: id,
|
|
131
|
+
summary: result.message,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
]);
|