@hraness/dawg 0.4.0 → 0.5.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/DAWG.md +300 -35
  3. package/README.md +4 -4
  4. package/core/chords.ts +288 -7
  5. package/core/diff.ts +37 -12
  6. package/core/expression.ts +1241 -0
  7. package/core/loop.ts +23 -0
  8. package/core/master.ts +455 -0
  9. package/core/midi.ts +452 -0
  10. package/core/rhythm.ts +7 -2
  11. package/core/score.ts +536 -56
  12. package/core/sdk/eval-child.ts +38 -3
  13. package/core/sdk/print.ts +445 -14
  14. package/core/sdk/v1.ts +1709 -23
  15. package/core/sections.ts +2046 -0
  16. package/core/synth.ts +11 -1
  17. package/core/tempo.ts +1318 -0
  18. package/core/tuning.ts +1180 -0
  19. package/guides/audition.md +26 -0
  20. package/guides/automation.md +26 -0
  21. package/guides/chords.md +28 -0
  22. package/guides/effects.md +27 -0
  23. package/guides/faders.md +29 -0
  24. package/guides/files.md +28 -0
  25. package/guides/getting-started.md +26 -0
  26. package/guides/index.ts +75 -0
  27. package/guides/keys.md +27 -0
  28. package/guides/media.md +27 -0
  29. package/guides/mix.md +23 -0
  30. package/guides/music.md +14 -0
  31. package/guides/notes.md +26 -0
  32. package/guides/performance.md +26 -0
  33. package/guides/play.md +24 -0
  34. package/guides/project.md +13 -0
  35. package/guides/providers.md +25 -0
  36. package/guides/rhythm.md +26 -0
  37. package/guides/sessions.md +21 -0
  38. package/guides/sound.md +15 -0
  39. package/guides/sounds.md +25 -0
  40. package/guides/tempo.md +26 -0
  41. package/guides/tracks.md +25 -0
  42. package/guides/web-search.md +21 -0
  43. package/package.json +3 -1
  44. package/src/agent/agent.ts +2 -0
  45. package/src/agent/brief.ts +77 -3
  46. package/src/agent/chord-tools.ts +9 -1
  47. package/src/agent/expression-tools.ts +336 -0
  48. package/src/agent/master-tools.ts +299 -0
  49. package/src/agent/models.ts +4 -4
  50. package/src/agent/ops.ts +29 -4
  51. package/src/agent/planner.ts +45 -2
  52. package/src/agent/preview-tool.ts +21 -3
  53. package/src/agent/section-tools.ts +411 -0
  54. package/src/agent/time-tools.ts +290 -0
  55. package/src/agent/tools.ts +36 -2
  56. package/src/agent/tuning-tools.ts +301 -0
  57. package/src/agent/xcb-agent.ts +2 -0
  58. package/src/audio/arrange.ts +471 -0
  59. package/src/audio/audition.ts +16 -3
  60. package/src/audio/click.ts +113 -1
  61. package/src/audio/clock.ts +71 -5
  62. package/src/audio/effects/bus.ts +5 -4
  63. package/src/audio/effects/chain.ts +12 -2
  64. package/src/audio/effects/common.ts +30 -1
  65. package/src/audio/effects/convolution.ts +43 -5
  66. package/src/audio/effects/dynamics.ts +2 -1
  67. package/src/audio/effects/filter.ts +5 -3
  68. package/src/audio/effects/modulation.ts +5 -1
  69. package/src/audio/effects/space.ts +197 -50
  70. package/src/audio/engine.ts +175 -20
  71. package/src/audio/live.ts +33 -6
  72. package/src/audio/loudness.ts +551 -0
  73. package/src/audio/master.ts +660 -0
  74. package/src/audio/measure-worker.ts +45 -0
  75. package/src/audio/measure.ts +110 -0
  76. package/src/audio/player.ts +11 -6
  77. package/src/audio/preview.ts +87 -16
  78. package/src/audio/render-worker.ts +6 -1
  79. package/src/audio/renderer.ts +7 -1
  80. package/src/audio/sampler.ts +108 -19
  81. package/src/audio/synth/voice.ts +60 -13
  82. package/src/audio/synth/zzfx.ts +10 -4
  83. package/src/audio/warp.ts +86 -0
  84. package/src/audio/wav.ts +448 -59
  85. package/src/commands/arrange.ts +949 -0
  86. package/src/commands/expression.ts +934 -0
  87. package/src/commands/help.ts +281 -24
  88. package/src/commands/master.ts +361 -0
  89. package/src/commands/music.ts +6 -1
  90. package/src/commands/synth.ts +11 -1
  91. package/src/commands/time.ts +964 -0
  92. package/src/commands/tuning.ts +490 -0
  93. package/src/main.ts +826 -47
  94. package/src/render.ts +109 -7
  95. package/src/session/daemon.ts +18 -8
  96. package/src/session/naming.ts +8 -1
  97. package/src/session/rebase.ts +21 -0
  98. package/src/tui/arrange-menu.ts +390 -0
  99. package/src/tui/audition.ts +58 -5
  100. package/src/tui/euclid.ts +50 -6
  101. package/src/tui/fader.ts +409 -0
  102. package/src/tui/menu-time.ts +401 -0
  103. package/src/tui/menu.ts +656 -16
  104. package/src/tui/performance-menu.ts +235 -0
  105. package/src/tui/play-chords.ts +95 -1
  106. package/src/tui/play-mode.ts +150 -6
  107. package/src/tui/play-session.ts +414 -41
  108. package/tui/app.ts +262 -13
  109. package/tui/arrange-strip.ts +174 -0
  110. package/tui/drawer.ts +478 -0
  111. package/tui/grammar.ts +79 -12
  112. package/tui/guide.ts +351 -0
  113. package/tui/highway.ts +87 -4
  114. package/tui/hits.ts +68 -0
  115. package/tui/input.ts +7 -0
  116. package/tui/keys.ts +72 -0
@@ -7,6 +7,7 @@ import {
7
7
  type ScoreOperation,
8
8
  type TrackScore,
9
9
  } from "../../core/score.ts";
10
+ import { loopTicksOf } from "../../core/tempo.ts";
10
11
  import { AVAILABLE_INSTRUMENTS } from "../audio/wav.ts";
11
12
  import {
12
13
  SYNTH_PRESETS,
@@ -44,9 +45,15 @@ import {
44
45
  import { PackError, type PackStore } from "../audio/packs.ts";
45
46
  import { RHYTHM_TOOLS } from "./rhythm-tools.ts";
46
47
  import { CHORD_TOOLS } from "./chord-tools.ts";
48
+ import { EXPRESSION_TOOLS } from "./expression-tools.ts";
49
+ import { TUNING_TOOLS } from "./tuning-tools.ts";
50
+ import { MASTER_TOOLS } from "./master-tools.ts";
47
51
  import { DRUM_TOOLS } from "./drum-tools.ts";
52
+ import { TIME_TOOLS } from "./time-tools.ts";
53
+ import { SECTION_TOOLS } from "./section-tools.ts";
48
54
  import type { MediaResult, MediaRunContext } from "../media/types.ts";
49
55
  import { pitchToMidi } from "./ops.ts";
56
+ import { TUNING_LIMITS } from "../../core/tuning.ts";
50
57
  import {
51
58
  DRUM_VOICES,
52
59
  drumVoicePitch,
@@ -244,6 +251,12 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
244
251
  description: "Length in beats",
245
252
  },
246
253
  velocity: { type: "number", minimum: 0, maximum: 1 },
254
+ cents: {
255
+ type: "number",
256
+ minimum: -TUNING_LIMITS.maxNoteCents,
257
+ maximum: TUNING_LIMITS.maxNoteCents,
258
+ description: "Detune from the tuned pitch (microtones)",
259
+ },
247
260
  },
248
261
  required: ["pitch", "start", "duration"],
249
262
  additionalProperties: false,
@@ -262,6 +275,10 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
262
275
  const start = number(note, "start", { min: 0 });
263
276
  const duration = number(note, "duration", { min: 0, exclusive: true });
264
277
  const velocity = optionalNumber(note, "velocity", { min: 0, max: 1 });
278
+ const cents = optionalNumber(note, "cents", {
279
+ min: -TUNING_LIMITS.maxNoteCents,
280
+ max: TUNING_LIMITS.maxNoteCents,
281
+ });
265
282
  return {
266
283
  type: "addNote",
267
284
  note: {
@@ -271,6 +288,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
271
288
  durationTicks: Math.max(1, Math.round(duration * tpb)),
272
289
  pitch: pitch(note.pitch, `notes[${index}].pitch`),
273
290
  velocity: velocity ?? 0.8,
291
+ ...(cents ? { cents } : {}),
274
292
  },
275
293
  };
276
294
  });
@@ -322,7 +340,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
322
340
  {
323
341
  name: "update_notes",
324
342
  description:
325
- "Move, resize, transpose, or re-velocity existing notes by id. Times are in beats.",
343
+ "Move, resize, transpose, re-velocity or detune existing notes by id. Times are in beats.",
326
344
  parameters: {
327
345
  type: "object",
328
346
  properties: {
@@ -343,6 +361,12 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
343
361
  ],
344
362
  },
345
363
  velocity: { type: "number", minimum: 0, maximum: 1 },
364
+ cents: {
365
+ type: "number",
366
+ minimum: -TUNING_LIMITS.maxNoteCents,
367
+ maximum: TUNING_LIMITS.maxNoteCents,
368
+ description: "Detune from the tuned pitch; 0 clears it",
369
+ },
346
370
  },
347
371
  required: ["noteId"],
348
372
  additionalProperties: false,
@@ -378,6 +402,11 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
378
402
  max: 1,
379
403
  });
380
404
  if (velocity !== undefined) patch.velocity = velocity;
405
+ const cents = optionalNumber(update, "cents", {
406
+ min: -TUNING_LIMITS.maxNoteCents,
407
+ max: TUNING_LIMITS.maxNoteCents,
408
+ });
409
+ if (cents !== undefined) patch.cents = cents;
381
410
  if (Object.keys(patch).length === 0)
382
411
  throw new ToolArgumentError(`updates[${index}] changes nothing`);
383
412
  return { type: "updateNote", noteId, patch };
@@ -915,7 +944,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
915
944
  `track ${trackId} is ${track.instrument}, not a drum kit; set_instrument kit or create a kit track`,
916
945
  );
917
946
  const tpb = context.score.ticksPerBeat;
918
- const loopBeats = context.score.bars * context.score.beatsPerBar;
947
+ const loopBeats = loopTicksOf(context.score) / tpb;
919
948
  const voiceOf = (value: unknown, label: string) => {
920
949
  const voice =
921
950
  typeof value === "string" ? parseDrumVoice(value) : undefined;
@@ -1350,9 +1379,14 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
1350
1379
  },
1351
1380
  ...RHYTHM_TOOLS,
1352
1381
  ...CHORD_TOOLS,
1382
+ ...EXPRESSION_TOOLS,
1383
+ ...TUNING_TOOLS,
1353
1384
  ...DRUM_TOOLS,
1385
+ ...TIME_TOOLS,
1386
+ ...SECTION_TOOLS,
1354
1387
  ...MEDIA_TOOLS,
1355
1388
  ...PACK_TOOLS,
1389
+ ...MASTER_TOOLS,
1356
1390
  // Looks tools up at call time, so it can plan any of the above.
1357
1391
  previewSoundTool((name) => findAgentTool(name)),
1358
1392
  ] satisfies AgentTool[]);
@@ -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
+ ]);
@@ -46,12 +46,14 @@ export const TEXT_AGENT_SYSTEM_PROMPT = [
46
46
  "Times are in beats from the loop start (0-based). Keep notes inside loopBeats unless you extend the loop first.",
47
47
  "Prefer a few well-formed ops (one add_notes op per track part) over many tiny ones.",
48
48
  '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.',
49
+ "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
50
  CHORD_PROCESS,
50
51
  "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…).",
51
52
  "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.",
52
53
  "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).",
53
54
  "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
55
  "Wavetable synth: set_wavetable picks a table (built-ins offline, Strudel wt_ sets fetched once) and scans it by position wt.",
56
+ "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
57
  '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
58
  '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
59
  "If an op was rejected, read its diagnostic and either send a corrected op or stop with done:true.",