@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,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tools for sample packs: `list_packs`, `search_sounds`, `use_sound`.
|
|
3
|
+
* `use_sound` resolves and fetches the sound first (a `prepare` plan), then
|
|
4
|
+
* commits ordinary score operations so it is one undo step like any edit.
|
|
5
|
+
*/
|
|
6
|
+
import { SCORE_LIMITS, type TrackScore } from "../../core/score.ts";
|
|
7
|
+
import {
|
|
8
|
+
PackError,
|
|
9
|
+
PackStore,
|
|
10
|
+
banksOf,
|
|
11
|
+
type Manifest,
|
|
12
|
+
} from "../audio/packs.ts";
|
|
13
|
+
|
|
14
|
+
/** `RolandTR909 (TR909)`: banks with their Strudel nickname, if any. */
|
|
15
|
+
async function kitsWithNicknames(
|
|
16
|
+
packs: PackStore,
|
|
17
|
+
pack: string,
|
|
18
|
+
manifest: Manifest,
|
|
19
|
+
): Promise<string[]> {
|
|
20
|
+
const aliases = await packs.bankAliases(pack);
|
|
21
|
+
return banksOf(manifest).map((bank) => {
|
|
22
|
+
const nickname = aliases.nicknames.get(bank);
|
|
23
|
+
return nickname ? `${bank} (${nickname})` : bank;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
import { useSound } from "../commands/pack.ts";
|
|
27
|
+
import { pickWavetable, wavetableOperation } from "../commands/wavetable.ts";
|
|
28
|
+
import {
|
|
29
|
+
WARP_MODES,
|
|
30
|
+
WAVETABLE_PARAMS,
|
|
31
|
+
wavetableOf,
|
|
32
|
+
type ScoreOperation,
|
|
33
|
+
type TrackWavetable,
|
|
34
|
+
type WavetableParam,
|
|
35
|
+
} from "../../core/score.ts";
|
|
36
|
+
import type { AgentTool } from "./tools.ts";
|
|
37
|
+
|
|
38
|
+
/** Thrown for arguments the pack tools refuse. */
|
|
39
|
+
export class PackToolError extends Error {
|
|
40
|
+
constructor(message: string) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "PackToolError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
47
|
+
|
|
48
|
+
function store(context: { packs?: PackStore }): PackStore {
|
|
49
|
+
return context.packs ?? defaultStore();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let shared: PackStore | undefined;
|
|
53
|
+
function defaultStore(): PackStore {
|
|
54
|
+
shared ??= new PackStore();
|
|
55
|
+
return shared;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function bounded(value: unknown): string {
|
|
59
|
+
const text = JSON.stringify(value);
|
|
60
|
+
return text.length <= 24_000 ? text : `${text.slice(0, 24_000)}…`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const wavetableParamSchema = Object.fromEntries(
|
|
64
|
+
(Object.keys(WAVETABLE_PARAMS) as WavetableParam[]).map((name) => [
|
|
65
|
+
name,
|
|
66
|
+
{
|
|
67
|
+
type: "number",
|
|
68
|
+
minimum: WAVETABLE_PARAMS[name][0],
|
|
69
|
+
maximum: WAVETABLE_PARAMS[name][1],
|
|
70
|
+
},
|
|
71
|
+
]),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
async function wavetableOps(
|
|
75
|
+
packs: PackStore,
|
|
76
|
+
score: TrackScore,
|
|
77
|
+
trackId: string,
|
|
78
|
+
table: string | undefined,
|
|
79
|
+
fields: Partial<Record<WavetableParam, number>> & { warpmode?: string },
|
|
80
|
+
projectRoot?: string,
|
|
81
|
+
): Promise<{ operations: ScoreOperation[]; summary: string }> {
|
|
82
|
+
const track = score.tracks.find((item) => item.id === trackId);
|
|
83
|
+
let base: TrackWavetable = track
|
|
84
|
+
? wavetableOf(track)
|
|
85
|
+
: { table: { src: "builtin:basic" } };
|
|
86
|
+
let summary = "wavetable";
|
|
87
|
+
if (table) {
|
|
88
|
+
// pickWavetable pins the table; take its settings, keep the score as is.
|
|
89
|
+
const picked = await pickWavetable(
|
|
90
|
+
packs,
|
|
91
|
+
score,
|
|
92
|
+
trackId,
|
|
93
|
+
table,
|
|
94
|
+
projectRoot,
|
|
95
|
+
);
|
|
96
|
+
summary = picked.summary;
|
|
97
|
+
const op = picked.operation;
|
|
98
|
+
const patched =
|
|
99
|
+
op.type === "addTrack"
|
|
100
|
+
? op.track.wavetable
|
|
101
|
+
: op.type === "updateTrack"
|
|
102
|
+
? op.patch.wavetable
|
|
103
|
+
: undefined;
|
|
104
|
+
if (patched) base = patched;
|
|
105
|
+
}
|
|
106
|
+
const changed = Object.entries(fields);
|
|
107
|
+
if (changed.length)
|
|
108
|
+
summary += ` · ${changed.map(([key, value]) => `${key} ${String(value)}`).join(" · ")}`;
|
|
109
|
+
return {
|
|
110
|
+
operations: [
|
|
111
|
+
wavetableOperation(score, trackId, {
|
|
112
|
+
...base,
|
|
113
|
+
...fields,
|
|
114
|
+
} as TrackWavetable),
|
|
115
|
+
],
|
|
116
|
+
summary,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const PACK_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
121
|
+
{
|
|
122
|
+
name: "list_packs",
|
|
123
|
+
description:
|
|
124
|
+
"List the sample packs dawg can use (Strudel-format manifests: drum machines, Dirt-Samples, VCSL, General MIDI soundfonts, …) with license and, for one pack, its kits and sounds.",
|
|
125
|
+
parameters: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
pack: {
|
|
129
|
+
type: "string",
|
|
130
|
+
maxLength: 64,
|
|
131
|
+
description: "A pack name to list its kits and sounds.",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
additionalProperties: false,
|
|
135
|
+
},
|
|
136
|
+
plan(args) {
|
|
137
|
+
const name = typeof args.pack === "string" ? args.pack.trim() : "";
|
|
138
|
+
return {
|
|
139
|
+
kind: "action",
|
|
140
|
+
summary: name ? `pack ${name}` : "list packs",
|
|
141
|
+
run: async (context) => {
|
|
142
|
+
const packs = store(context);
|
|
143
|
+
if (!name) {
|
|
144
|
+
const list = await packs.list();
|
|
145
|
+
return {
|
|
146
|
+
content: bounded({
|
|
147
|
+
ok: true,
|
|
148
|
+
packs: list.map((pack) => ({
|
|
149
|
+
name: pack.name,
|
|
150
|
+
title: pack.title,
|
|
151
|
+
license: pack.license,
|
|
152
|
+
})),
|
|
153
|
+
hint: "search_sounds finds sounds; use_sound puts one on a track",
|
|
154
|
+
}),
|
|
155
|
+
summary: `${list.length} packs`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const info = await packs.info(name);
|
|
159
|
+
if (!info)
|
|
160
|
+
throw new PackToolError(`no pack named ${name.slice(0, 40)}`);
|
|
161
|
+
const manifest = await packs.manifest(info.name);
|
|
162
|
+
const sounds = [...manifest.sounds]
|
|
163
|
+
.slice(0, 300)
|
|
164
|
+
.map(([sound, entry]) =>
|
|
165
|
+
entry.kind === "zones"
|
|
166
|
+
? `${sound} (keyed, ${entry.zones.length} notes)`
|
|
167
|
+
: `${sound} (${entry.files.length})`,
|
|
168
|
+
);
|
|
169
|
+
return {
|
|
170
|
+
content: bounded({
|
|
171
|
+
ok: true,
|
|
172
|
+
pack: info.name,
|
|
173
|
+
license: info.license,
|
|
174
|
+
...(info.attribution ? { attribution: info.attribution } : {}),
|
|
175
|
+
kits: await kitsWithNicknames(packs, info.name, manifest),
|
|
176
|
+
sounds,
|
|
177
|
+
total: manifest.sounds.size,
|
|
178
|
+
}),
|
|
179
|
+
summary: `${info.name} · ${manifest.sounds.size} sounds`,
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "search_sounds",
|
|
187
|
+
description:
|
|
188
|
+
"Search pack sounds by name (e.g. 'bd', '909 hh', 'piano', 'gm guitar'). Returns pack:<pack>/<sound> refs for use_sound; keyed sounds are pitched instruments.",
|
|
189
|
+
parameters: {
|
|
190
|
+
type: "object",
|
|
191
|
+
properties: {
|
|
192
|
+
query: { type: "string", minLength: 1, maxLength: 80 },
|
|
193
|
+
pack: { type: "string", maxLength: 64 },
|
|
194
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
195
|
+
},
|
|
196
|
+
required: ["query"],
|
|
197
|
+
additionalProperties: false,
|
|
198
|
+
},
|
|
199
|
+
plan(args) {
|
|
200
|
+
if (typeof args.query !== "string" || !args.query.trim())
|
|
201
|
+
throw new PackToolError("query must be a non-empty string");
|
|
202
|
+
const query = args.query.trim().slice(0, 80);
|
|
203
|
+
const pack = typeof args.pack === "string" ? args.pack.trim() : undefined;
|
|
204
|
+
const limit =
|
|
205
|
+
typeof args.limit === "number" && Number.isInteger(args.limit)
|
|
206
|
+
? Math.max(1, Math.min(100, args.limit))
|
|
207
|
+
: 30;
|
|
208
|
+
return {
|
|
209
|
+
kind: "action",
|
|
210
|
+
summary: `search sounds ${query}`,
|
|
211
|
+
run: async (context) => {
|
|
212
|
+
const matches = await store(context).search(query, {
|
|
213
|
+
...(pack ? { pack } : {}),
|
|
214
|
+
limit,
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
content: bounded({ ok: true, matches }),
|
|
218
|
+
summary: `${matches.length} sounds for ${query}`,
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: "use_sound",
|
|
226
|
+
description:
|
|
227
|
+
'Put a pack sound on a track through the sampler. sound is <pack>/<sound>[:<n>] (n picks a file, like Strudel s("bd:3")), a kit/bank name or Strudel bank nickname (909, 808, linn, TR909, tr808, sp12, dmx, RolandTR909, tidal-drum-machines/RolandTR707) to load a whole drum kit, or a keyed instrument such as gm/gm_acoustic_grand_piano or piano/piano. The file is fetched and pinned by sha256 so renders stay reproducible. Drum hits on the track keep playing the matching kit voice.',
|
|
228
|
+
parameters: {
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: {
|
|
231
|
+
sound: { type: "string", minLength: 1, maxLength: 200 },
|
|
232
|
+
trackId: {
|
|
233
|
+
type: "string",
|
|
234
|
+
maxLength: SCORE_LIMITS.maxIdLength,
|
|
235
|
+
description:
|
|
236
|
+
"Target track id (created when missing). Defaults to the focused track.",
|
|
237
|
+
},
|
|
238
|
+
voice: {
|
|
239
|
+
type: "string",
|
|
240
|
+
maxLength: 32,
|
|
241
|
+
description: "Oneshot voice name (default from the sound, e.g. bd).",
|
|
242
|
+
},
|
|
243
|
+
mode: { type: "string", enum: ["auto", "keyed", "oneshot"] },
|
|
244
|
+
},
|
|
245
|
+
required: ["sound"],
|
|
246
|
+
additionalProperties: false,
|
|
247
|
+
},
|
|
248
|
+
plan(args, context) {
|
|
249
|
+
if (typeof args.sound !== "string" || !args.sound.trim())
|
|
250
|
+
throw new PackToolError("sound must be <pack>/<sound> or a kit name");
|
|
251
|
+
const sound = args.sound.trim();
|
|
252
|
+
const trackId =
|
|
253
|
+
typeof args.trackId === "string" && args.trackId
|
|
254
|
+
? args.trackId
|
|
255
|
+
: context.focusedTrackId;
|
|
256
|
+
if (!ID_PATTERN.test(trackId))
|
|
257
|
+
throw new PackToolError(
|
|
258
|
+
"trackId must be 1-64 letters, digits, dot, dash, or underscore",
|
|
259
|
+
);
|
|
260
|
+
const voice = typeof args.voice === "string" ? args.voice : undefined;
|
|
261
|
+
const mode =
|
|
262
|
+
args.mode === "keyed" || args.mode === "oneshot" ? args.mode : "auto";
|
|
263
|
+
return {
|
|
264
|
+
kind: "prepare",
|
|
265
|
+
summary: `use ${sound}`,
|
|
266
|
+
run: async (action) => {
|
|
267
|
+
try {
|
|
268
|
+
const result = await useSound(
|
|
269
|
+
store(action),
|
|
270
|
+
context.score,
|
|
271
|
+
trackId,
|
|
272
|
+
sound,
|
|
273
|
+
{
|
|
274
|
+
...(voice ? { voice } : {}),
|
|
275
|
+
mode,
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
return {
|
|
279
|
+
kind: "score",
|
|
280
|
+
operations: result.operations,
|
|
281
|
+
summary: result.summary,
|
|
282
|
+
trackId: result.trackId,
|
|
283
|
+
};
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (error instanceof PackError)
|
|
286
|
+
throw new PackToolError(error.message);
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "set_wavetable",
|
|
295
|
+
description:
|
|
296
|
+
"Make a track a wavetable synth and shape it (Strudel names). table: basic (sine>tri>saw>square), pwm, formant, harmonics (offline), wt_digital:0-4, wt_vgame:0-10 (Strudel uzu-wavetables), pack:<pack>/<sound>[:n], or a project table from make_wavetable (tracks/<slug>/wavetables/<name>.wav); omit to keep. wt: position 0..1. wtenv/wtattack/wtdecay/wtsustain/wtrelease: position envelope (amount -1..1, seconds). wtrate Hz/wtdepth: position LFO. warp+warpmode bend the phase. wtphaserand: start phase spread. Automate position with set_automation wt.",
|
|
297
|
+
parameters: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
trackId: {
|
|
301
|
+
type: "string",
|
|
302
|
+
maxLength: SCORE_LIMITS.maxIdLength,
|
|
303
|
+
description:
|
|
304
|
+
"Target track (created when missing). Defaults to the focused track.",
|
|
305
|
+
},
|
|
306
|
+
table: { type: "string", minLength: 1, maxLength: 200 },
|
|
307
|
+
...wavetableParamSchema,
|
|
308
|
+
warpmode: { type: "string", enum: [...WARP_MODES] },
|
|
309
|
+
},
|
|
310
|
+
additionalProperties: false,
|
|
311
|
+
},
|
|
312
|
+
plan(args, context) {
|
|
313
|
+
const trackId =
|
|
314
|
+
typeof args.trackId === "string" && args.trackId
|
|
315
|
+
? args.trackId
|
|
316
|
+
: context.focusedTrackId;
|
|
317
|
+
if (!ID_PATTERN.test(trackId))
|
|
318
|
+
throw new PackToolError(
|
|
319
|
+
"trackId must be 1-64 letters, digits, dot, dash, or underscore",
|
|
320
|
+
);
|
|
321
|
+
const table =
|
|
322
|
+
typeof args.table === "string" && args.table.trim()
|
|
323
|
+
? args.table.trim()
|
|
324
|
+
: undefined;
|
|
325
|
+
const fields: Partial<Record<WavetableParam, number>> & {
|
|
326
|
+
warpmode?: string;
|
|
327
|
+
} = {};
|
|
328
|
+
for (const name of Object.keys(WAVETABLE_PARAMS) as WavetableParam[]) {
|
|
329
|
+
const value = args[name];
|
|
330
|
+
if (value === undefined) continue;
|
|
331
|
+
const [min, max] = WAVETABLE_PARAMS[name];
|
|
332
|
+
if (
|
|
333
|
+
typeof value !== "number" ||
|
|
334
|
+
!Number.isFinite(value) ||
|
|
335
|
+
value < min ||
|
|
336
|
+
value > max
|
|
337
|
+
)
|
|
338
|
+
throw new PackToolError(`${name} must be ${min}..${max}`);
|
|
339
|
+
fields[name] = value;
|
|
340
|
+
}
|
|
341
|
+
if (args.warpmode !== undefined) {
|
|
342
|
+
if (!WARP_MODES.includes(args.warpmode as never))
|
|
343
|
+
throw new PackToolError(`warpmode must be ${WARP_MODES.join(", ")}`);
|
|
344
|
+
fields.warpmode = args.warpmode as string;
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
kind: "prepare",
|
|
348
|
+
summary: table ? `wavetable ${table}` : "shape wavetable",
|
|
349
|
+
run: async (action) => {
|
|
350
|
+
try {
|
|
351
|
+
const result = await wavetableOps(
|
|
352
|
+
store(action),
|
|
353
|
+
context.score,
|
|
354
|
+
trackId,
|
|
355
|
+
table,
|
|
356
|
+
fields,
|
|
357
|
+
action.workspace?.root,
|
|
358
|
+
);
|
|
359
|
+
return { kind: "score", ...result, trackId };
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error instanceof PackError)
|
|
362
|
+
throw new PackToolError(error.message);
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
]);
|
package/src/agent/planner.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { normalizeSynth } from "../../core/synth.ts";
|
|
2
|
+
import { normalizeFx } from "../../core/fx.ts";
|
|
1
3
|
import {
|
|
2
4
|
SCORE_LIMITS,
|
|
3
|
-
|
|
5
|
+
automationRange,
|
|
4
6
|
isAutomationParameter,
|
|
5
7
|
normalizeDelay,
|
|
6
8
|
normalizeFilter,
|
|
7
9
|
normalizeReverb,
|
|
10
|
+
normalizeSampler,
|
|
11
|
+
normalizeRhythm,
|
|
8
12
|
scoreFromJSON,
|
|
9
13
|
type ScoreOperation,
|
|
10
14
|
} from "../../core/score.ts";
|
|
11
15
|
import { DRUM_VOICES } from "../../core/drums.ts";
|
|
16
|
+
import { synthKit } from "../../core/kits.ts";
|
|
12
17
|
|
|
13
18
|
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
14
19
|
const MAX_OPERATIONS = 32;
|
|
@@ -91,6 +96,41 @@ function parseOperation(value: unknown): ScoreOperation {
|
|
|
91
96
|
) {
|
|
92
97
|
return { type: "clearTrack", trackId: value.trackId };
|
|
93
98
|
}
|
|
99
|
+
if (
|
|
100
|
+
value.type === "removeTrack" &&
|
|
101
|
+
typeof value.trackId === "string" &&
|
|
102
|
+
value.trackId.length <= 64
|
|
103
|
+
) {
|
|
104
|
+
return { type: "removeTrack", trackId: value.trackId };
|
|
105
|
+
}
|
|
106
|
+
if (
|
|
107
|
+
value.type === "moveTrack" &&
|
|
108
|
+
typeof value.trackId === "string" &&
|
|
109
|
+
value.trackId.length <= 64 &&
|
|
110
|
+
typeof value.index === "number" &&
|
|
111
|
+
Number.isInteger(value.index) &&
|
|
112
|
+
value.index >= 0 &&
|
|
113
|
+
value.index < SCORE_LIMITS.maxTracks
|
|
114
|
+
) {
|
|
115
|
+
return { type: "moveTrack", trackId: value.trackId, index: value.index };
|
|
116
|
+
}
|
|
117
|
+
if (
|
|
118
|
+
value.type === "setKey" &&
|
|
119
|
+
(value.key === null ||
|
|
120
|
+
(typeof value.key === "string" &&
|
|
121
|
+
value.key.length <= SCORE_LIMITS.maxNameLength))
|
|
122
|
+
) {
|
|
123
|
+
return { type: "setKey", key: value.key };
|
|
124
|
+
}
|
|
125
|
+
if (
|
|
126
|
+
value.type === "setMeter" &&
|
|
127
|
+
typeof value.beatsPerBar === "number" &&
|
|
128
|
+
Number.isInteger(value.beatsPerBar) &&
|
|
129
|
+
value.beatsPerBar >= 1 &&
|
|
130
|
+
value.beatsPerBar <= SCORE_LIMITS.maxBeatsPerBar
|
|
131
|
+
) {
|
|
132
|
+
return { type: "setMeter", beatsPerBar: value.beatsPerBar };
|
|
133
|
+
}
|
|
94
134
|
if (
|
|
95
135
|
value.type === "updateTrack" &&
|
|
96
136
|
typeof value.trackId === "string" &&
|
|
@@ -115,6 +155,19 @@ function parseOperation(value: unknown): ScoreOperation {
|
|
|
115
155
|
safe.delay = normalizeDelay(patch.delay) ?? null;
|
|
116
156
|
if (patch.reverb !== undefined)
|
|
117
157
|
safe.reverb = normalizeReverb(patch.reverb) ?? null;
|
|
158
|
+
if (patch.sampler !== undefined)
|
|
159
|
+
safe.sampler = normalizeSampler(patch.sampler) ?? null;
|
|
160
|
+
if (patch.rhythm !== undefined)
|
|
161
|
+
safe.rhythm = normalizeRhythm(patch.rhythm, value.trackId) ?? null;
|
|
162
|
+
// Only synth kit names pass; anything else leaves the kit unchanged.
|
|
163
|
+
if (patch.kit === null) safe.kit = null;
|
|
164
|
+
else if (typeof patch.kit === "string") {
|
|
165
|
+
const kit = synthKit(patch.kit);
|
|
166
|
+
if (kit) safe.kit = kit.name;
|
|
167
|
+
}
|
|
168
|
+
if (patch.fx !== undefined) safe.fx = normalizeFx(patch.fx) ?? null;
|
|
169
|
+
if (patch.synth !== undefined)
|
|
170
|
+
safe.synth = normalizeSynth(patch.synth) ?? null;
|
|
118
171
|
return { type: "updateTrack", trackId: value.trackId, patch: safe };
|
|
119
172
|
}
|
|
120
173
|
if (
|
|
@@ -125,7 +178,7 @@ function parseOperation(value: unknown): ScoreOperation {
|
|
|
125
178
|
Array.isArray(value.points) &&
|
|
126
179
|
value.points.length <= SCORE_LIMITS.maxAutomationPoints
|
|
127
180
|
) {
|
|
128
|
-
const { min: minValue, max: maxValue } =
|
|
181
|
+
const { min: minValue, max: maxValue } = automationRange(value.parameter);
|
|
129
182
|
const points = value.points.map((candidate) => {
|
|
130
183
|
if (!isRecord(candidate))
|
|
131
184
|
throw new Error("agent automation point is malformed");
|