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.
- package/README.md +2 -2
- package/data/sound/sidwave.md +113 -36
- package/data/sound/sidwave.schema.json +138 -28
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +83 -93
- package/dist/config.js +21 -4
- package/dist/device.js +130 -24
- package/dist/diagnostics.js +10 -2
- package/dist/mcp-server.js +12 -4
- package/dist/sidwave.js +453 -61
- package/dist/sidwaveCompiler.js +1424 -269
- package/dist/tools/audio.js +70 -12
- package/dist/tools/developer.js +7 -7
- package/dist/tools/errors.js +7 -0
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/input.js +8 -11
- package/dist/tools/machineControl.js +6 -6
- package/dist/tools/memory.js +6 -6
- package/dist/tools/meta/audio.js +8 -2
- package/dist/tools/meta/graphics.js +1 -1
- package/dist/tools/meta/memory.js +4 -4
- package/dist/tools/meta/program.js +12 -8
- package/dist/tools/printer.js +4 -4
- package/dist/tools/programRunners.js +8 -8
- package/dist/tools/registry/disk.js +7 -5
- package/dist/tools/storage.js +21 -17
- package/dist/tools/streaming.js +2 -2
- package/dist/tools/translation/assembler.js +44 -12
- package/dist/tools/translation/basicTokenizer.js +5 -1
- package/dist/tools/translation/opcodeInventory.js +1 -1
- package/dist/tools/vice.js +1 -1
- package/dist/vice/mockServer.js +92 -5
- package/dist/vice/readiness.js +4 -9
- package/dist/vice/viceClient.js +18 -0
- package/mcp/schemas/c64_disk.schema.json +2 -2
- package/mcp/schemas/c64_input.schema.json +3 -3
- package/mcp/schemas/c64_program.schema.json +2 -2
- package/mcp/schemas/c64_vice.schema.json +3 -3
- package/mcp/server.json +1 -1
- package/mcp/tools.json +14 -10
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/mockC64Server.mjs +15 -1
- package/scripts/run-coverage.mjs +31 -1
- package/scripts/update-readme.ts +3 -2
- package/server.json +2 -2
package/dist/tools/audio.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
{
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/tools/developer.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
527
|
+
throw new ToolExecutionError("C64 backend reported failure while writing debug register", {
|
|
528
528
|
details: toRecord(result.details),
|
|
529
529
|
});
|
|
530
530
|
}
|
package/dist/tools/errors.js
CHANGED
|
@@ -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 });
|
package/dist/tools/graphics.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
501
|
+
throw new ToolExecutionError("C64 backend reported failure while rendering PETSCII art", {
|
|
502
502
|
details: normaliseFailure(runResult.details),
|
|
503
503
|
});
|
|
504
504
|
}
|
package/dist/tools/input.js
CHANGED
|
@@ -57,12 +57,10 @@ const PETSCII_TOKENS = {
|
|
|
57
57
|
LIGHT_GREY: 155,
|
|
58
58
|
REVERSE_ON: 18,
|
|
59
59
|
REVERSE_OFF: 146,
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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"}
|
|
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.
|
|
325
|
+
await ctx.client.viceJoystickPress(port, pressedByte);
|
|
329
326
|
await new Promise((res) => setTimeout(res, durationMs));
|
|
330
|
-
await ctx.client.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
298
|
+
throw new ToolExecutionError("C64 backend reported failure while toggling menu button", {
|
|
299
299
|
details: normaliseFailure(result.details),
|
|
300
300
|
});
|
|
301
301
|
}
|
package/dist/tools/memory.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
299
|
+
throw new ToolExecutionError("C64 backend reported failure while reading back memory", {
|
|
300
300
|
details: normaliseFailure(postRead.details),
|
|
301
301
|
});
|
|
302
302
|
}
|
package/dist/tools/meta/audio.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
|
729
|
-
const isSilent =
|
|
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 :
|
|
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";
|
package/dist/tools/printer.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
461
|
+
throw new ToolExecutionError("C64 backend reported failure while defining DLL characters", {
|
|
462
462
|
details: normaliseFailure(result.details),
|
|
463
463
|
});
|
|
464
464
|
}
|