c64bridge 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.
- package/.github/prompts/README.md +6 -0
- package/.github/prompts/assembly-routine-session.prompt.md +13 -0
- package/.github/prompts/basic-program-session.prompt.md +13 -0
- package/.github/prompts/drive-management-session.prompt.md +13 -0
- package/.github/prompts/petscii-art-session.prompt.md +13 -0
- package/.github/prompts/play-classic-game.prompt.md +13 -0
- package/.github/prompts/sid-music-session.prompt.md +13 -0
- package/AGENTS.md +173 -0
- package/LICENSE +339 -0
- package/README.md +582 -0
- package/data/README.md +16 -0
- package/data/api/basic-api-spec.md +154 -0
- package/data/api/kernal-api-spec.md +68 -0
- package/data/assembly/assembly-spec.md +196 -0
- package/data/assembly/examples/effects/sinewave.asm +5 -0
- package/data/assembly/examples/graphics/sprite/mover.asm +15 -0
- package/data/assembly/examples/sid-file-structure.md +8 -0
- package/data/assembly/examples/sid-overview.md +11 -0
- package/data/audio/examples/Biosphere.sid +0 -0
- package/data/audio/sid-file-structure.md +91 -0
- package/data/audio/sid-programming-best-practices.md +198 -0
- package/data/audio/sid-spec.md +166 -0
- package/data/audio/sidwave.md +131 -0
- package/data/audio/sidwave.schema.json +64 -0
- package/data/basic/basic-spec.md +230 -0
- package/data/basic/examples/audio/alle-meine-entchen-expressive.bas +41 -0
- package/data/basic/examples/audio/c-major-scale-pleasant.bas +29 -0
- package/data/basic/examples/io/joystick.bas +5 -0
- package/data/basic/examples/video/bounce.bas +8 -0
- package/data/basic/examples/video/entchen-petscii.bas +16 -0
- package/data/basic/examples/video/hello-world.bas +4 -0
- package/data/basic/examples/video/wave.bas +7 -0
- package/data/context/bootstrap.md +148 -0
- package/data/context/chat.md +36 -0
- package/data/drive/drive-spec.md +215 -0
- package/data/embeddings_asm.json +1 -0
- package/data/embeddings_basic.json +1 -0
- package/data/embeddings_hardware.json +1 -0
- package/data/embeddings_mixed.json +1 -0
- package/data/embeddings_other.json +1 -0
- package/data/io/cia-spec.md +115 -0
- package/data/io/io-spec.md +124 -0
- package/data/memory/kernal-memory-map.md +227 -0
- package/data/memory/low-memory-map.md +133 -0
- package/data/memory/memory-map.md +186 -0
- package/data/printer/printer-commodore-bitmap.md +31 -0
- package/data/printer/printer-commodore.md +28 -0
- package/data/printer/printer-epson-bitmap.md +29 -0
- package/data/printer/printer-epson.md +30 -0
- package/data/printer/printer-prompts.md +29 -0
- package/data/printer/printer-spec.md +180 -0
- package/data/video/character-set.csv +257 -0
- package/data/video/vic-spec.md +276 -0
- package/dist/assemblyConverter.js +1299 -0
- package/dist/audio/record_and_analyze_audio.js +292 -0
- package/dist/basicConverter.js +193 -0
- package/dist/bootstrap/stdio-logger.js +32 -0
- package/dist/c64Client.js +1052 -0
- package/dist/chargen.js +197 -0
- package/dist/config.js +142 -0
- package/dist/context.js +157 -0
- package/dist/device.js +413 -0
- package/dist/index.js +7 -0
- package/dist/knowledge.js +454 -0
- package/dist/logger.js +182 -0
- package/dist/loggingHttpClient.js +88 -0
- package/dist/mcp-server.js +426 -0
- package/dist/petscii.js +135 -0
- package/dist/petsciiArt.js +356 -0
- package/dist/platform.js +82 -0
- package/dist/prompts/registry.js +560 -0
- package/dist/rag/discover.config.json +41 -0
- package/dist/rag/discover.js +401 -0
- package/dist/rag/embeddings.js +60 -0
- package/dist/rag/externalFetcher.js +934 -0
- package/dist/rag/indexer.js +770 -0
- package/dist/rag/init.js +139 -0
- package/dist/rag/knowledgeIndex.js +361 -0
- package/dist/rag/loggingRetriever.js +46 -0
- package/dist/rag/rateLimiter.js +127 -0
- package/dist/rag/retriever.js +74 -0
- package/dist/rag/sources.csv +31 -0
- package/dist/rag/types.js +5 -0
- package/dist/rag/urlUtils.js +38 -0
- package/dist/sidplayRunner.js +240 -0
- package/dist/sidwave.js +101 -0
- package/dist/sidwaveCompiler.js +329 -0
- package/dist/tools/audio.js +944 -0
- package/dist/tools/developer.js +515 -0
- package/dist/tools/errors.js +59 -0
- package/dist/tools/graphics.js +469 -0
- package/dist/tools/machineControl.js +267 -0
- package/dist/tools/memory.js +224 -0
- package/dist/tools/printer.js +482 -0
- package/dist/tools/programRunners.js +315 -0
- package/dist/tools/rag.js +153 -0
- package/dist/tools/registry.js +58 -0
- package/dist/tools/responses.js +36 -0
- package/dist/tools/schema.js +304 -0
- package/dist/tools/storage.js +740 -0
- package/dist/tools/streaming.js +132 -0
- package/dist/tools/types.js +99 -0
- package/dist/viceRunner.js +239 -0
- package/doc/MCP_SETUP.md +153 -0
- package/doc/README.md +7 -0
- package/doc/developer.md +312 -0
- package/doc/img/logo.png +0 -0
- package/doc/img/prompts/duck_petscii.png +0 -0
- package/doc/img/prompts/duck_song.png +0 -0
- package/doc/img/vscode/vscode-copilot-c64-chat-mode.png +0 -0
- package/doc/img/vscode/vscode-copilot-hello-world.png +0 -0
- package/doc/prompts/README.md +96 -0
- package/doc/prompts/user-prompt-ideas.md +147 -0
- package/doc/rest/c64-openapi.yaml +979 -0
- package/doc/rest/c64-rest-api.md +106 -0
- package/doc/troubleshooting-mcp.md +177 -0
- package/generated/c64/index.js +857 -0
- package/mcp.json +106 -0
- package/package.json +79 -0
- package/scripts/c64-cli.mjs +142 -0
- package/scripts/c64-tool.sh +83 -0
- package/scripts/check-package.mjs +89 -0
- package/scripts/cli.js +61 -0
- package/scripts/fetch-external-sources.mjs +67 -0
- package/scripts/generate-changelog.mjs +131 -0
- package/scripts/postbuild.mjs +101 -0
- package/scripts/prepare-release.mjs +58 -0
- package/scripts/rag-rebuild.mjs +53 -0
- package/scripts/register-ts-node.mjs +6 -0
- package/scripts/run-mcp-check.sh +104 -0
- package/scripts/run-tests.mjs +195 -0
- package/scripts/sid2wav.sh +94 -0
- package/scripts/smoke-run.sh +65 -0
- package/scripts/start-mock-c64.mjs +51 -0
- package/scripts/start-mock.mjs +31 -0
- package/scripts/start.mjs +72 -0
- package/scripts/update-readme.ts +164 -0
- package/scripts/verify-package.mjs +147 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
import { compileSidwaveToPrg, compileSidwaveToSid } from "../sidwaveCompiler.js";
|
|
2
|
+
import { parseSidwave } from "../sidwave.js";
|
|
3
|
+
import { recordAndAnalyzeAudio } from "../audio/record_and_analyze_audio.js";
|
|
4
|
+
import { defineToolModule } from "./types.js";
|
|
5
|
+
import { booleanSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "./schema.js";
|
|
6
|
+
import { jsonResult, textResult } from "./responses.js";
|
|
7
|
+
import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
|
|
8
|
+
const NOTE_PATTERN = /^([A-Ga-g])([#b]?)(-?\d+)$/;
|
|
9
|
+
function toRecord(details) {
|
|
10
|
+
if (details && typeof details === "object") {
|
|
11
|
+
return details;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
function normaliseFailure(details) {
|
|
16
|
+
if (details === undefined || details === null) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
if (typeof details === "object") {
|
|
20
|
+
return details;
|
|
21
|
+
}
|
|
22
|
+
return { value: details };
|
|
23
|
+
}
|
|
24
|
+
function normaliseAddress(value, fallback) {
|
|
25
|
+
if (typeof value === "number") {
|
|
26
|
+
return `$${value.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "string" && value.length > 0) {
|
|
29
|
+
return value.startsWith("$") ? value : `$${value.toUpperCase()}`;
|
|
30
|
+
}
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
const sidVolumeArgsSchema = objectSchema({
|
|
34
|
+
description: "Set the SID master volume (0-15).",
|
|
35
|
+
properties: {
|
|
36
|
+
volume: numberSchema({
|
|
37
|
+
description: "Desired SID master volume. Values outside 0-15 are clamped.",
|
|
38
|
+
minimum: 0,
|
|
39
|
+
maximum: 15,
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
required: ["volume"],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
});
|
|
45
|
+
const sidResetArgsSchema = objectSchema({
|
|
46
|
+
description: "Reset or silence the SID chip.",
|
|
47
|
+
properties: {
|
|
48
|
+
hard: booleanSchema({
|
|
49
|
+
description: "If true, perform a hard register scrub; otherwise silence voices softly.",
|
|
50
|
+
default: false,
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
});
|
|
55
|
+
const sidNoteOnArgsSchema = objectSchema({
|
|
56
|
+
description: "Parameters for starting a SID note on a specific voice.",
|
|
57
|
+
properties: {
|
|
58
|
+
voice: numberSchema({
|
|
59
|
+
description: "SID voice to trigger (1-3).",
|
|
60
|
+
integer: true,
|
|
61
|
+
minimum: 1,
|
|
62
|
+
maximum: 3,
|
|
63
|
+
default: 1,
|
|
64
|
+
}),
|
|
65
|
+
note: optionalSchema(stringSchema({
|
|
66
|
+
description: "Musical note like A4, C#5, or Bb3. Overrides frequencyHz if provided.",
|
|
67
|
+
pattern: NOTE_PATTERN,
|
|
68
|
+
})),
|
|
69
|
+
frequencyHz: optionalSchema(numberSchema({
|
|
70
|
+
description: "Explicit frequency in Hz. Used when note is omitted.",
|
|
71
|
+
minimum: 1,
|
|
72
|
+
})),
|
|
73
|
+
system: stringSchema({
|
|
74
|
+
description: "Video system for frequency calculation.",
|
|
75
|
+
enum: ["PAL", "NTSC"],
|
|
76
|
+
default: "PAL",
|
|
77
|
+
}),
|
|
78
|
+
waveform: stringSchema({
|
|
79
|
+
description: "SID waveform to enable.",
|
|
80
|
+
enum: ["pulse", "saw", "tri", "noise"],
|
|
81
|
+
default: "pulse",
|
|
82
|
+
}),
|
|
83
|
+
pulseWidth: numberSchema({
|
|
84
|
+
description: "Pulse width (0-4095). Used for pulse waveform.",
|
|
85
|
+
integer: true,
|
|
86
|
+
minimum: 0,
|
|
87
|
+
maximum: 0x0fff,
|
|
88
|
+
default: 0x0800,
|
|
89
|
+
}),
|
|
90
|
+
attack: numberSchema({
|
|
91
|
+
description: "Attack rate nibble (0-15).",
|
|
92
|
+
integer: true,
|
|
93
|
+
minimum: 0,
|
|
94
|
+
maximum: 15,
|
|
95
|
+
default: 1,
|
|
96
|
+
}),
|
|
97
|
+
decay: numberSchema({
|
|
98
|
+
description: "Decay rate nibble (0-15).",
|
|
99
|
+
integer: true,
|
|
100
|
+
minimum: 0,
|
|
101
|
+
maximum: 15,
|
|
102
|
+
default: 1,
|
|
103
|
+
}),
|
|
104
|
+
sustain: numberSchema({
|
|
105
|
+
description: "Sustain level nibble (0-15).",
|
|
106
|
+
integer: true,
|
|
107
|
+
minimum: 0,
|
|
108
|
+
maximum: 15,
|
|
109
|
+
default: 15,
|
|
110
|
+
}),
|
|
111
|
+
release: numberSchema({
|
|
112
|
+
description: "Release rate nibble (0-15).",
|
|
113
|
+
integer: true,
|
|
114
|
+
minimum: 0,
|
|
115
|
+
maximum: 15,
|
|
116
|
+
default: 3,
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
});
|
|
121
|
+
const sidNoteOffArgsSchema = objectSchema({
|
|
122
|
+
description: "Parameters for releasing a SID voice.",
|
|
123
|
+
properties: {
|
|
124
|
+
voice: numberSchema({
|
|
125
|
+
description: "SID voice to release (1-3).",
|
|
126
|
+
integer: true,
|
|
127
|
+
minimum: 1,
|
|
128
|
+
maximum: 3,
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
required: ["voice"],
|
|
132
|
+
additionalProperties: false,
|
|
133
|
+
});
|
|
134
|
+
const sidSilenceArgsSchema = objectSchema({
|
|
135
|
+
description: "No arguments are required to silence all SID voices.",
|
|
136
|
+
properties: {},
|
|
137
|
+
additionalProperties: false,
|
|
138
|
+
});
|
|
139
|
+
const sidFileSchema = stringSchema({
|
|
140
|
+
description: "Absolute or Ultimate filesystem path to the SID, PRG, or audio file.",
|
|
141
|
+
minLength: 1,
|
|
142
|
+
});
|
|
143
|
+
const sidplayFileArgsSchema = objectSchema({
|
|
144
|
+
description: "Parameters for playing a SID file that already resides on the Ultimate filesystem.",
|
|
145
|
+
properties: {
|
|
146
|
+
path: sidFileSchema,
|
|
147
|
+
songnr: optionalSchema(numberSchema({
|
|
148
|
+
description: "Song number within the SID file (0-based index).",
|
|
149
|
+
integer: true,
|
|
150
|
+
minimum: 0,
|
|
151
|
+
})),
|
|
152
|
+
},
|
|
153
|
+
required: ["path"],
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
});
|
|
156
|
+
const modplayFileArgsSchema = objectSchema({
|
|
157
|
+
description: "Parameters for playing a MOD tracker module via the Ultimate SID player.",
|
|
158
|
+
properties: {
|
|
159
|
+
path: sidFileSchema,
|
|
160
|
+
},
|
|
161
|
+
required: ["path"],
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
});
|
|
164
|
+
const musicGenerateArgsSchema = objectSchema({
|
|
165
|
+
description: "Generate a simple arpeggio pattern and schedule playback on voice 1.",
|
|
166
|
+
properties: {
|
|
167
|
+
root: stringSchema({
|
|
168
|
+
description: "Root note for the arpeggio such as C4 or A#3.",
|
|
169
|
+
pattern: NOTE_PATTERN,
|
|
170
|
+
default: "C4",
|
|
171
|
+
}),
|
|
172
|
+
pattern: stringSchema({
|
|
173
|
+
description: "Comma-separated semitone offsets (e.g. '0,4,7').",
|
|
174
|
+
minLength: 1,
|
|
175
|
+
default: "0,4,7",
|
|
176
|
+
}),
|
|
177
|
+
steps: numberSchema({
|
|
178
|
+
description: "Number of notes to schedule.",
|
|
179
|
+
integer: true,
|
|
180
|
+
minimum: 1,
|
|
181
|
+
maximum: 128,
|
|
182
|
+
default: 16,
|
|
183
|
+
}),
|
|
184
|
+
tempoMs: numberSchema({
|
|
185
|
+
description: "Delay in milliseconds between notes.",
|
|
186
|
+
integer: true,
|
|
187
|
+
minimum: 20,
|
|
188
|
+
maximum: 2000,
|
|
189
|
+
default: 120,
|
|
190
|
+
}),
|
|
191
|
+
waveform: stringSchema({
|
|
192
|
+
description: "Waveform for playback.",
|
|
193
|
+
enum: ["pulse", "saw", "tri", "noise"],
|
|
194
|
+
default: "pulse",
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
additionalProperties: false,
|
|
198
|
+
});
|
|
199
|
+
const sidwaveSourceSchema = {
|
|
200
|
+
jsonSchema: {
|
|
201
|
+
description: "SIDWAVE composition represented as YAML/JSON text or an already parsed object.",
|
|
202
|
+
type: ["string", "object"],
|
|
203
|
+
},
|
|
204
|
+
parse(value, path) {
|
|
205
|
+
const resolvedPath = path ?? "$";
|
|
206
|
+
if (typeof value === "string") {
|
|
207
|
+
const trimmed = value.trim();
|
|
208
|
+
if (!trimmed) {
|
|
209
|
+
throw new ToolValidationError("SIDWAVE string must not be empty", { path: resolvedPath });
|
|
210
|
+
}
|
|
211
|
+
return trimmed;
|
|
212
|
+
}
|
|
213
|
+
if (value && typeof value === "object") {
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
throw new ToolValidationError("Expected SIDWAVE string or object", { path: resolvedPath });
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
const musicCompileArgsSchema = objectSchema({
|
|
220
|
+
description: "Compile a SIDWAVE composition and optionally play it immediately.",
|
|
221
|
+
properties: {
|
|
222
|
+
sidwave: optionalSchema(sidwaveSourceSchema),
|
|
223
|
+
cpg: optionalSchema(sidwaveSourceSchema),
|
|
224
|
+
format: optionalSchema(stringSchema({
|
|
225
|
+
description: "Source format hint when providing raw text.",
|
|
226
|
+
enum: ["yaml", "json"],
|
|
227
|
+
})),
|
|
228
|
+
output: optionalSchema(stringSchema({
|
|
229
|
+
description: "Playback target format.",
|
|
230
|
+
enum: ["prg", "sid"],
|
|
231
|
+
default: "prg",
|
|
232
|
+
}), "prg"),
|
|
233
|
+
dryRun: optionalSchema(booleanSchema({
|
|
234
|
+
description: "When true, skip playback and only return compilation metadata.",
|
|
235
|
+
default: false,
|
|
236
|
+
}), false),
|
|
237
|
+
},
|
|
238
|
+
additionalProperties: false,
|
|
239
|
+
});
|
|
240
|
+
const recordAndAnalyzeArgsSchema = objectSchema({
|
|
241
|
+
description: "Record audio from the default input and analyze SID playback characteristics.",
|
|
242
|
+
properties: {
|
|
243
|
+
durationSeconds: numberSchema({
|
|
244
|
+
description: "Capture duration in seconds (0.5 - 30).",
|
|
245
|
+
minimum: 0.5,
|
|
246
|
+
maximum: 30,
|
|
247
|
+
}),
|
|
248
|
+
expectedSidwave: optionalSchema(sidwaveSourceSchema),
|
|
249
|
+
},
|
|
250
|
+
required: ["durationSeconds"],
|
|
251
|
+
additionalProperties: false,
|
|
252
|
+
});
|
|
253
|
+
const analyzeAudioArgsSchema = objectSchema({
|
|
254
|
+
description: "Analyze recent SID playback when a verification-style request is detected.",
|
|
255
|
+
properties: {
|
|
256
|
+
request: stringSchema({
|
|
257
|
+
description: "Natural language user request describing the verification goal.",
|
|
258
|
+
minLength: 1,
|
|
259
|
+
}),
|
|
260
|
+
durationSeconds: optionalSchema(numberSchema({
|
|
261
|
+
description: "Override capture duration in seconds.",
|
|
262
|
+
minimum: 0.5,
|
|
263
|
+
maximum: 30,
|
|
264
|
+
})),
|
|
265
|
+
expectedSidwave: optionalSchema(sidwaveSourceSchema),
|
|
266
|
+
},
|
|
267
|
+
required: ["request"],
|
|
268
|
+
additionalProperties: false,
|
|
269
|
+
});
|
|
270
|
+
function parseIntervals(pattern) {
|
|
271
|
+
const intervals = pattern
|
|
272
|
+
.split(/[\s,]+/)
|
|
273
|
+
.map((part) => Number.parseInt(part, 10))
|
|
274
|
+
.filter((value) => Number.isFinite(value));
|
|
275
|
+
if (intervals.length === 0) {
|
|
276
|
+
throw new ToolValidationError("Pattern must contain at least one semitone offset", { path: "$.pattern" });
|
|
277
|
+
}
|
|
278
|
+
return intervals;
|
|
279
|
+
}
|
|
280
|
+
function transposeNote(note, semitones) {
|
|
281
|
+
const match = NOTE_PATTERN.exec((note ?? "").trim());
|
|
282
|
+
if (!match) {
|
|
283
|
+
return note;
|
|
284
|
+
}
|
|
285
|
+
const [, letterRaw, accidentalRaw, octaveRaw] = match;
|
|
286
|
+
const letter = (letterRaw ?? "C").toUpperCase();
|
|
287
|
+
const accidental = accidentalRaw ?? "";
|
|
288
|
+
const octave = Number(octaveRaw ?? "0");
|
|
289
|
+
const base = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
290
|
+
let midi = (octave + 1) * 12 + (base[letter] ?? 0);
|
|
291
|
+
if (accidental === "#")
|
|
292
|
+
midi += 1;
|
|
293
|
+
if (accidental === "b")
|
|
294
|
+
midi -= 1;
|
|
295
|
+
midi += semitones;
|
|
296
|
+
const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
297
|
+
const wrapped = ((midi % 12) + 12) % 12;
|
|
298
|
+
const newOctave = Math.floor(midi / 12) - 1;
|
|
299
|
+
return `${names[wrapped]}${newOctave}`;
|
|
300
|
+
}
|
|
301
|
+
function sleep(ms) {
|
|
302
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
303
|
+
}
|
|
304
|
+
function shouldAutoAnalyze(request) {
|
|
305
|
+
const lowered = request.toLowerCase();
|
|
306
|
+
const actionMatch = /(check|verify|test|analyze|listen|hear)/.test(lowered);
|
|
307
|
+
const subjectMatch = /(sid|audio|music|sound|song|play)/.test(lowered);
|
|
308
|
+
const qualitativeMatch = /(does.*sound|how.*sound|sound.*right|sound.*good|sound.*correct)/.test(lowered);
|
|
309
|
+
return (actionMatch && subjectMatch) || qualitativeMatch;
|
|
310
|
+
}
|
|
311
|
+
function normaliseSidwaveInput(input) {
|
|
312
|
+
if (typeof input === "string") {
|
|
313
|
+
const trimmed = input.trim();
|
|
314
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
315
|
+
}
|
|
316
|
+
if (input && typeof input === "object") {
|
|
317
|
+
return input;
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
function generateAudioFeedback(analysisResult, userRequest) {
|
|
322
|
+
try {
|
|
323
|
+
const analysis = analysisResult?.analysis;
|
|
324
|
+
if (!analysis) {
|
|
325
|
+
return "Audio analysis completed but no musical content was detected. Ensure the C64 is playing audio.";
|
|
326
|
+
}
|
|
327
|
+
const voices = Array.isArray(analysis.voices) ? analysis.voices : [];
|
|
328
|
+
const globalMetrics = (analysis.global_metrics ?? {});
|
|
329
|
+
let feedback = `Audio analysis detected ${voices.length} voice(s) over ${Math.round((analysis.durationSeconds ?? 0) * 10) / 10}s:\n\n`;
|
|
330
|
+
voices.forEach((voice, index) => {
|
|
331
|
+
const notes = Array.isArray(voice?.detected_notes) ? voice.detected_notes : [];
|
|
332
|
+
const validNotes = notes.filter((n) => n?.note && n?.frequency);
|
|
333
|
+
const voiceId = voice?.id ?? index + 1;
|
|
334
|
+
if (validNotes.length > 0) {
|
|
335
|
+
const preview = validNotes.slice(0, 5).map((n) => `${n.note}(${Math.round(n.frequency)}Hz)`);
|
|
336
|
+
const extra = validNotes.length > preview.length ? "..." : "";
|
|
337
|
+
feedback += `Voice ${voiceId}: ${validNotes.length} note(s) - ${preview.join(", ")}${extra}`;
|
|
338
|
+
if (typeof voice?.average_deviation === "number") {
|
|
339
|
+
feedback += ` [avg deviation: ${Math.round(voice.average_deviation * 10) / 10} cents]`;
|
|
340
|
+
}
|
|
341
|
+
feedback += "\n";
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
feedback += `Voice ${voiceId}: No clear notes detected\n`;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
if (typeof globalMetrics.average_pitch_deviation === "number") {
|
|
348
|
+
feedback += `\nOverall pitch accuracy: ${Math.round(globalMetrics.average_pitch_deviation * 10) / 10} cents deviation`;
|
|
349
|
+
}
|
|
350
|
+
if (typeof globalMetrics.detected_bpm === "number") {
|
|
351
|
+
feedback += `\nDetected tempo: ${Math.round(globalMetrics.detected_bpm)} BPM`;
|
|
352
|
+
}
|
|
353
|
+
if (/(sound.*right|sound.*good|sound.*correct)/.test(userRequest.toLowerCase())) {
|
|
354
|
+
const deviation = Math.abs(globalMetrics.average_pitch_deviation ?? 0);
|
|
355
|
+
if (deviation < 20) {
|
|
356
|
+
feedback += "\n\n✓ The music sounds accurate with good pitch stability.";
|
|
357
|
+
}
|
|
358
|
+
else if (deviation < 50) {
|
|
359
|
+
feedback += "\n\n⚠ The music has some pitch variation but is generally recognizable.";
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
feedback += "\n\n✗ The music shows significant pitch deviation - check SID programming or playback.";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return feedback;
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
369
|
+
return `Audio feedback generation failed: ${message}`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
export const audioModule = defineToolModule({
|
|
373
|
+
domain: "audio",
|
|
374
|
+
summary: "SID composition, playback, and audio analysis workflows.",
|
|
375
|
+
resources: [
|
|
376
|
+
"c64://specs/sid",
|
|
377
|
+
"c64://specs/sidwave",
|
|
378
|
+
"c64://docs/sid/file-structure",
|
|
379
|
+
],
|
|
380
|
+
prompts: ["sid-music"],
|
|
381
|
+
defaultTags: ["sid", "audio"],
|
|
382
|
+
workflowHints: [
|
|
383
|
+
"Reach for SID helpers when the user talks about sound design, playback quality, or stuck notes.",
|
|
384
|
+
"After changing playback state, suggest verify-by-ear steps such as analyze_audio so the user gets concrete feedback.",
|
|
385
|
+
],
|
|
386
|
+
tools: [
|
|
387
|
+
{
|
|
388
|
+
name: "sid_volume",
|
|
389
|
+
description: "Set the SID master volume register at $D418. See c64://specs/sid.",
|
|
390
|
+
summary: "Clamps the requested volume and writes it to the SID master volume register.",
|
|
391
|
+
inputSchema: sidVolumeArgsSchema.jsonSchema,
|
|
392
|
+
tags: ["sid", "control"],
|
|
393
|
+
prerequisites: [],
|
|
394
|
+
examples: [
|
|
395
|
+
{ name: "Set volume", description: "Volume 12", arguments: { volume: 12 } },
|
|
396
|
+
],
|
|
397
|
+
workflowHints: [
|
|
398
|
+
"Adjust volume when the user mentions level issues and remind them the valid range is 0-15.",
|
|
399
|
+
"Offer to run analyze_audio if the listener still cannot confirm the change.",
|
|
400
|
+
],
|
|
401
|
+
async execute(args, ctx) {
|
|
402
|
+
try {
|
|
403
|
+
const parsed = sidVolumeArgsSchema.parse(args ?? {});
|
|
404
|
+
const appliedVolume = Math.max(0, Math.min(15, Math.floor(parsed.volume)));
|
|
405
|
+
ctx.logger.info("Setting SID master volume", {
|
|
406
|
+
requested: parsed.volume,
|
|
407
|
+
applied: appliedVolume,
|
|
408
|
+
});
|
|
409
|
+
const result = await ctx.client.sidSetVolume(parsed.volume);
|
|
410
|
+
if (!result.success) {
|
|
411
|
+
throw new ToolExecutionError("C64 firmware reported failure while setting SID volume", {
|
|
412
|
+
details: normaliseFailure(result.details),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const detailRecord = toRecord(result.details) ?? {};
|
|
416
|
+
const address = normaliseAddress(detailRecord.address, "$D418");
|
|
417
|
+
return textResult(`SID master volume set to ${appliedVolume}.`, {
|
|
418
|
+
success: true,
|
|
419
|
+
requestedVolume: parsed.volume,
|
|
420
|
+
appliedVolume,
|
|
421
|
+
address,
|
|
422
|
+
bytes: detailRecord.bytes ?? null,
|
|
423
|
+
details: detailRecord,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
if (error instanceof ToolError) {
|
|
428
|
+
return toolErrorResult(error);
|
|
429
|
+
}
|
|
430
|
+
return unknownErrorResult(error);
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: "sid_reset",
|
|
436
|
+
description: "Reset the SID chip either softly (silence) or with a full register scrub.",
|
|
437
|
+
summary: "Invokes the Ultimate firmware to silence or fully reset SID registers.",
|
|
438
|
+
inputSchema: sidResetArgsSchema.jsonSchema,
|
|
439
|
+
tags: ["sid", "control"],
|
|
440
|
+
prerequisites: [],
|
|
441
|
+
examples: [
|
|
442
|
+
{ name: "Hard reset", description: "Full register scrub", arguments: { hard: true } },
|
|
443
|
+
],
|
|
444
|
+
workflowHints: [
|
|
445
|
+
"Use after glitches or hanging notes and tell the user whether you performed a soft or hard reset.",
|
|
446
|
+
],
|
|
447
|
+
async execute(args, ctx) {
|
|
448
|
+
try {
|
|
449
|
+
const parsed = sidResetArgsSchema.parse(args ?? {});
|
|
450
|
+
ctx.logger.info("Resetting SID", { mode: parsed.hard ? "hard" : "soft" });
|
|
451
|
+
const result = await ctx.client.sidReset(parsed.hard);
|
|
452
|
+
if (!result.success) {
|
|
453
|
+
throw new ToolExecutionError("C64 firmware reported failure while resetting SID", {
|
|
454
|
+
details: normaliseFailure(result.details),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const detailRecord = toRecord(result.details) ?? {};
|
|
458
|
+
return textResult(`SID ${parsed.hard ? "hard" : "soft"} reset completed.`, {
|
|
459
|
+
success: true,
|
|
460
|
+
mode: parsed.hard ? "hard" : "soft",
|
|
461
|
+
details: detailRecord,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
if (error instanceof ToolError) {
|
|
466
|
+
return toolErrorResult(error);
|
|
467
|
+
}
|
|
468
|
+
return unknownErrorResult(error);
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: "sid_note_on",
|
|
474
|
+
description: "Trigger a SID voice with configurable waveform, pulse width, and ADSR envelope. See c64://specs/sid.",
|
|
475
|
+
summary: "Resolves note or frequency, clamps parameters, and writes the SID voice registers.",
|
|
476
|
+
inputSchema: sidNoteOnArgsSchema.jsonSchema,
|
|
477
|
+
tags: ["sid", "control", "music"],
|
|
478
|
+
prerequisites: ["sid_volume"],
|
|
479
|
+
examples: [
|
|
480
|
+
{ name: "Note on", description: "Voice 1 C4 triangle", arguments: { voice: 1, note: "C4", waveform: "tri" } },
|
|
481
|
+
],
|
|
482
|
+
workflowHints: [
|
|
483
|
+
"Invoke when the user wants to audition a single voice; summarise waveform, ADSR, and pitch afterwards.",
|
|
484
|
+
"Translate note descriptions into frequencies yourself if the request is ambiguous.",
|
|
485
|
+
],
|
|
486
|
+
async execute(args, ctx) {
|
|
487
|
+
try {
|
|
488
|
+
const parsed = sidNoteOnArgsSchema.parse(args ?? {});
|
|
489
|
+
const voice = Math.max(1, Math.min(3, Math.floor(parsed.voice ?? 1)));
|
|
490
|
+
ctx.logger.info("Starting SID voice", {
|
|
491
|
+
voice,
|
|
492
|
+
note: parsed.note ?? null,
|
|
493
|
+
frequencyHz: parsed.frequencyHz ?? null,
|
|
494
|
+
waveform: parsed.waveform,
|
|
495
|
+
});
|
|
496
|
+
const result = await ctx.client.sidNoteOn({
|
|
497
|
+
voice,
|
|
498
|
+
note: parsed.note ?? undefined,
|
|
499
|
+
frequencyHz: parsed.frequencyHz ?? undefined,
|
|
500
|
+
system: (parsed.system ?? "PAL"),
|
|
501
|
+
waveform: (parsed.waveform ?? "pulse"),
|
|
502
|
+
pulseWidth: parsed.pulseWidth,
|
|
503
|
+
attack: parsed.attack,
|
|
504
|
+
decay: parsed.decay,
|
|
505
|
+
sustain: parsed.sustain,
|
|
506
|
+
release: parsed.release,
|
|
507
|
+
});
|
|
508
|
+
if (!result.success) {
|
|
509
|
+
throw new ToolExecutionError("C64 firmware reported failure while starting SID voice", {
|
|
510
|
+
details: normaliseFailure(result.details),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const detailRecord = toRecord(result.details) ?? {};
|
|
514
|
+
return textResult(`Started SID voice ${voice}${parsed.note ? ` with note ${parsed.note.toUpperCase()}` : ""}.`, {
|
|
515
|
+
success: true,
|
|
516
|
+
voice,
|
|
517
|
+
note: parsed.note ?? null,
|
|
518
|
+
frequencyHz: parsed.frequencyHz ?? null,
|
|
519
|
+
system: parsed.system ?? "PAL",
|
|
520
|
+
waveform: parsed.waveform ?? "pulse",
|
|
521
|
+
pulseWidth: parsed.pulseWidth,
|
|
522
|
+
envelope: {
|
|
523
|
+
attack: parsed.attack,
|
|
524
|
+
decay: parsed.decay,
|
|
525
|
+
sustain: parsed.sustain,
|
|
526
|
+
release: parsed.release,
|
|
527
|
+
},
|
|
528
|
+
details: detailRecord,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
if (error instanceof ToolError) {
|
|
533
|
+
return toolErrorResult(error);
|
|
534
|
+
}
|
|
535
|
+
return unknownErrorResult(error);
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: "sid_note_off",
|
|
541
|
+
description: "Release a SID voice by clearing its GATE bit.",
|
|
542
|
+
summary: "Writes zero to the selected voice control register to stop playback.",
|
|
543
|
+
inputSchema: sidNoteOffArgsSchema.jsonSchema,
|
|
544
|
+
tags: ["sid", "control", "music"],
|
|
545
|
+
prerequisites: ["sid_note_on"],
|
|
546
|
+
examples: [
|
|
547
|
+
{ name: "Note off", description: "Release voice 1", arguments: { voice: 1 } },
|
|
548
|
+
],
|
|
549
|
+
workflowHints: [
|
|
550
|
+
"Stop individual voices after prior note_on calls and confirm which voice you released.",
|
|
551
|
+
],
|
|
552
|
+
async execute(args, ctx) {
|
|
553
|
+
try {
|
|
554
|
+
const parsed = sidNoteOffArgsSchema.parse(args ?? {});
|
|
555
|
+
const voice = Math.max(1, Math.min(3, Math.floor(parsed.voice)));
|
|
556
|
+
ctx.logger.info("Stopping SID voice", { voice });
|
|
557
|
+
const result = await ctx.client.sidNoteOff(voice);
|
|
558
|
+
if (!result.success) {
|
|
559
|
+
throw new ToolExecutionError("C64 firmware reported failure while stopping SID voice", {
|
|
560
|
+
details: normaliseFailure(result.details),
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const detailRecord = toRecord(result.details) ?? {};
|
|
564
|
+
return textResult(`Released SID voice ${voice}.`, {
|
|
565
|
+
success: true,
|
|
566
|
+
voice,
|
|
567
|
+
details: detailRecord,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
if (error instanceof ToolError) {
|
|
572
|
+
return toolErrorResult(error);
|
|
573
|
+
}
|
|
574
|
+
return unknownErrorResult(error);
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "sid_silence_all",
|
|
580
|
+
description: "Silence all SID voices by clearing control and envelope registers.",
|
|
581
|
+
summary: "Performs a soft reset of SID voices, ensuring audio output stops.",
|
|
582
|
+
inputSchema: sidSilenceArgsSchema.jsonSchema,
|
|
583
|
+
tags: ["sid", "control"],
|
|
584
|
+
prerequisites: [],
|
|
585
|
+
examples: [
|
|
586
|
+
{ name: "Silence", description: "Stop all voices", arguments: {} },
|
|
587
|
+
],
|
|
588
|
+
workflowHints: [
|
|
589
|
+
"Run when the user asks to stop all audio or before switching to a new composition.",
|
|
590
|
+
],
|
|
591
|
+
async execute(args, ctx) {
|
|
592
|
+
try {
|
|
593
|
+
sidSilenceArgsSchema.parse(args ?? {});
|
|
594
|
+
ctx.logger.info("Silencing all SID voices");
|
|
595
|
+
const result = await ctx.client.sidSilenceAll();
|
|
596
|
+
if (!result.success) {
|
|
597
|
+
throw new ToolExecutionError("C64 firmware reported failure while silencing SID", {
|
|
598
|
+
details: normaliseFailure(result.details),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
const detailRecord = toRecord(result.details) ?? {};
|
|
602
|
+
return textResult("SID voices silenced.", {
|
|
603
|
+
success: true,
|
|
604
|
+
details: detailRecord,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
if (error instanceof ToolError) {
|
|
609
|
+
return toolErrorResult(error);
|
|
610
|
+
}
|
|
611
|
+
return unknownErrorResult(error);
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: "sidplay_file",
|
|
617
|
+
description: "Play a SID file stored on the Ultimate filesystem via the firmware player.",
|
|
618
|
+
summary: "Instructs the SID player to load the given file and optional song number.",
|
|
619
|
+
inputSchema: sidplayFileArgsSchema.jsonSchema,
|
|
620
|
+
relatedResources: ["c64://specs/sid"],
|
|
621
|
+
tags: ["sid", "playback"],
|
|
622
|
+
prerequisites: ["drives_list"],
|
|
623
|
+
examples: [
|
|
624
|
+
{ name: "Play SID", description: "song 0", arguments: { path: "//USB0/tune.sid", songnr: 0 } },
|
|
625
|
+
],
|
|
626
|
+
workflowHints: [
|
|
627
|
+
"Use when the user references an existing SID file path; surface song numbers if the firmware reports them.",
|
|
628
|
+
],
|
|
629
|
+
async execute(args, ctx) {
|
|
630
|
+
try {
|
|
631
|
+
const parsed = sidplayFileArgsSchema.parse(args ?? {});
|
|
632
|
+
ctx.logger.info("Playing SID file", { path: parsed.path, songnr: parsed.songnr ?? null });
|
|
633
|
+
const result = await ctx.client.sidplayFile(parsed.path, parsed.songnr);
|
|
634
|
+
if (!result.success) {
|
|
635
|
+
throw new ToolExecutionError("C64 firmware reported failure while starting SID playback", {
|
|
636
|
+
details: normaliseFailure(result.details),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const details = toRecord(result.details) ?? {};
|
|
640
|
+
return textResult(`SID file ${parsed.path} queued for playback.`, {
|
|
641
|
+
success: true,
|
|
642
|
+
path: parsed.path,
|
|
643
|
+
songnr: parsed.songnr ?? null,
|
|
644
|
+
details,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
if (error instanceof ToolError) {
|
|
649
|
+
return toolErrorResult(error);
|
|
650
|
+
}
|
|
651
|
+
return unknownErrorResult(error);
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: "modplay_file",
|
|
657
|
+
description: "Play a MOD tracker module stored on the Ultimate filesystem.",
|
|
658
|
+
summary: "Sends the module to the Ultimate SID player and starts playback.",
|
|
659
|
+
inputSchema: modplayFileArgsSchema.jsonSchema,
|
|
660
|
+
relatedResources: ["c64://specs/sid"],
|
|
661
|
+
tags: ["sid", "playback"],
|
|
662
|
+
prerequisites: ["drives_list"],
|
|
663
|
+
examples: [
|
|
664
|
+
{ name: "Play MOD", description: "module.mod", arguments: { path: "//USB0/module.mod" } },
|
|
665
|
+
],
|
|
666
|
+
workflowHints: [
|
|
667
|
+
"Trigger for MOD playback requests and confirm the module path is reachable on Ultimate storage.",
|
|
668
|
+
],
|
|
669
|
+
async execute(args, ctx) {
|
|
670
|
+
try {
|
|
671
|
+
const parsed = modplayFileArgsSchema.parse(args ?? {});
|
|
672
|
+
ctx.logger.info("Playing MOD file", { path: parsed.path });
|
|
673
|
+
const result = await ctx.client.modplayFile(parsed.path);
|
|
674
|
+
if (!result.success) {
|
|
675
|
+
throw new ToolExecutionError("C64 firmware reported failure while starting MOD playback", {
|
|
676
|
+
details: normaliseFailure(result.details),
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const details = toRecord(result.details) ?? {};
|
|
680
|
+
return textResult(`MOD file ${parsed.path} queued for playback.`, {
|
|
681
|
+
success: true,
|
|
682
|
+
path: parsed.path,
|
|
683
|
+
details,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
if (error instanceof ToolError) {
|
|
688
|
+
return toolErrorResult(error);
|
|
689
|
+
}
|
|
690
|
+
return unknownErrorResult(error);
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: "music_generate",
|
|
696
|
+
description: "Generate a lightweight arpeggio and schedule playback on SID voice 1.",
|
|
697
|
+
summary: "Builds a note timeline from semitone offsets and triggers SID playback asynchronously.",
|
|
698
|
+
lifecycle: "fire-and-forget",
|
|
699
|
+
inputSchema: musicGenerateArgsSchema.jsonSchema,
|
|
700
|
+
relatedResources: ["c64://specs/sid"],
|
|
701
|
+
relatedPrompts: ["sid-music"],
|
|
702
|
+
tags: ["sid", "music", "generator"],
|
|
703
|
+
prerequisites: ["sid_volume"],
|
|
704
|
+
examples: [
|
|
705
|
+
{ name: "C major", description: "C4 arpeggio", arguments: { root: "C4", pattern: "0,4,7", steps: 8, tempoMs: 120, waveform: "pulse" } },
|
|
706
|
+
],
|
|
707
|
+
workflowHints: [
|
|
708
|
+
"Offer as a quick inspiration loop when the user wants to hear something immediately; explain how to tweak pattern or tempo.",
|
|
709
|
+
],
|
|
710
|
+
async execute(args, ctx) {
|
|
711
|
+
try {
|
|
712
|
+
const parsed = musicGenerateArgsSchema.parse(args ?? {});
|
|
713
|
+
const intervals = parseIntervals(parsed.pattern);
|
|
714
|
+
const timeline = [];
|
|
715
|
+
let timestamp = 0;
|
|
716
|
+
for (let i = 0; i < parsed.steps; i += 1) {
|
|
717
|
+
const iv = intervals[i % intervals.length];
|
|
718
|
+
const note = transposeNote(parsed.root, iv);
|
|
719
|
+
timeline.push({ t: timestamp, note });
|
|
720
|
+
timestamp += parsed.tempoMs;
|
|
721
|
+
}
|
|
722
|
+
ctx.logger.info("Scheduling SID arpeggio", {
|
|
723
|
+
root: parsed.root,
|
|
724
|
+
intervals,
|
|
725
|
+
steps: parsed.steps,
|
|
726
|
+
tempoMs: parsed.tempoMs,
|
|
727
|
+
waveform: parsed.waveform,
|
|
728
|
+
});
|
|
729
|
+
void (async () => {
|
|
730
|
+
try {
|
|
731
|
+
await ctx.client.sidSetVolume(8);
|
|
732
|
+
for (let i = 0; i < parsed.steps; i += 1) {
|
|
733
|
+
const iv = intervals[i % intervals.length];
|
|
734
|
+
const note = transposeNote(parsed.root, iv);
|
|
735
|
+
await ctx.client.sidNoteOn({
|
|
736
|
+
voice: 1,
|
|
737
|
+
note,
|
|
738
|
+
waveform: parsed.waveform,
|
|
739
|
+
pulseWidth: 0x0800,
|
|
740
|
+
attack: 1,
|
|
741
|
+
decay: 2,
|
|
742
|
+
sustain: 8,
|
|
743
|
+
release: 3,
|
|
744
|
+
});
|
|
745
|
+
await sleep(parsed.tempoMs);
|
|
746
|
+
}
|
|
747
|
+
await ctx.client.sidNoteOff(1);
|
|
748
|
+
}
|
|
749
|
+
catch (playbackError) {
|
|
750
|
+
const message = playbackError instanceof Error ? playbackError.message : String(playbackError);
|
|
751
|
+
ctx.logger.warn("music_generate playback failed", { error: message });
|
|
752
|
+
}
|
|
753
|
+
})();
|
|
754
|
+
return textResult(`Scheduled ${parsed.steps} note arpeggio starting on ${parsed.root}.`, {
|
|
755
|
+
success: true,
|
|
756
|
+
root: parsed.root,
|
|
757
|
+
intervals,
|
|
758
|
+
steps: parsed.steps,
|
|
759
|
+
tempoMs: parsed.tempoMs,
|
|
760
|
+
waveform: parsed.waveform,
|
|
761
|
+
timeline,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
if (error instanceof ToolError) {
|
|
766
|
+
return toolErrorResult(error);
|
|
767
|
+
}
|
|
768
|
+
return unknownErrorResult(error);
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
name: "music_compile_and_play",
|
|
774
|
+
description: "Compile a SIDWAVE composition to PRG or SID and optionally play it immediately.",
|
|
775
|
+
summary: "Parses SIDWAVE/CPG input, compiles to a PRG, and plays via PRG or SID attachment.",
|
|
776
|
+
inputSchema: musicCompileArgsSchema.jsonSchema,
|
|
777
|
+
relatedResources: ["c64://specs/sid", "c64://specs/sidwave", "c64://docs/sid/file-structure"],
|
|
778
|
+
relatedPrompts: ["sid-music"],
|
|
779
|
+
tags: ["sid", "music", "compiler"],
|
|
780
|
+
prerequisites: [],
|
|
781
|
+
examples: [
|
|
782
|
+
{ name: "Compile PRG", description: "Play compiled PRG", arguments: { sidwave: "song: { title: 'Demo' }", output: "prg", dryRun: false } },
|
|
783
|
+
],
|
|
784
|
+
workflowHints: [
|
|
785
|
+
"Use when the user provides SIDWAVE/CPG scores or asks for export to PRG/SID; share any generated download URIs in your response.",
|
|
786
|
+
"Suggest analyze_audio afterwards if they want objective feedback on the compiled performance.",
|
|
787
|
+
],
|
|
788
|
+
async execute(args, ctx) {
|
|
789
|
+
try {
|
|
790
|
+
const parsed = musicCompileArgsSchema.parse(args ?? {});
|
|
791
|
+
const source = normaliseSidwaveInput(parsed.sidwave) ?? normaliseSidwaveInput(parsed.cpg);
|
|
792
|
+
if (!source) {
|
|
793
|
+
throw new ToolValidationError("Provide sidwave or cpg source", { path: "$.sidwave" });
|
|
794
|
+
}
|
|
795
|
+
const outputFormat = parsed.output ?? "prg";
|
|
796
|
+
ctx.logger.info("Compiling SIDWAVE composition", {
|
|
797
|
+
output: outputFormat,
|
|
798
|
+
dryRun: parsed.dryRun,
|
|
799
|
+
});
|
|
800
|
+
const document = parseSidwave(source);
|
|
801
|
+
const compiled = compileSidwaveToPrg(document);
|
|
802
|
+
let ranOnC64 = false;
|
|
803
|
+
let runDetails = null;
|
|
804
|
+
if (!parsed.dryRun) {
|
|
805
|
+
if (outputFormat === "sid") {
|
|
806
|
+
const sid = compileSidwaveToSid(document, compiled.prg, { entryAddress: compiled.entryAddress });
|
|
807
|
+
const result = await ctx.client.sidplayAttachment(sid.sid);
|
|
808
|
+
if (!result.success) {
|
|
809
|
+
throw new ToolExecutionError("C64 firmware reported failure while playing compiled SID", {
|
|
810
|
+
details: normaliseFailure(result.details),
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
ranOnC64 = true;
|
|
814
|
+
runDetails = toRecord(result.details) ?? null;
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
const result = await ctx.client.runPrg(compiled.prg);
|
|
818
|
+
if (!result.success) {
|
|
819
|
+
throw new ToolExecutionError("C64 firmware reported failure while running compiled PRG", {
|
|
820
|
+
details: normaliseFailure(result.details),
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
ranOnC64 = true;
|
|
824
|
+
runDetails = toRecord(result.details) ?? null;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const message = `Compiled '${document.song.title}' to ${outputFormat.toUpperCase()}${parsed.dryRun ? " (dry run)" : ""}.`;
|
|
828
|
+
const voiceSummaries = document.voices.map((voice) => ({
|
|
829
|
+
id: voice.id,
|
|
830
|
+
name: voice.name,
|
|
831
|
+
waveform: voice.waveform,
|
|
832
|
+
pulse_width: voice.pulse_width,
|
|
833
|
+
adsr: voice.adsr,
|
|
834
|
+
}));
|
|
835
|
+
return textResult(message, {
|
|
836
|
+
success: true,
|
|
837
|
+
ranOnC64,
|
|
838
|
+
dryRun: parsed.dryRun,
|
|
839
|
+
format: outputFormat,
|
|
840
|
+
prgSize: compiled.prg.length,
|
|
841
|
+
entryAddress: compiled.entryAddress,
|
|
842
|
+
song: document.song,
|
|
843
|
+
voices: voiceSummaries,
|
|
844
|
+
runDetails,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
if (error instanceof ToolError) {
|
|
849
|
+
return toolErrorResult(error);
|
|
850
|
+
}
|
|
851
|
+
return unknownErrorResult(error);
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
name: "record_and_analyze_audio",
|
|
857
|
+
description: "Record audio from the default input device and analyze SID playback characteristics.",
|
|
858
|
+
summary: "Captures PCM data, extracts notes, tempo, and deviation metrics for SID verification workflows.",
|
|
859
|
+
inputSchema: recordAndAnalyzeArgsSchema.jsonSchema,
|
|
860
|
+
relatedResources: ["c64://specs/sid", "c64://docs/sid/file-structure"],
|
|
861
|
+
relatedPrompts: ["sid-music"],
|
|
862
|
+
tags: ["sid", "analysis"],
|
|
863
|
+
prerequisites: [],
|
|
864
|
+
examples: [
|
|
865
|
+
{ name: "Analyze 3s", description: "Quick capture", arguments: { durationSeconds: 3 } },
|
|
866
|
+
],
|
|
867
|
+
workflowHints: [
|
|
868
|
+
"Recommend after playback when the user wants tuning confirmation; mention capture duration in your summary.",
|
|
869
|
+
"If the user sounded uncertain about audio quality, run this proactively for objective metrics.",
|
|
870
|
+
],
|
|
871
|
+
async execute(args, ctx) {
|
|
872
|
+
try {
|
|
873
|
+
const parsed = recordAndAnalyzeArgsSchema.parse(args ?? {});
|
|
874
|
+
ctx.logger.info("Recording audio for analysis", { durationSeconds: parsed.durationSeconds });
|
|
875
|
+
const result = await recordAndAnalyzeAudio({
|
|
876
|
+
durationSeconds: parsed.durationSeconds,
|
|
877
|
+
expectedSidwave: normaliseSidwaveInput(parsed.expectedSidwave),
|
|
878
|
+
});
|
|
879
|
+
return jsonResult(result, {
|
|
880
|
+
success: true,
|
|
881
|
+
durationSeconds: result.analysis?.durationSeconds ?? parsed.durationSeconds,
|
|
882
|
+
voices: result.analysis?.voices ?? [],
|
|
883
|
+
globalMetrics: result.analysis?.global_metrics ?? {},
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
if (error instanceof ToolError) {
|
|
888
|
+
return toolErrorResult(error);
|
|
889
|
+
}
|
|
890
|
+
const executionError = new ToolExecutionError(error instanceof Error ? error.message : String(error), { cause: error instanceof Error ? error : undefined });
|
|
891
|
+
return toolErrorResult(executionError);
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
name: "analyze_audio",
|
|
897
|
+
description: "Automatically analyze SID playback when the user requests verification feedback.",
|
|
898
|
+
summary: "Detects verification intent in natural language, records a short clip, and returns musical feedback.",
|
|
899
|
+
inputSchema: analyzeAudioArgsSchema.jsonSchema,
|
|
900
|
+
relatedResources: ["c64://specs/sid", "c64://docs/sid/file-structure"],
|
|
901
|
+
relatedPrompts: ["sid-music"],
|
|
902
|
+
tags: ["sid", "analysis"],
|
|
903
|
+
prerequisites: [],
|
|
904
|
+
examples: [
|
|
905
|
+
{ name: "Check music", description: "Verify by ear", arguments: { request: "does the music sound right?" } },
|
|
906
|
+
],
|
|
907
|
+
workflowHints: [
|
|
908
|
+
"Invoke when the user asks to check or verify the music so you can return measured results.",
|
|
909
|
+
"Translate the analysis into concrete next steps like ADSR or tempo tweaks in your response.",
|
|
910
|
+
],
|
|
911
|
+
async execute(args, ctx) {
|
|
912
|
+
try {
|
|
913
|
+
const parsed = analyzeAudioArgsSchema.parse(args ?? {});
|
|
914
|
+
if (!shouldAutoAnalyze(parsed.request)) {
|
|
915
|
+
return textResult("No audio verification keywords detected. Use phrases like 'check the music' or 'verify the SID playback'.", {
|
|
916
|
+
analyzed: false,
|
|
917
|
+
request: parsed.request,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
const duration = parsed.durationSeconds ?? 3;
|
|
921
|
+
ctx.logger.info("Auto-analyzing SID audio", { durationSeconds: duration });
|
|
922
|
+
const analysis = await recordAndAnalyzeAudio({
|
|
923
|
+
durationSeconds: duration,
|
|
924
|
+
expectedSidwave: normaliseSidwaveInput(parsed.expectedSidwave),
|
|
925
|
+
});
|
|
926
|
+
const feedback = generateAudioFeedback(analysis, parsed.request);
|
|
927
|
+
return textResult(feedback, {
|
|
928
|
+
analyzed: true,
|
|
929
|
+
request: parsed.request,
|
|
930
|
+
durationSeconds: analysis.analysis?.durationSeconds ?? duration,
|
|
931
|
+
analysis,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
if (error instanceof ToolError) {
|
|
936
|
+
return toolErrorResult(error);
|
|
937
|
+
}
|
|
938
|
+
const executionError = new ToolExecutionError(error instanceof Error ? error.message : String(error), { cause: error instanceof Error ? error : undefined });
|
|
939
|
+
return toolErrorResult(executionError);
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
});
|