c64bridge 1.0.5 → 1.0.6

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 (46) hide show
  1. package/README.md +2 -2
  2. package/data/sound/sidwave.md +113 -36
  3. package/data/sound/sidwave.schema.json +138 -28
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/c64Client.js +83 -93
  6. package/dist/config.js +21 -4
  7. package/dist/device.js +130 -24
  8. package/dist/diagnostics.js +10 -2
  9. package/dist/mcp-server.js +12 -4
  10. package/dist/sidwave.js +453 -61
  11. package/dist/sidwaveCompiler.js +1424 -269
  12. package/dist/tools/audio.js +70 -12
  13. package/dist/tools/developer.js +7 -7
  14. package/dist/tools/errors.js +7 -0
  15. package/dist/tools/graphics.js +4 -4
  16. package/dist/tools/input.js +8 -11
  17. package/dist/tools/machineControl.js +6 -6
  18. package/dist/tools/memory.js +6 -6
  19. package/dist/tools/meta/audio.js +8 -2
  20. package/dist/tools/meta/graphics.js +1 -1
  21. package/dist/tools/meta/memory.js +4 -4
  22. package/dist/tools/meta/program.js +12 -8
  23. package/dist/tools/printer.js +4 -4
  24. package/dist/tools/programRunners.js +8 -8
  25. package/dist/tools/registry/disk.js +7 -5
  26. package/dist/tools/storage.js +21 -17
  27. package/dist/tools/streaming.js +2 -2
  28. package/dist/tools/translation/assembler.js +44 -12
  29. package/dist/tools/translation/basicTokenizer.js +5 -1
  30. package/dist/tools/translation/opcodeInventory.js +1 -1
  31. package/dist/tools/vice.js +1 -1
  32. package/dist/vice/mockServer.js +92 -5
  33. package/dist/vice/readiness.js +4 -9
  34. package/dist/vice/viceClient.js +18 -0
  35. package/mcp/schemas/c64_disk.schema.json +2 -2
  36. package/mcp/schemas/c64_input.schema.json +3 -3
  37. package/mcp/schemas/c64_program.schema.json +2 -2
  38. package/mcp/schemas/c64_vice.schema.json +3 -3
  39. package/mcp/server.json +1 -1
  40. package/mcp/tools.json +14 -10
  41. package/mcp.json +1 -1
  42. package/package.json +1 -1
  43. package/scripts/mockC64Server.mjs +15 -1
  44. package/scripts/run-coverage.mjs +31 -1
  45. package/scripts/update-readme.ts +3 -2
  46. package/server.json +2 -2
@@ -1,5 +1,5 @@
1
1
  import { compileSidwaveToPrg, compileSidwaveToSid } from "../sidwaveCompiler.js";
2
- import { parseSidwave } from "../sidwave.js";
2
+ import { midiToHz, noteNameToMidi, parseSidwave } from "../sidwave.js";
3
3
  import { recordAndAnalyzeAudio } from "../audio/record_and_analyze_audio.js";
4
4
  import { defineToolModule } from "./types.js";
5
5
  import { booleanSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "./schema.js";
@@ -318,6 +318,21 @@ function transposeNote(note, semitones) {
318
318
  const newOctave = Math.floor(midi / 12) - 1;
319
319
  return `${names[wrapped]}${newOctave}`;
320
320
  }
321
+ /** Highest frequency the SID's 16-bit frequency register can produce (about 3848 Hz PAL, 3995 Hz NTSC). */
322
+ function maxSidFrequencyHz(system) {
323
+ return (0xffff * (system === "PAL" ? 985248 : 1022727)) / 16777216;
324
+ }
325
+ /** Reject a note or frequency the SID cannot play instead of letting playback clamp or skip it. */
326
+ function assertPlayableFrequency(hz, label, system, path) {
327
+ const max = maxSidFrequencyHz(system);
328
+ if (hz > max + 0.5) {
329
+ throw new ToolValidationError(`${label} (${hz.toFixed(1)} Hz) is above the SID's highest ${system} frequency of ${max.toFixed(1)} Hz`, { path });
330
+ }
331
+ }
332
+ function noteHz(note) {
333
+ const midi = noteNameToMidi(note);
334
+ return midi === undefined ? Number.NaN : midiToHz(midi);
335
+ }
321
336
  function sleep(ms, signal) {
322
337
  return new Promise((resolve) => {
323
338
  if (signal?.aborted) {
@@ -347,6 +362,28 @@ function resolveAnalyzer(ctx) {
347
362
  }
348
363
  return (options) => recordAndAnalyzeAudio(options);
349
364
  }
365
+ /**
366
+ * Measure whether the SID is silent by capturing audio (the Ultimate audio
367
+ * stream on C64U, otherwise the host microphone). The SID's control registers
368
+ * are write-only, so reading them back cannot show whether a voice sounds.
369
+ */
370
+ export async function measureSidSilence(ctx) {
371
+ if (DEFAULT_SILENCE_VERIFY_WAIT_MS > 0) {
372
+ await sleep(DEFAULT_SILENCE_VERIFY_WAIT_MS);
373
+ }
374
+ const analysis = await resolveAnalyzer(ctx)({ durationSeconds: DEFAULT_SILENCE_VERIFY_DURATION_SECONDS });
375
+ const { average, max } = extractRmsMetrics((analysis?.analysis?.global_metrics ?? {}));
376
+ if (average === null && max === null) {
377
+ throw new ToolExecutionError("Audio analysis did not provide RMS metrics for silence verification");
378
+ }
379
+ const maxRms = max ?? average ?? 0;
380
+ return {
381
+ silent: maxRms <= DEFAULT_SILENCE_VERIFY_RMS_THRESHOLD,
382
+ averageRms: average ?? maxRms,
383
+ maxRms,
384
+ threshold: DEFAULT_SILENCE_VERIFY_RMS_THRESHOLD,
385
+ };
386
+ }
350
387
  function extractRmsMetrics(globalMetrics) {
351
388
  const average = typeof globalMetrics.average_rms === "number" ? globalMetrics.average_rms : null;
352
389
  const max = typeof globalMetrics.max_rms === "number" ? globalMetrics.max_rms : null;
@@ -453,7 +490,7 @@ export const audioModule = defineToolModule({
453
490
  });
454
491
  const result = await ctx.client.sidSetVolume(parsed.volume);
455
492
  if (!result.success) {
456
- throw new ToolExecutionError("C64 firmware reported failure while setting SID volume", {
493
+ throw new ToolExecutionError("C64 backend reported failure while setting SID volume", {
457
494
  details: normaliseFailure(result.details),
458
495
  });
459
496
  }
@@ -496,7 +533,7 @@ export const audioModule = defineToolModule({
496
533
  cancelGeneratedSidPlayback(); // Reset is the documented way to stop a generated arpeggio.
497
534
  const result = await ctx.client.sidReset(parsed.hard);
498
535
  if (!result.success) {
499
- throw new ToolExecutionError("C64 firmware reported failure while resetting SID", {
536
+ throw new ToolExecutionError("C64 backend reported failure while resetting SID", {
500
537
  details: normaliseFailure(result.details),
501
538
  });
502
539
  }
@@ -533,6 +570,13 @@ export const audioModule = defineToolModule({
533
570
  try {
534
571
  const parsed = sidNoteOnArgsSchema.parse(args ?? {});
535
572
  const voice = Math.max(1, Math.min(3, Math.floor(parsed.voice ?? 1)));
573
+ const system = (parsed.system ?? "PAL");
574
+ if (parsed.note) {
575
+ assertPlayableFrequency(noteHz(parsed.note), parsed.note, system, "$.note");
576
+ }
577
+ else if (parsed.frequencyHz !== undefined) {
578
+ assertPlayableFrequency(parsed.frequencyHz, `${parsed.frequencyHz} Hz`, system, "$.frequencyHz");
579
+ }
536
580
  ctx.logger.info("Starting SID voice", {
537
581
  voice,
538
582
  note: parsed.note ?? null,
@@ -552,7 +596,7 @@ export const audioModule = defineToolModule({
552
596
  release: parsed.release,
553
597
  });
554
598
  if (!result.success) {
555
- throw new ToolExecutionError("C64 firmware reported failure while starting SID voice", {
599
+ throw new ToolExecutionError("C64 backend reported failure while starting SID voice", {
556
600
  details: normaliseFailure(result.details),
557
601
  });
558
602
  }
@@ -606,7 +650,7 @@ export const audioModule = defineToolModule({
606
650
  ctx.logger.info("Stopping SID voice", { voice });
607
651
  const result = await ctx.client.sidNoteOff(voice);
608
652
  if (!result.success) {
609
- throw new ToolExecutionError("C64 firmware reported failure while stopping SID voice", {
653
+ throw new ToolExecutionError("C64 backend reported failure while stopping SID voice", {
610
654
  details: normaliseFailure(result.details),
611
655
  });
612
656
  }
@@ -646,7 +690,7 @@ export const audioModule = defineToolModule({
646
690
  cancelGeneratedSidPlayback(); // Otherwise its remaining notes would resume writing right after this silences the chip.
647
691
  const result = await ctx.client.sidSilenceAll();
648
692
  if (!result.success) {
649
- throw new ToolExecutionError("C64 firmware reported failure while silencing SID", {
693
+ throw new ToolExecutionError("C64 backend reported failure while silencing SID", {
650
694
  details: normaliseFailure(result.details),
651
695
  });
652
696
  }
@@ -737,7 +781,7 @@ export const audioModule = defineToolModule({
737
781
  ctx.logger.info("Playing SID file", { path: parsed.path, songnr: parsed.songnr ?? null });
738
782
  const result = await ctx.client.sidplayFile(parsed.path, parsed.songnr);
739
783
  if (!result.success) {
740
- throw new ToolExecutionError("C64 firmware reported failure while starting SID playback", {
784
+ throw new ToolExecutionError("C64 backend reported failure while starting SID playback", {
741
785
  details: normaliseFailure(result.details),
742
786
  });
743
787
  }
@@ -777,7 +821,7 @@ export const audioModule = defineToolModule({
777
821
  ctx.logger.info("Playing MOD file", { path: parsed.path });
778
822
  const result = await ctx.client.modplayFile(parsed.path);
779
823
  if (!result.success) {
780
- throw new ToolExecutionError("C64 firmware reported failure while starting MOD playback", {
824
+ throw new ToolExecutionError("C64 backend reported failure while starting MOD playback", {
781
825
  details: normaliseFailure(result.details),
782
826
  });
783
827
  }
@@ -824,6 +868,8 @@ export const audioModule = defineToolModule({
824
868
  const iv = intervals[i % intervals.length];
825
869
  const note = transposeNote(parsed.root, iv);
826
870
  const durationMs = parsed.preset === "expression" ? expressiveDurations[i % expressiveDurations.length] : parsed.tempoMs;
871
+ // Playback runs in the background, so a note the SID cannot play must be rejected here.
872
+ assertPlayableFrequency(noteHz(note), note, "PAL", "$.root");
827
873
  timeline.push({ t: timestamp, note, durationMs });
828
874
  timestamp += durationMs;
829
875
  }
@@ -910,7 +956,15 @@ export const audioModule = defineToolModule({
910
956
  tags: ["sid", "music", "compiler"],
911
957
  prerequisites: [],
912
958
  examples: [
913
- { name: "Compile PRG", description: "Play compiled PRG", arguments: { sidwave: "song: { title: 'Demo' }", output: "prg", dryRun: false } },
959
+ {
960
+ name: "Compile PRG",
961
+ description: "Play compiled PRG",
962
+ arguments: {
963
+ sidwave: "sidwave: 1.0\ntitle: Demo\ntempo: 120\nmode: PAL\nvoices:\n - id: 1\n waveform: triangle\n patterns: { A: { groove: [C4, E4, G4, C5] } }\ntimeline:\n - { bars: 2, layers: { v1: A } }\n",
964
+ output: "prg",
965
+ dryRun: false,
966
+ },
967
+ },
914
968
  ],
915
969
  workflowHints: [
916
970
  "Use when the user provides SIDWAVE/CPG scores or asks for export to PRG/SID; share any generated download URIs in your response.",
@@ -941,7 +995,7 @@ export const audioModule = defineToolModule({
941
995
  const sid = compileSidwaveToSid(document, compiled.prg, { entryAddress: compiled.entryAddress });
942
996
  const result = await ctx.client.sidplayAttachment(sid.sid);
943
997
  if (!result.success) {
944
- throw new ToolExecutionError("C64 firmware reported failure while playing compiled SID", {
998
+ throw new ToolExecutionError("C64 backend reported failure while playing compiled SID", {
945
999
  details: normaliseFailure(result.details),
946
1000
  });
947
1001
  }
@@ -951,7 +1005,7 @@ export const audioModule = defineToolModule({
951
1005
  else {
952
1006
  const result = await ctx.client.runPrg(compiled.prg);
953
1007
  if (!result.success) {
954
- throw new ToolExecutionError("C64 firmware reported failure while running compiled PRG", {
1008
+ throw new ToolExecutionError("C64 backend reported failure while running compiled PRG", {
955
1009
  details: normaliseFailure(result.details),
956
1010
  });
957
1011
  }
@@ -967,13 +1021,17 @@ export const audioModule = defineToolModule({
967
1021
  pulse_width: voice.pulse_width,
968
1022
  adsr: voice.adsr,
969
1023
  }));
970
- return textResult(message, {
1024
+ // Warnings (for example a PRG that leaves no room for BASIC) go into the text that clients show.
1025
+ const warningText = compiled.warnings.length > 0 ? `\nWarning: ${compiled.warnings.join("\nWarning: ")}` : "";
1026
+ return textResult(`${message}${warningText}`, {
971
1027
  success: true,
972
1028
  ranOnC64,
973
1029
  dryRun: parsed.dryRun,
974
1030
  format: outputFormat,
975
1031
  prgSize: compiled.prg.length,
976
1032
  entryAddress: compiled.entryAddress,
1033
+ basicFreeBytes: compiled.basicFreeBytes,
1034
+ warnings: compiled.warnings,
977
1035
  song: document.song,
978
1036
  voices: voiceSummaries,
979
1037
  runDetails,
@@ -229,7 +229,7 @@ export const developerModule = defineToolModule({
229
229
  });
230
230
  const result = await ctx.client.configSet(parsed.category, parsed.item, String(parsed.value));
231
231
  if (!result.success) {
232
- throw new ToolExecutionError("C64 firmware reported failure while updating configuration", {
232
+ throw new ToolExecutionError("C64 backend reported failure while updating configuration", {
233
233
  details: toRecord(result.details),
234
234
  });
235
235
  }
@@ -275,7 +275,7 @@ export const developerModule = defineToolModule({
275
275
  ]));
276
276
  const result = await ctx.client.configBatchUpdate(requestPayload);
277
277
  if (!result.success) {
278
- throw new ToolExecutionError("C64 firmware reported failure during batch configuration update", {
278
+ throw new ToolExecutionError("C64 backend reported failure during batch configuration update", {
279
279
  details: toRecord(result.details),
280
280
  });
281
281
  }
@@ -312,7 +312,7 @@ export const developerModule = defineToolModule({
312
312
  ctx.logger.info("Loading configuration from flash");
313
313
  const result = await ctx.client.configLoadFromFlash();
314
314
  if (!result.success) {
315
- throw new ToolExecutionError("C64 firmware reported failure while loading from flash", {
315
+ throw new ToolExecutionError("C64 backend reported failure while loading from flash", {
316
316
  details: toRecord(result.details),
317
317
  });
318
318
  }
@@ -348,7 +348,7 @@ export const developerModule = defineToolModule({
348
348
  ctx.logger.info("Saving configuration to flash");
349
349
  const result = await ctx.client.configSaveToFlash();
350
350
  if (!result.success) {
351
- throw new ToolExecutionError("C64 firmware reported failure while saving to flash", {
351
+ throw new ToolExecutionError("C64 backend reported failure while saving to flash", {
352
352
  details: toRecord(result.details),
353
353
  });
354
354
  }
@@ -384,7 +384,7 @@ export const developerModule = defineToolModule({
384
384
  ctx.logger.info("Resetting configuration to defaults");
385
385
  const result = await ctx.client.configResetToDefault();
386
386
  if (!result.success) {
387
- throw new ToolExecutionError("C64 firmware reported failure while resetting configuration", {
387
+ throw new ToolExecutionError("C64 backend reported failure while resetting configuration", {
388
388
  details: toRecord(result.details),
389
389
  });
390
390
  }
@@ -484,7 +484,7 @@ export const developerModule = defineToolModule({
484
484
  ctx.logger.info("Reading debug register");
485
485
  const result = await ctx.client.debugregRead();
486
486
  if (!result.success) {
487
- throw new ToolExecutionError("C64 firmware reported failure while reading debug register", {
487
+ throw new ToolExecutionError("C64 backend reported failure while reading debug register", {
488
488
  details: toRecord(result.details),
489
489
  });
490
490
  }
@@ -524,7 +524,7 @@ export const developerModule = defineToolModule({
524
524
  ctx.logger.info("Writing debug register", { value });
525
525
  const result = await ctx.client.debugregWrite(value);
526
526
  if (!result.success) {
527
- throw new ToolExecutionError("C64 firmware reported failure while writing debug register", {
527
+ throw new ToolExecutionError("C64 backend reported failure while writing debug register", {
528
528
  details: toRecord(result.details),
529
529
  });
530
530
  }
@@ -63,6 +63,13 @@ export function unknownErrorResult(error) {
63
63
  if (error instanceof ToolError) {
64
64
  return toolErrorResult(error);
65
65
  }
66
+ // Input documents such as SIDWAVE scores are checked outside the tool schemas;
67
+ // their errors carry a path and are input errors, not internal failures.
68
+ if (error instanceof Error && error.name === "SidwaveValidationError") {
69
+ const path = error.path;
70
+ const metadata = { kind: "validation", ...(typeof path === "string" ? { path } : {}) };
71
+ return { ...textResult(error.message, { error: metadata }), isError: true };
72
+ }
66
73
  const metadata = { kind: "unknown" };
67
74
  const message = error instanceof Error ? error.message : String(error);
68
75
  const base = textResult(message, { error: metadata });
@@ -349,7 +349,7 @@ async function executeGenerateSprite(rawArgs, ctx) {
349
349
  multicolour: parsed.multicolour,
350
350
  });
351
351
  if (!result.success) {
352
- throw new ToolExecutionError("C64 firmware reported failure while rendering sprite", {
352
+ throw new ToolExecutionError("C64 backend reported failure while rendering sprite", {
353
353
  details: normaliseFailure(result.details),
354
354
  });
355
355
  }
@@ -383,7 +383,7 @@ async function executeRenderPetscii(rawArgs, ctx) {
383
383
  });
384
384
  const result = await ctx.client.renderPetsciiScreenAndRun(parsed);
385
385
  if (!result.success) {
386
- throw new ToolExecutionError("C64 firmware reported failure while rendering PETSCII text", {
386
+ throw new ToolExecutionError("C64 backend reported failure while rendering PETSCII text", {
387
387
  details: normaliseFailure(result.details),
388
388
  });
389
389
  }
@@ -426,7 +426,7 @@ async function executeGenerateBitmap(rawArgs, ctx) {
426
426
  screenAddress: parsed.screenAddress,
427
427
  });
428
428
  if (!result.success) {
429
- throw new ToolExecutionError("C64 firmware reported failure while displaying bitmap image", {
429
+ throw new ToolExecutionError("C64 backend reported failure while displaying bitmap image", {
430
430
  details: normaliseFailure(result.details),
431
431
  });
432
432
  }
@@ -498,7 +498,7 @@ async function executeCreatePetscii(rawArgs, ctx) {
498
498
  if (!parsed.dryRun) {
499
499
  runResult = await ctx.client.uploadAndRunBasic(art.program);
500
500
  if (!runResult.success) {
501
- throw new ToolExecutionError("C64 firmware reported failure while rendering PETSCII art", {
501
+ throw new ToolExecutionError("C64 backend reported failure while rendering PETSCII art", {
502
502
  details: normaliseFailure(runResult.details),
503
503
  });
504
504
  }
@@ -57,12 +57,10 @@ const PETSCII_TOKENS = {
57
57
  LIGHT_GREY: 155,
58
58
  REVERSE_ON: 18,
59
59
  REVERSE_OFF: 146,
60
- FLASH_ON: 130,
61
- FLASH_OFF: 148,
60
+ // FLASH ON/OFF, ESC and TAB are C128 codes: on a C64, 148 is INST (inserts a
61
+ // space) and 9 re-enables SHIFT+C= case switching, so they are not offered.
62
62
  LOWERCASE: 14,
63
63
  UPPERCASE: 142,
64
- ESCAPE: 27,
65
- TAB: 9,
66
64
  CURSOR_UP: 145,
67
65
  CURSOR_DOWN: 17,
68
66
  CURSOR_LEFT: 157,
@@ -176,7 +174,7 @@ export const keyArgsSchema = objectSchema({
176
174
  additionalProperties: false,
177
175
  });
178
176
  export const joystickArgsSchema = objectSchema({
179
- description: "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
177
+ description: "Simulate joystick input. On C64U/U64 this uses machine:input. On VICE it attaches VICE's I/O simulation joyport device while pressed; that device also pulls CIA lines 5-7 low, so port 1 produces phantom keys (e.g. SHIFT+C=) and port 2 reads $1E for up instead of $7E. Prefer port 2 on VICE and test bits 0-4.",
180
178
  properties: {
181
179
  op: literalSchema("joystick"),
182
180
  port: numberSchema({
@@ -297,7 +295,6 @@ const inputOperationHandlers = {
297
295
  const port = parsed.port;
298
296
  const durationMs = parsed.durationMs ?? 80;
299
297
  const pressedByte = joystickByte(parsed.controls);
300
- const releasedByte = 0xff;
301
298
  const platform = await ctx.client.getActiveBackendType();
302
299
  if (platform === "c64u") {
303
300
  if (parsed.controls.length === 0 && parsed.action === "release") {
@@ -315,19 +312,19 @@ const inputOperationHandlers = {
315
312
  throw new ToolValidationError("VICE joystick supports up, down, left, right, and fire only.", { path: "$.controls" });
316
313
  }
317
314
  if (parsed.action === "release") {
318
- await ctx.client.viceJoyportSet(port, releasedByte);
315
+ await ctx.client.viceJoystickRelease(port);
319
316
  ctx.logger.info("Released joystick", { port });
320
317
  return textResult(`Joystick port ${port} released.`, { success: true, port, action: "release" });
321
318
  }
322
319
  if (parsed.action === "press") {
323
- await ctx.client.viceJoyportSet(port, pressedByte);
320
+ await ctx.client.viceJoystickPress(port, pressedByte);
324
321
  ctx.logger.info("Pressed joystick", { port, controls: parsed.controls, byte: pressedByte });
325
- return textResult(`Joystick port ${port} pressed: ${parsed.controls.join(", ") || "none"}.`, { success: true, port, action: "press", controls: parsed.controls });
322
+ return textResult(`Joystick port ${port} pressed: ${parsed.controls.join(", ") || "none"}.${port === 1 ? " On VICE, port 1 also shows phantom keys while held." : ""}`, { success: true, port, action: "press", controls: parsed.controls });
326
323
  }
327
324
  // tap: press → wait → release
328
- await ctx.client.viceJoyportSet(port, pressedByte);
325
+ await ctx.client.viceJoystickPress(port, pressedByte);
329
326
  await new Promise((res) => setTimeout(res, durationMs));
330
- await ctx.client.viceJoyportSet(port, releasedByte);
327
+ await ctx.client.viceJoystickRelease(port);
331
328
  ctx.logger.info("Tapped joystick", { port, controls: parsed.controls, durationMs });
332
329
  return textResult(`Joystick port ${port} tapped: ${parsed.controls.join(", ") || "none"} for ${durationMs}ms.`, { success: true, port, action: "tap", controls: parsed.controls, durationMs });
333
330
  }
@@ -70,7 +70,7 @@ export const machineControlModule = defineToolModule({
70
70
  ctx.logger.info("Resetting C64");
71
71
  const result = await ctx.client.reset();
72
72
  if (!result.success) {
73
- throw new ToolExecutionError("C64 firmware reported failure while resetting", {
73
+ throw new ToolExecutionError("C64 backend reported failure while resetting", {
74
74
  details: normaliseFailure(result.details),
75
75
  });
76
76
  }
@@ -108,7 +108,7 @@ export const machineControlModule = defineToolModule({
108
108
  ctx.logger.info("Rebooting C64");
109
109
  const result = await ctx.client.reboot();
110
110
  if (!result.success) {
111
- throw new ToolExecutionError("C64 firmware reported failure while rebooting", {
111
+ throw new ToolExecutionError("C64 backend reported failure while rebooting", {
112
112
  details: normaliseFailure(result.details),
113
113
  });
114
114
  }
@@ -146,7 +146,7 @@ export const machineControlModule = defineToolModule({
146
146
  ctx.logger.info("Pausing C64 execution");
147
147
  const result = await ctx.client.pause();
148
148
  if (!result.success) {
149
- throw new ToolExecutionError("C64 firmware reported failure while pausing", {
149
+ throw new ToolExecutionError("C64 backend reported failure while pausing", {
150
150
  details: normaliseFailure(result.details),
151
151
  });
152
152
  }
@@ -184,7 +184,7 @@ export const machineControlModule = defineToolModule({
184
184
  ctx.logger.info("Resuming C64 execution");
185
185
  const result = await ctx.client.resume();
186
186
  if (!result.success) {
187
- throw new ToolExecutionError("C64 firmware reported failure while resuming", {
187
+ throw new ToolExecutionError("C64 backend reported failure while resuming", {
188
188
  details: normaliseFailure(result.details),
189
189
  });
190
190
  }
@@ -222,7 +222,7 @@ export const machineControlModule = defineToolModule({
222
222
  ctx.logger.info("Powering off C64");
223
223
  const result = await ctx.client.poweroff();
224
224
  if (!result.success) {
225
- throw new ToolExecutionError("C64 firmware reported failure while powering off", {
225
+ throw new ToolExecutionError("C64 backend reported failure while powering off", {
226
226
  details: normaliseFailure(result.details),
227
227
  });
228
228
  }
@@ -295,7 +295,7 @@ export const machineControlModule = defineToolModule({
295
295
  ctx.logger.info("Toggling Ultimate menu button");
296
296
  const result = await ctx.client.menuButton();
297
297
  if (!result.success) {
298
- throw new ToolExecutionError("C64 firmware reported failure while toggling menu button", {
298
+ throw new ToolExecutionError("C64 backend reported failure while toggling menu button", {
299
299
  details: normaliseFailure(result.details),
300
300
  });
301
301
  }
@@ -186,7 +186,7 @@ async function executeReadMemory(rawArgs, ctx) {
186
186
  ctx.logger.info("Reading C64 memory", { address: parsed.address, length: parsed.length });
187
187
  const result = await ctx.client.readMemory(parsed.address, String(parsed.length));
188
188
  if (!result.success) {
189
- throw new ToolExecutionError("C64 firmware reported failure while reading memory", {
189
+ throw new ToolExecutionError("C64 backend reported failure while reading memory", {
190
190
  details: normaliseFailure(result.details),
191
191
  });
192
192
  }
@@ -229,7 +229,7 @@ async function executeWriteMemory(rawArgs, ctx) {
229
229
  if (!shouldVerify) {
230
230
  const result = await ctx.client.writeMemory(parsed.address, writeInfo.canonical);
231
231
  if (!result.success) {
232
- throw new ToolExecutionError("C64 firmware reported failure while writing memory", {
232
+ throw new ToolExecutionError("C64 backend reported failure while writing memory", {
233
233
  details: normaliseFailure(result.details),
234
234
  });
235
235
  }
@@ -251,7 +251,7 @@ async function executeWriteMemory(rawArgs, ctx) {
251
251
  if (canPause) {
252
252
  const pauseResult = await ctx.client.pause(pinnedFacade);
253
253
  if (!pauseResult.success) {
254
- throw new ToolExecutionError("C64 firmware reported failure while pausing", {
254
+ throw new ToolExecutionError("C64 backend reported failure while pausing", {
255
255
  details: normaliseFailure(pauseResult.details),
256
256
  });
257
257
  }
@@ -262,7 +262,7 @@ async function executeWriteMemory(rawArgs, ctx) {
262
262
  const readLength = Math.max(1, Math.max(writeInfo.bytes.length, expectedBytes.length));
263
263
  const preRead = await ctx.client.readMemory(parsed.address, String(readLength), pinnedFacade);
264
264
  if (!preRead.success) {
265
- throw new ToolExecutionError("C64 firmware reported failure while reading memory", {
265
+ throw new ToolExecutionError("C64 backend reported failure while reading memory", {
266
266
  details: normaliseFailure(preRead.details),
267
267
  });
268
268
  }
@@ -290,13 +290,13 @@ async function executeWriteMemory(rawArgs, ctx) {
290
290
  }
291
291
  const writeResult = await ctx.client.writeMemory(parsed.address, writeInfo.canonical, pinnedFacade);
292
292
  if (!writeResult.success) {
293
- throw new ToolExecutionError("C64 firmware reported failure while writing memory", {
293
+ throw new ToolExecutionError("C64 backend reported failure while writing memory", {
294
294
  details: normaliseFailure(writeResult.details),
295
295
  });
296
296
  }
297
297
  const postRead = await ctx.client.readMemory(parsed.address, String(Math.max(1, writeInfo.bytes.length)), pinnedFacade);
298
298
  if (!postRead.success) {
299
- throw new ToolExecutionError("C64 firmware reported failure while reading back memory", {
299
+ throw new ToolExecutionError("C64 backend reported failure while reading back memory", {
300
300
  details: normaliseFailure(postRead.details),
301
301
  });
302
302
  }
@@ -267,7 +267,7 @@ export const tools = [
267
267
  const results = [];
268
268
  let restoreError;
269
269
  try {
270
- for (const backend of requestedBackends) {
270
+ for (const [backendIndex, backend] of requestedBackends.entries()) {
271
271
  context.client.switchBackend(backend);
272
272
  context.setPlatform(backend);
273
273
  await trySilenceSid(context);
@@ -303,7 +303,11 @@ export const tools = [
303
303
  analysis,
304
304
  };
305
305
  }
306
- await trySilenceSid(context);
306
+ // Stop this backend's playback only before moving on to the next one;
307
+ // the last backend keeps playing so the preset can be heard in full.
308
+ if (backendIndex < requestedBackends.length - 1) {
309
+ await trySilenceSid(context);
310
+ }
307
311
  backendResult.verification = verification;
308
312
  backendResult.success = true;
309
313
  results.push(backendResult);
@@ -493,6 +497,8 @@ export const tools = [
493
497
  entryAddress: compiled.entryAddress,
494
498
  prgBytes: compiled.prg.length,
495
499
  title: document.song?.title ?? null,
500
+ basicFreeBytes: compiled.basicFreeBytes,
501
+ warnings: compiled.warnings,
496
502
  },
497
503
  playback: {
498
504
  method: playbackMethod,
@@ -223,7 +223,7 @@ export const tools = [
223
223
  try {
224
224
  const read = await ctx.client.readMemory(`$${formatAddressHex(startAddress)}`, String(length));
225
225
  if (!read.success || typeof read.data !== "string") {
226
- throw new ToolExecutionError("C64 firmware reported failure while reading memory", { details: normalizeErrorDetails(read.details) });
226
+ throw new ToolExecutionError("C64 backend reported failure while reading memory", { details: normalizeErrorDetails(read.details) });
227
227
  }
228
228
  const bytes = hexToBytes(read.data);
229
229
  if (bytes.length < length) {
@@ -97,7 +97,7 @@ export const tools = [
97
97
  if (canPause) {
98
98
  const pauseResult = await ctx.client.pause();
99
99
  if (!pauseResult.success) {
100
- throw new ToolExecutionError("C64 firmware reported failure while pausing", { details: normalizeErrorDetails(pauseResult.details) });
100
+ throw new ToolExecutionError("C64 backend reported failure while pausing", { details: normalizeErrorDetails(pauseResult.details) });
101
101
  }
102
102
  paused = true;
103
103
  }
@@ -105,7 +105,7 @@ export const tools = [
105
105
  try {
106
106
  const pre = await ctx.client.readMemory(parsed.address, String(Math.max(1, readLen)));
107
107
  if (!pre.success) {
108
- throw new ToolExecutionError("C64 firmware reported failure while reading memory", { details: normalizeErrorDetails(pre.details) });
108
+ throw new ToolExecutionError("C64 backend reported failure while reading memory", { details: normalizeErrorDetails(pre.details) });
109
109
  }
110
110
  preReadHex = pre.data ?? "$";
111
111
  const preBytes = hexToBytes(preReadHex);
@@ -125,11 +125,11 @@ export const tools = [
125
125
  }
126
126
  const write = await ctx.client.writeMemory(parsed.address, bytesToHex(writeBytes));
127
127
  if (!write.success) {
128
- throw new ToolExecutionError("C64 firmware reported failure while writing memory", { details: normalizeErrorDetails(write.details) });
128
+ throw new ToolExecutionError("C64 backend reported failure while writing memory", { details: normalizeErrorDetails(write.details) });
129
129
  }
130
130
  const post = await ctx.client.readMemory(parsed.address, String(Math.max(1, writeBytes.length)));
131
131
  if (!post.success) {
132
- throw new ToolExecutionError("C64 firmware reported failure while reading back memory", { details: normalizeErrorDetails(post.details) });
132
+ throw new ToolExecutionError("C64 backend reported failure while reading back memory", { details: normalizeErrorDetails(post.details) });
133
133
  }
134
134
  const postBytes = hexToBytes(post.data ?? "$");
135
135
  const diffs = [];
@@ -1,6 +1,7 @@
1
1
  import { objectSchema, stringSchema, arraySchema, numberSchema, optionalSchema, booleanSchema } from "../schema.js";
2
2
  import { jsonResult } from "../responses.js";
3
3
  import { ToolError, ToolUnsupportedPlatformError, ToolExecutionError, toolErrorResult, unknownErrorResult } from "../errors.js";
4
+ import { measureSidSilence } from "../audio.js";
4
5
  import { getPlatformStatus } from "../../platform.js";
5
6
  import { promises as fs } from "node:fs";
6
7
  import { resolve as resolvePath, join as joinPath } from "node:path";
@@ -206,7 +207,7 @@ const batchRunWithAssertionsArgsSchema = objectSchema({
206
207
  assertions: optionalSchema(arraySchema(objectSchema({
207
208
  description: "Assertion to check after run",
208
209
  properties: {
209
- type: stringSchema({ description: "Assertion type", enum: ["screen_contains", "memory_equals", "sid_silent"] }),
210
+ type: stringSchema({ description: "Assertion type; sid_silent needs the C64 Ultimate audio stream (c64u backend).", enum: ["screen_contains", "memory_equals", "sid_silent"] }),
210
211
  pattern: optionalSchema(stringSchema({ description: "Pattern for screen_contains", minLength: 1 })),
211
212
  address: optionalSchema(stringSchema({ description: "Address for memory_equals", minLength: 1 })),
212
213
  expected: optionalSchema(stringSchema({ description: "Expected value (hex)", minLength: 1 })),
@@ -720,17 +721,20 @@ export const tools = [
720
721
  status = "fail";
721
722
  }
722
723
  else if (assertion.type === "sid_silent") {
723
- // Check SID gate bits are off (simple check)
724
- const result = await ctx.client.readMemory("$D404", "1");
725
- if (!result.success || typeof result.data !== "string") {
726
- throw new Error("Failed to read SID voice 1 control register for sid_silent assertion");
724
+ // SID control registers are write-only (an Ultimate 64 reads $D404 as $00
725
+ // whatever was written), so silence is measured from the Ultimate audio
726
+ // stream. Other backends would fall back to the host microphone, which
727
+ // measures the room rather than the C64, so they report an error instead.
728
+ const backend = ctx.platform?.id ?? await ctx.client.getActiveBackendType?.();
729
+ if (backend !== undefined && backend !== "c64u") {
730
+ throw new Error(`sid_silent needs the C64 Ultimate audio stream and is not available on ${backend}`);
727
731
  }
728
- const gate1 = parseInt(result.data.slice(1), 16) & 0x01;
729
- const isSilent = gate1 === 0;
732
+ const silence = await measureSidSilence(ctx);
733
+ const isSilent = silence.silent;
730
734
  assertionResults.push({
731
735
  type: assertion.type,
732
736
  status: isSilent ? "pass" : "fail",
733
- message: isSilent ? undefined : "SID voice 1 gate is on",
737
+ message: isSilent ? undefined : `SID output is not silent (peak RMS ${silence.maxRms.toFixed(3)} > ${silence.threshold})`,
734
738
  });
735
739
  if (!isSilent)
736
740
  status = "fail";
@@ -278,7 +278,7 @@ export const printerModule = defineToolModule({
278
278
  formFeed: parsed.formFeed,
279
279
  });
280
280
  if (!result.success) {
281
- throw new ToolExecutionError("C64 firmware reported failure while printing text", {
281
+ throw new ToolExecutionError("C64 backend reported failure while printing text", {
282
282
  details: normaliseFailure(result.details),
283
283
  });
284
284
  }
@@ -337,7 +337,7 @@ export const printerModule = defineToolModule({
337
337
  ensureMsb: parsed.ensureMsb,
338
338
  });
339
339
  if (!result.success) {
340
- throw new ToolExecutionError("C64 firmware reported failure while printing Commodore bitmap", {
340
+ throw new ToolExecutionError("C64 backend reported failure while printing Commodore bitmap", {
341
341
  details: normaliseFailure(result.details),
342
342
  });
343
343
  }
@@ -398,7 +398,7 @@ export const printerModule = defineToolModule({
398
398
  timesPerLine: parsed.timesPerLine,
399
399
  });
400
400
  if (!result.success) {
401
- throw new ToolExecutionError("C64 firmware reported failure while printing Epson bitmap", {
401
+ throw new ToolExecutionError("C64 backend reported failure while printing Epson bitmap", {
402
402
  details: normaliseFailure(result.details),
403
403
  });
404
404
  }
@@ -458,7 +458,7 @@ export const printerModule = defineToolModule({
458
458
  secondaryAddress: parsed.secondaryAddress,
459
459
  });
460
460
  if (!result.success) {
461
- throw new ToolExecutionError("C64 firmware reported failure while defining DLL characters", {
461
+ throw new ToolExecutionError("C64 backend reported failure while defining DLL characters", {
462
462
  details: normaliseFailure(result.details),
463
463
  });
464
464
  }