@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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tools for tunings and scales: `set_tuning` and `set_scale`. Both
|
|
3
|
+
* build the same `/tuning` and `/scale` commands the prompt and the menu
|
|
4
|
+
* run (src/commands/tuning.ts), so every surface validates alike.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import type { ScoreOperation, TrackScore } from "../../core/score.ts";
|
|
9
|
+
import { TUNING_LIMITS, TUNING_MAPS, type Tuning } from "../../core/tuning.ts";
|
|
10
|
+
import {
|
|
11
|
+
applyTuningCommand,
|
|
12
|
+
parseTuningCommand,
|
|
13
|
+
type ProjectRead,
|
|
14
|
+
type TuningResult,
|
|
15
|
+
} from "../commands/tuning.ts";
|
|
16
|
+
import { resolveReadPath, WorkspaceError } from "./workspace.ts";
|
|
17
|
+
import type { AgentTool, ScorePlan, ToolContext } from "./tools.ts";
|
|
18
|
+
|
|
19
|
+
class TuningToolError extends Error {
|
|
20
|
+
constructor(message: string) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "ToolArgumentError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MAX_TABLE = 128;
|
|
27
|
+
|
|
28
|
+
/** The `/tuning …` arguments one set_tuning call stands for. */
|
|
29
|
+
export function tuningCommandsFor(args: Record<string, unknown>): string[] {
|
|
30
|
+
const target = args.target === undefined ? "song" : args.target;
|
|
31
|
+
if (target !== "song" && target !== "track")
|
|
32
|
+
throw new TuningToolError("target must be song or track");
|
|
33
|
+
const prefix = target === "track" ? "tuning track" : "tuning";
|
|
34
|
+
if (args.off === true) return [`${prefix} off`];
|
|
35
|
+
const commands: string[] = [];
|
|
36
|
+
const tables = ["name", "edo", "ratios", "cents", "scl"].filter(
|
|
37
|
+
(field) => args[field] !== undefined,
|
|
38
|
+
);
|
|
39
|
+
if (tables.length > 1)
|
|
40
|
+
throw new TuningToolError(
|
|
41
|
+
`give one of name, edo, ratios, cents or scl (got ${tables.join(", ")})`,
|
|
42
|
+
);
|
|
43
|
+
const word = (value: unknown, field: string): string => {
|
|
44
|
+
if (typeof value !== "string" || !/^[^\s]{1,256}$/.test(value))
|
|
45
|
+
throw new TuningToolError(`${field} must be one word`);
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
const table = tables[0];
|
|
49
|
+
if (table === "name") commands.push(`${prefix} ${word(args.name, "name")}`);
|
|
50
|
+
else if (table === "edo") {
|
|
51
|
+
const edo = args.edo;
|
|
52
|
+
if (
|
|
53
|
+
typeof edo !== "number" ||
|
|
54
|
+
!Number.isInteger(edo) ||
|
|
55
|
+
edo < 1 ||
|
|
56
|
+
edo > TUNING_LIMITS.maxEdo
|
|
57
|
+
)
|
|
58
|
+
throw new TuningToolError(`edo must be 1..${TUNING_LIMITS.maxEdo}`);
|
|
59
|
+
commands.push(`${prefix} edo ${edo}`);
|
|
60
|
+
} else if (table === "ratios" || table === "cents") {
|
|
61
|
+
const list = args[table];
|
|
62
|
+
if (!Array.isArray(list) || list.length < 1 || list.length > MAX_TABLE)
|
|
63
|
+
throw new TuningToolError(`${table} must list 1..${MAX_TABLE} steps`);
|
|
64
|
+
const items = list.map((item, index) => {
|
|
65
|
+
if (table === "cents" && typeof item === "number" && isFinite(item))
|
|
66
|
+
return String(item);
|
|
67
|
+
return word(
|
|
68
|
+
typeof item === "number" ? String(item) : item,
|
|
69
|
+
`${table}[${index}]`,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
commands.push(`${prefix} ${table} ${items.join(" ")}`);
|
|
73
|
+
} else if (table === "scl") {
|
|
74
|
+
const scl = word(args.scl, "scl");
|
|
75
|
+
commands.push(
|
|
76
|
+
args.kbm !== undefined
|
|
77
|
+
? `${prefix} scl ${scl} kbm ${word(args.kbm, "kbm")}`
|
|
78
|
+
: `${prefix} scl ${scl}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (table !== "scl" && args.kbm !== undefined)
|
|
82
|
+
commands.push(
|
|
83
|
+
args.kbm === null
|
|
84
|
+
? `${prefix} kbm off`
|
|
85
|
+
: `${prefix} kbm ${word(args.kbm, "kbm")}`,
|
|
86
|
+
);
|
|
87
|
+
if (args.ref !== undefined) {
|
|
88
|
+
if (args.ref === null) commands.push(`${prefix} ref off`);
|
|
89
|
+
else if (typeof args.ref === "number" && isFinite(args.ref))
|
|
90
|
+
commands.push(`${prefix} ref ${args.ref}`);
|
|
91
|
+
else throw new TuningToolError("ref must be a number of Hz or null");
|
|
92
|
+
}
|
|
93
|
+
if (args.root !== undefined)
|
|
94
|
+
commands.push(
|
|
95
|
+
`${prefix} root ${args.root === null ? "auto" : word(String(args.root), "root")}`,
|
|
96
|
+
);
|
|
97
|
+
if (args.map !== undefined) {
|
|
98
|
+
if (!TUNING_MAPS.includes(args.map as never))
|
|
99
|
+
throw new TuningToolError(`map must be ${TUNING_MAPS.join(" or ")}`);
|
|
100
|
+
commands.push(`${prefix} map ${String(args.map)}`);
|
|
101
|
+
}
|
|
102
|
+
if (commands.length === 0)
|
|
103
|
+
throw new TuningToolError(
|
|
104
|
+
"give a tuning (name, edo, ratios, cents or scl), ref, root, map or off",
|
|
105
|
+
);
|
|
106
|
+
return commands;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Runs `commands` in order and returns the operations they produced. */
|
|
110
|
+
function planCommands(
|
|
111
|
+
score: TrackScore,
|
|
112
|
+
trackId: string,
|
|
113
|
+
commands: readonly string[],
|
|
114
|
+
read: ProjectRead,
|
|
115
|
+
): ScorePlan {
|
|
116
|
+
let next = score;
|
|
117
|
+
const operations: ScoreOperation[] = [];
|
|
118
|
+
const messages: string[] = [];
|
|
119
|
+
for (const text of commands) {
|
|
120
|
+
const command = parseTuningCommand(text);
|
|
121
|
+
if (!command) throw new TuningToolError(`cannot read ${text}`);
|
|
122
|
+
const result: TuningResult = applyTuningCommand(
|
|
123
|
+
next,
|
|
124
|
+
trackId,
|
|
125
|
+
command,
|
|
126
|
+
read,
|
|
127
|
+
);
|
|
128
|
+
if (!result.ok || !result.next) throw new TuningToolError(result.message);
|
|
129
|
+
operations.push(operationOf(result, trackId));
|
|
130
|
+
messages.push(result.message);
|
|
131
|
+
next = result.next;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
kind: "score",
|
|
135
|
+
operations,
|
|
136
|
+
summary: messages.at(-1) ?? "tuning",
|
|
137
|
+
trackId,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function operationOf(result: TuningResult, trackId: string): ScoreOperation {
|
|
142
|
+
const payload = result.payload ?? {};
|
|
143
|
+
if (result.kind === "score.tuning")
|
|
144
|
+
return {
|
|
145
|
+
type: "setTuning",
|
|
146
|
+
tuning: (payload.tuning as Tuning | null | undefined) ?? null,
|
|
147
|
+
};
|
|
148
|
+
if (result.kind === "score.track")
|
|
149
|
+
return {
|
|
150
|
+
type: "updateTrack",
|
|
151
|
+
trackId,
|
|
152
|
+
patch: (payload.patch as { tuning: Tuning | null }) ?? {},
|
|
153
|
+
};
|
|
154
|
+
if (result.kind === "score.key")
|
|
155
|
+
return { type: "setKey", key: (payload.key as string | null) ?? null };
|
|
156
|
+
throw new TuningToolError(`unexpected result ${result.kind ?? ""}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function trackFor(args: Record<string, unknown>, context: ToolContext): string {
|
|
160
|
+
const id = args.trackId ?? context.focusedTrackId;
|
|
161
|
+
if (typeof id !== "string" || !context.score.tracks.some((t) => t.id === id))
|
|
162
|
+
throw new TuningToolError(`no track ${String(id)}`);
|
|
163
|
+
return id;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const SET_TUNING: AgentTool = {
|
|
167
|
+
name: "set_tuning",
|
|
168
|
+
description:
|
|
169
|
+
"Set the song tuning (every track follows it) or one track's own tuning: a library tuning by name, n-EDO, just ratios, cents, or a Scala .scl/.kbm file in the project; plus A4 reference, root key and key mapping. off returns to 12-TET (track: follows the song). Note-level detune is add_notes cents.",
|
|
170
|
+
parameters: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
target: { type: "string", enum: ["song", "track"] },
|
|
174
|
+
trackId: { type: "string", description: "track target; default focused" },
|
|
175
|
+
name: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description:
|
|
178
|
+
"19-edo, 24-edo, 31-edo, pythagorean, just, 7-limit, well-tuned-piano, pelog, slendro, rast, bayati, yaman…",
|
|
179
|
+
},
|
|
180
|
+
edo: { type: "integer", minimum: 1, maximum: TUNING_LIMITS.maxEdo },
|
|
181
|
+
ratios: {
|
|
182
|
+
type: "array",
|
|
183
|
+
items: { type: "string" },
|
|
184
|
+
maxItems: MAX_TABLE,
|
|
185
|
+
description:
|
|
186
|
+
'degrees 1..n, last is the period: ["9/8","5/4","3/2","2/1"]',
|
|
187
|
+
},
|
|
188
|
+
cents: {
|
|
189
|
+
type: "array",
|
|
190
|
+
items: { type: "number" },
|
|
191
|
+
maxItems: MAX_TABLE,
|
|
192
|
+
description: "degrees 1..n in cents, last is the period (1200)",
|
|
193
|
+
},
|
|
194
|
+
scl: { type: "string", description: "project path to a .scl file" },
|
|
195
|
+
kbm: {
|
|
196
|
+
type: ["string", "null"],
|
|
197
|
+
description: "project path to a .kbm keyboard map; null removes it",
|
|
198
|
+
},
|
|
199
|
+
ref: {
|
|
200
|
+
type: ["number", "null"],
|
|
201
|
+
minimum: TUNING_LIMITS.minRefHz,
|
|
202
|
+
maximum: TUNING_LIMITS.maxRefHz,
|
|
203
|
+
description: "A4 in Hz (440); null resets",
|
|
204
|
+
},
|
|
205
|
+
root: {
|
|
206
|
+
type: ["string", "null"],
|
|
207
|
+
description: 'key of degree 0 ("D4"); null follows the song key',
|
|
208
|
+
},
|
|
209
|
+
map: {
|
|
210
|
+
type: "string",
|
|
211
|
+
enum: TUNING_MAPS,
|
|
212
|
+
description:
|
|
213
|
+
"linear: one key per step · nearest: 12 keys to nearest step",
|
|
214
|
+
},
|
|
215
|
+
off: { type: "boolean" },
|
|
216
|
+
},
|
|
217
|
+
additionalProperties: false,
|
|
218
|
+
},
|
|
219
|
+
plan(args, context) {
|
|
220
|
+
const trackId = trackFor(args, context);
|
|
221
|
+
const commands = tuningCommandsFor(args);
|
|
222
|
+
const needsFiles = args.scl !== undefined || typeof args.kbm === "string";
|
|
223
|
+
const current =
|
|
224
|
+
args.target === "track"
|
|
225
|
+
? context.score.tracks.find((t) => t.id === trackId)?.tuning
|
|
226
|
+
: context.score.tuning;
|
|
227
|
+
if (!needsFiles && !current?.scl && !current?.kbm)
|
|
228
|
+
return planCommands(context.score, trackId, commands, () => undefined);
|
|
229
|
+
return {
|
|
230
|
+
kind: "prepare",
|
|
231
|
+
summary: commands.join(" · "),
|
|
232
|
+
async run(host) {
|
|
233
|
+
const files = new Map<string, string>();
|
|
234
|
+
if (host.workspace) {
|
|
235
|
+
const paths = [args.scl, args.kbm, current?.scl, current?.kbm].filter(
|
|
236
|
+
(path): path is string => typeof path === "string",
|
|
237
|
+
);
|
|
238
|
+
for (const path of paths) {
|
|
239
|
+
try {
|
|
240
|
+
const resolved = await resolveReadPath(
|
|
241
|
+
{ root: host.workspace.root, trackSlug: "" },
|
|
242
|
+
path,
|
|
243
|
+
);
|
|
244
|
+
files.set(path, await readFile(resolved.real, "utf8"));
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (!(error instanceof WorkspaceError)) throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return planCommands(context.score, trackId, commands, (path) =>
|
|
251
|
+
files.get(path),
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const SET_SCALE: AgentTool = {
|
|
259
|
+
name: "set_scale",
|
|
260
|
+
description:
|
|
261
|
+
"Set the song key and scale: a church mode, harmonic or melodic minor, pentatonic, blues, maqam (hijaz, bayati), raga (yaman, bhairav, kafi…) or a Messiaen mode. Chords, play-mode keys and scale snapping follow it. Scales with microtones or just intonation pair with set_tuning name=<scale>.",
|
|
262
|
+
parameters: {
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
tonic: {
|
|
266
|
+
type: "string",
|
|
267
|
+
description: '"C", "F#", "Eb"; default song tonic',
|
|
268
|
+
},
|
|
269
|
+
scale: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description:
|
|
272
|
+
"dorian, harmonic-minor, minor-pentatonic, blues, hijaz, bayati, yaman, bhairav, kafi, messiaen-3…",
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
required: ["scale"],
|
|
276
|
+
additionalProperties: false,
|
|
277
|
+
},
|
|
278
|
+
plan(args, context) {
|
|
279
|
+
const scale = args.scale;
|
|
280
|
+
if (typeof scale !== "string" || !/^[a-z0-9-]{1,32}$/i.test(scale))
|
|
281
|
+
throw new TuningToolError("scale must be a scale name");
|
|
282
|
+
const tonic = args.tonic;
|
|
283
|
+
if (
|
|
284
|
+
tonic !== undefined &&
|
|
285
|
+
(typeof tonic !== "string" || !/^[a-g][#b]?$/i.test(tonic))
|
|
286
|
+
)
|
|
287
|
+
throw new TuningToolError('tonic must be a note name like "D" or "Eb"');
|
|
288
|
+
const command = tonic ? `scale ${tonic} ${scale}` : `scale ${scale}`;
|
|
289
|
+
return planCommands(
|
|
290
|
+
context.score,
|
|
291
|
+
context.focusedTrackId,
|
|
292
|
+
[command],
|
|
293
|
+
() => undefined,
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const TUNING_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
299
|
+
SET_TUNING,
|
|
300
|
+
SET_SCALE,
|
|
301
|
+
]);
|
package/src/agent/xcb-agent.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GRANULAR_PARAMS } from "../../core/granular.ts";
|
|
1
2
|
import { compositionBrief } from "./brief.ts";
|
|
2
3
|
import { CHORD_PROCESS } from "../../core/chords.ts";
|
|
3
4
|
import {
|
|
@@ -46,12 +47,20 @@ export const TEXT_AGENT_SYSTEM_PROMPT = [
|
|
|
46
47
|
"Times are in beats from the loop start (0-based). Keep notes inside loopBeats unless you extend the loop first.",
|
|
47
48
|
"Prefer a few well-formed ops (one add_notes op per track part) over many tiny ones.",
|
|
48
49
|
'For drums, create a track with instrument "kit" and prefer set_rhythm (Euclidean rows: pulses over steps, rotate, repeats for rolls, accent, probability, swing) so the beat stays editable as parameters; for a genre groove start from apply_drum_pattern (list_drum_patterns) and pick a sound with set_drum_kit; use add_drums only for one-off fills. Drum pitches select voices, so do not use add_notes for beats.',
|
|
50
|
+
"Song structure: list_sections shows named sections (bars count from 1); edit_section marks, adds, duplicates, moves, renames, deletes, mutes, varies (transpose/gain) and loops them; set_form sets the play order with repeats (intro verse chorus*2 outro); add_transition generates builds (riser, snare roll, filter sweep, uplifter), drops (cut plus impact) and drum fills at section boundaries.",
|
|
49
51
|
CHORD_PROCESS,
|
|
50
52
|
"Synth voices: set_synth shapes a synth track with Strudel params (ADSR, lpf/lpenv filter envelopes, fm/fmh, supersaw unison/detune, vib, penv, noise; z_* ZzFX sounds take slide/pitchJump/lfo/zcrush…) or a preset (pad lead pluck bass keys bell…).",
|
|
53
|
+
"Plucked strings: set_string makes a track a physical-model string (nylon steel electric jangle ebass slap upright motown sitar tanpura harpsichord lute oud setar tar santur dulcimer koto harp banjo tres requinto) and overrides params (ring bright damp pos mute buzz body sym). The bare words sitar, ebass, pluck keep their older voices; use set_string for the modelled ones.",
|
|
54
|
+
"Pianos: set_instrument piano (or grand upright felt honkytonk prepared, ballad, lofi) loads a modelled piano with real hammers, stretched inharmonic strings, dampers and the sustain pedal; set_keys tweaks it (hardness touch decay release felt stretch body…, automate as keys-<param> lanes). A track already stored as instrument piano keeps the legacy tone until you set it again.",
|
|
55
|
+
"Mallets and bells: set_instrument vibes (or marimba via modal, xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani) plays the modal engine; set_modal changes mallet (yarn…brass), hardness, position, ring, damp, motor/motordepth (vibes tremolo), buzz, click. Plain instrument marimba stays the legacy tone.",
|
|
51
56
|
"Sampler voices: set_sample sets Strudel sample controls per voice (begin end speed unit loop loopBegin loopEnd clip/legato fit loopAt accelerate squiz cut); splice = slices with fit.",
|
|
57
|
+
"Fitting samples (0.6): fit_sample sets a voice's own bpm (or len in beats) so it follows the song's tempo map; fitmode beats for drums and speech (hits stay sharp), tones for pads and vocals (pitch kept), repitch (default) like tape.",
|
|
52
58
|
"Effects (set_fx; set_effects and set_automation): the brief's effects list is the fixed chain order with each effect's simple params. set_fx turns an effect on with good defaults, loads a preset or sets params by dawg or Strudel name. Lanes: volume, pan, filter, resonance, delay-feedback, delay-mix, wt and <effect>-<param> (e.g. autofilter-cutoff).",
|
|
59
|
+
"Guitar rig (set_rig): rig loads a whole rig (clean crunch punk ragged lead metal fuzz octave funk wah bachata spring bassdrive reese jangle alt) on a guitar or bass track; stomp, head and cab tweak the pedal, amp (level-matched, gate in dB) and cabinet. Tracks named jangle punk funk ragged gtr-lead gtr-metal bachata start with a guitar voice and that rig. `amp` is Strudel gain; the guitar amp is head.",
|
|
53
60
|
"Previewing: preview_sound renders a track, or candidate sound tool calls (changes: [{tool, args}]), without committing, returns loudness, brightness and a comparison, and plays it once in the user's window. Use it when choosing between sounds (tables, presets, effect amounts), say how it sounds, then commit with the normal tools.",
|
|
54
61
|
"Wavetable synth: set_wavetable picks a table (built-ins offline, Strudel wt_ sets fetched once) and scans it by position wt.",
|
|
62
|
+
`Granular: set_granular makes a track a grain cloud (presets cloud hold sparkle swarm stutter microloop backwards dust) of a synth source (src synth:pad, the default; synth:<name>@<note>) or one of its own sampler voices, and overrides params (${Object.keys(GRANULAR_PARAMS).join(" ")}; repeat and hold latch the head for beat repeats, begin and end pick a region, root takes a MIDI number or note name); off returns to the previous voice; the same seed gives the same grains.`,
|
|
63
|
+
"Mastering: set_master sets the song master and loudness target; measure_mix reports LUFS, true peak, balance and correlation. Master only when asked for loudness or a finished sound, and measure before and after.",
|
|
55
64
|
'Reply with exactly one JSON object and nothing else, shaped {"ops":[{"tool":"<tool name>","args":{...}}],"say":"<one short sentence describing the musical change>","done":true}.',
|
|
56
65
|
'Set "done":false only if you need to see the results of these ops before continuing; you will then get each op\'s result and can send more ops.',
|
|
57
66
|
"If an op was rejected, read its diagnostic and either send a corrected op or stop with done:true.",
|