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,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
C64 Bridge - Local RAG Retriever
|
|
3
|
+
GPL-2.0-only
|
|
4
|
+
*/
|
|
5
|
+
import { cosineSimilarity } from "./embeddings.js";
|
|
6
|
+
const BASIC_SIGNAL_RE = /(^\s*\d{1,5}\s)|\b(PRINT|POKE|GOTO|GOSUB|RESTORE|READ|DATA|INPUT|CHR\$|TI\$|TAB\()/im;
|
|
7
|
+
const ASM_SIGNAL_RE = /(^\s*(?:[A-Z_][\w]*:)?\s*(?:\.?[A-Z]{2,4})\b)|\$[0-9A-F]{2,4}/im;
|
|
8
|
+
export class LocalRagRetriever {
|
|
9
|
+
constructor(model, opts) {
|
|
10
|
+
this.model = model;
|
|
11
|
+
this.basic = opts.basic;
|
|
12
|
+
this.asm = opts.asm;
|
|
13
|
+
this.mixed = opts.mixed;
|
|
14
|
+
this.hardware = opts.hardware;
|
|
15
|
+
this.other = opts.other;
|
|
16
|
+
}
|
|
17
|
+
updateIndexes(opts) {
|
|
18
|
+
this.basic = opts.basic;
|
|
19
|
+
this.asm = opts.asm;
|
|
20
|
+
this.mixed = opts.mixed;
|
|
21
|
+
this.hardware = opts.hardware;
|
|
22
|
+
this.other = opts.other;
|
|
23
|
+
}
|
|
24
|
+
async retrieve(query, topK = 3, filterLanguage) {
|
|
25
|
+
const qv = await this.model.embed(query);
|
|
26
|
+
const candidates = [];
|
|
27
|
+
const consider = (index) => {
|
|
28
|
+
if (!index)
|
|
29
|
+
return;
|
|
30
|
+
for (const rec of index.records) {
|
|
31
|
+
const matchesBasic = BASIC_SIGNAL_RE.test(rec.text);
|
|
32
|
+
const matchesAsm = ASM_SIGNAL_RE.test(rec.text);
|
|
33
|
+
if (normalized === "basic" && !matchesBasic) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (normalized === "asm" && !matchesAsm) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
let score = cosineSimilarity(qv, new Float32Array(rec.vector));
|
|
40
|
+
if (normalized === "basic" && matchesBasic) {
|
|
41
|
+
score += 0.05;
|
|
42
|
+
}
|
|
43
|
+
else if (normalized === "asm" && matchesAsm) {
|
|
44
|
+
score += 0.05;
|
|
45
|
+
}
|
|
46
|
+
candidates.push({ score, text: rec.text });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const normalized = filterLanguage;
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const push = (index) => {
|
|
52
|
+
if (!index || seen.has(index))
|
|
53
|
+
return;
|
|
54
|
+
seen.add(index);
|
|
55
|
+
consider(index);
|
|
56
|
+
};
|
|
57
|
+
if (!normalized || normalized === "basic")
|
|
58
|
+
push(this.basic);
|
|
59
|
+
if (!normalized || normalized === "asm")
|
|
60
|
+
push(this.asm);
|
|
61
|
+
if (!normalized || normalized === "mixed")
|
|
62
|
+
push(this.mixed);
|
|
63
|
+
if (normalized === "basic" && (!this.basic || this.basic.records.length === 0))
|
|
64
|
+
push(this.mixed);
|
|
65
|
+
if (normalized === "asm" && (!this.asm || this.asm.records.length === 0))
|
|
66
|
+
push(this.mixed);
|
|
67
|
+
if (!normalized || normalized === "hardware")
|
|
68
|
+
push(this.hardware);
|
|
69
|
+
if (!normalized || normalized === "other")
|
|
70
|
+
push(this.other);
|
|
71
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
72
|
+
return candidates.slice(0, topK).map((c) => c.text);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type,description,link,depth
|
|
2
|
+
assembly,C64 ASM Book Listings,https://github.com/64kramsystem/c64_asm_book_listings/tree/master/machine_code_games_routines_for_the_commodore_64,10
|
|
3
|
+
assembly,Assorted C64 assembly samples (Make builds PRG),https://github.com/jarrodparkes/c64-examples,10
|
|
4
|
+
assembly,C64 Users Guide ML examples (ACME asm),https://github.com/lacerto/c64_users_guide_examples,10
|
|
5
|
+
assembly,CHIP-8 interpreter for C64 (includes D64/PRG),https://github.com/kschuetz/c64-chip8,10
|
|
6
|
+
assembly,Elite (C64) fully documented sources + D64 builds,https://github.com/markmoxon/elite-source,10
|
|
7
|
+
assembly,Examples in 61002 assembly (PRG build via Make),https://github.com/digitsensitive/c64,10
|
|
8
|
+
#assembly,Fully commented ROM disassembly (master .txt sources),https://github.com/lagomorph/c64rom,10
|
|
9
|
+
assembly,Fully commented ROM disassembly (text),https://www.zimmers.net/anonftp/pub/cbm/src/c64/c64_rom_disassembly.txt,2
|
|
10
|
+
#assembly,Kernel disassembly (text),https://raw.githubusercontent.com/androdev4u/c64rom/refs/heads/master/c64disasm/c64disasm_cbm.txt,1
|
|
11
|
+
#assembly,Original C64 BASIC+KERNAL source (buildable),https://github.com/mist64/c64rom,10
|
|
12
|
+
assembly,Sample programs using Kick Assembler,https://github.com/nealvis/c64_samples_kick,10
|
|
13
|
+
assembly,Simple ASM tutorial series (multiple examples),https://github.com/Ziagl/C64-Assembler,10
|
|
14
|
+
assembly,Starfield demo including PRG,https://github.com/Starcross/starfield,10
|
|
15
|
+
assembly,Various small assembly programs (ACME/PRG),https://github.com/cslarsen/c64-examples,10
|
|
16
|
+
assembly,Machine Language for the Commodore 64,https://www.retro-commodore.eu/files/development/c64-development/Butterfield_Machine_Language_for_the_Commodore_64_and_other_Commodore_Computers.ocr.txt,1
|
|
17
|
+
assembly,C64 Programmer’s Reference Guide (text dump),https://www.zimmers.net/cbmpics/cbm/c64/c64prg.txt,1
|
|
18
|
+
basic,Assorted C64 BASIC sources and PRGs,https://github.com/spiroharvey/c64,10
|
|
19
|
+
basic,BASIC demo PRGs,https://www.zimmers.net/anonftp/pub/cbm/c64/demos/generic/poor/,2
|
|
20
|
+
basic,C64 type-in programs from Compute!/Gazette (includes .prg and .bas),https://github.com/xopowoparvo/C64-type-in-programs,10
|
|
21
|
+
basic,Collection of BASIC programs (PRG + ASCII),https://github.com/robfromoz/C64-BASIC,10
|
|
22
|
+
#basic,Compute!'s Gazette disks (.d64) – complete archive,https://archive.org/details/computes.gazette,1
|
|
23
|
+
#basic,Misc BASIC programs by Howard Feldman (miscprog.txt),https://www.zimmers.net/anonftp/pub/cbm/c64/utilities/miscprog.lnx.gz,1
|
|
24
|
+
basic,Simple C64 BASIC and Assembly examples,https://github.com/cslarsen/c64-examples,10
|
|
25
|
+
basic,Snake clone in pure C64 BASIC (snake.bas),https://github.com/StefanHolmes/C64-Snake,10
|
|
26
|
+
hardware,Ultimate C64 Reference Guide,https://github.com/mist64/c64ref,10
|
|
27
|
+
hardware,61002 CPU notes (text),https://raw.githubusercontent.com/androdev4u/c64rom/refs/heads/master/61002/cpu_61002.txt,1
|
|
28
|
+
hardware,Mapping the C64,https://www.zimmers.net/anonftp/pub/cbm/c64/manuals/mapping-c64.txt,1
|
|
29
|
+
hardware,C64 Service Manual (text),https://www.zimmers.net/cbmpics/cbm/c64/C64ServiceManual.txt,1
|
|
30
|
+
hardware,Commodore 64 memory map (text),https://www.zimmers.net/anonftp/pub/cbm/maps/C64.MemoryMap.txt,1
|
|
31
|
+
hardware,VIC-II controller documentation (text),https://www.zimmers.net/cbmpics/cbm/c64/vic-ii.txt,1
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
C64 Bridge - URL/domain utilities
|
|
3
|
+
GPL-2.0-only
|
|
4
|
+
*/
|
|
5
|
+
import { URL } from 'node:url';
|
|
6
|
+
/**
|
|
7
|
+
* Very small public-suffix-like heuristic to derive the registered domain.
|
|
8
|
+
* Avoids extra deps; not perfect but works for common TLDs.
|
|
9
|
+
*/
|
|
10
|
+
export function getRegisteredDomain(hostname) {
|
|
11
|
+
const parts = hostname.toLowerCase().split('.');
|
|
12
|
+
if (parts.length <= 2)
|
|
13
|
+
return hostname.toLowerCase();
|
|
14
|
+
const tld = parts.slice(-2).join('.');
|
|
15
|
+
// Handle some common multi-part TLDs
|
|
16
|
+
const multi = new Set([
|
|
17
|
+
'co.uk', 'ac.uk', 'gov.uk', 'org.uk', 'co.jp', 'com.au', 'net.au', 'org.au',
|
|
18
|
+
]);
|
|
19
|
+
if (multi.has(parts.slice(-3).join('.'))) {
|
|
20
|
+
return parts.slice(-3).join('.');
|
|
21
|
+
}
|
|
22
|
+
return tld;
|
|
23
|
+
}
|
|
24
|
+
export function parseUrlSafe(input) {
|
|
25
|
+
try {
|
|
26
|
+
return new URL(input);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function sameRegisteredDomain(a, b) {
|
|
33
|
+
const ua = parseUrlSafe(a);
|
|
34
|
+
const ub = parseUrlSafe(b);
|
|
35
|
+
if (!ua || !ub)
|
|
36
|
+
return false;
|
|
37
|
+
return getRegisteredDomain(ua.hostname) === getRegisteredDomain(ub.hostname);
|
|
38
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* sidplayfp Runner Utility for converting SID files to WAV output.
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
export class SidplayExecutionError extends Error {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
const first = options.stderr.subarray(0, 1000).toString("utf8");
|
|
10
|
+
const last = options.stderr.length > 1000
|
|
11
|
+
? options.stderr.subarray(options.stderr.length - 1000).toString("utf8")
|
|
12
|
+
: "";
|
|
13
|
+
super(`${options.message}\n` +
|
|
14
|
+
`command: ${options.command}\n` +
|
|
15
|
+
`exitCode: ${String(options.exitCode)}\n` +
|
|
16
|
+
`stderr(first 1000):\n${first}\n` +
|
|
17
|
+
(last ? `stderr(last 1000):\n${last}\n` : "") +
|
|
18
|
+
`wav: exists=${options.wavExists}, size=${options.wavSize}`);
|
|
19
|
+
this.name = "SidplayExecutionError";
|
|
20
|
+
this.command = options.command;
|
|
21
|
+
this.exitCode = options.exitCode;
|
|
22
|
+
this.stderrFirst = first;
|
|
23
|
+
this.stderrLast = last;
|
|
24
|
+
this.wavExists = options.wavExists;
|
|
25
|
+
this.wavSize = options.wavSize;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function resolveMode(input) {
|
|
29
|
+
const envMode = (process.env.SIDPLAY_MODE || "").toLowerCase();
|
|
30
|
+
const raw = (input || envMode || "ntsc").toLowerCase();
|
|
31
|
+
return raw === "pal" ? "pal" : "ntsc";
|
|
32
|
+
}
|
|
33
|
+
function resolveLimitCycles(input) {
|
|
34
|
+
const fromEnv = Number(process.env.SIDPLAY_LIMIT_CYCLES);
|
|
35
|
+
if (Number.isFinite(fromEnv) && fromEnv > 0)
|
|
36
|
+
return Math.floor(fromEnv);
|
|
37
|
+
if (typeof input === "number" && input > 0)
|
|
38
|
+
return Math.floor(input);
|
|
39
|
+
return 120000000;
|
|
40
|
+
}
|
|
41
|
+
function resolveBinary(input) {
|
|
42
|
+
return process.env.SIDPLAY_BINARY || process.env.SIDPLAYFP_BINARY || input || "sidplayfp";
|
|
43
|
+
}
|
|
44
|
+
function which(binary) {
|
|
45
|
+
const hasSep = binary.includes("/") || binary.includes("\\");
|
|
46
|
+
const pathExts = process.platform === "win32"
|
|
47
|
+
? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").map((ext) => ext.startsWith(".") ? ext : `.${ext}`)
|
|
48
|
+
: [""];
|
|
49
|
+
const tryResolve = (base) => {
|
|
50
|
+
for (const ext of pathExts) {
|
|
51
|
+
const candidate = ext ? `${base}${ext}` : base;
|
|
52
|
+
try {
|
|
53
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
if (hasSep) {
|
|
62
|
+
return tryResolve(binary);
|
|
63
|
+
}
|
|
64
|
+
const envPath = process.env.PATH || "";
|
|
65
|
+
for (const dir of envPath.split(path.delimiter)) {
|
|
66
|
+
if (!dir)
|
|
67
|
+
continue;
|
|
68
|
+
const resolved = tryResolve(path.join(dir, binary));
|
|
69
|
+
if (resolved)
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function ensureParentDir(filePath) {
|
|
75
|
+
const dir = path.dirname(filePath);
|
|
76
|
+
if (!fs.existsSync(dir))
|
|
77
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
const PAL_CLOCK = 985248;
|
|
80
|
+
const NTSC_CLOCK = 1022727;
|
|
81
|
+
function cyclesToSeconds(cycles, mode) {
|
|
82
|
+
const clock = mode === "pal" ? PAL_CLOCK : NTSC_CLOCK;
|
|
83
|
+
return Math.max(1, Math.ceil(cycles / clock));
|
|
84
|
+
}
|
|
85
|
+
function buildSidplayArgs(params, seconds) {
|
|
86
|
+
const args = [];
|
|
87
|
+
if (params.mode === "pal") {
|
|
88
|
+
args.push("-vp");
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
args.push("-vn");
|
|
92
|
+
}
|
|
93
|
+
args.push("-f44100");
|
|
94
|
+
args.push("-p16");
|
|
95
|
+
args.push(`-t${seconds}`);
|
|
96
|
+
args.push(`-w${params.wavPath}`);
|
|
97
|
+
if (typeof params.tune === "number" && Number.isFinite(params.tune)) {
|
|
98
|
+
const tuneIdx = Math.max(1, Math.floor(params.tune));
|
|
99
|
+
args.push(`-o${tuneIdx}`);
|
|
100
|
+
}
|
|
101
|
+
args.push(params.sidPath);
|
|
102
|
+
return args;
|
|
103
|
+
}
|
|
104
|
+
function parseWavHeader(buffer) {
|
|
105
|
+
if (buffer.length < 44)
|
|
106
|
+
throw new Error("WAV too small");
|
|
107
|
+
if (buffer.toString("ascii", 0, 4) !== "RIFF")
|
|
108
|
+
throw new Error("WAV missing RIFF header");
|
|
109
|
+
if (buffer.toString("ascii", 8, 12) !== "WAVE")
|
|
110
|
+
throw new Error("WAV missing WAVE header");
|
|
111
|
+
let offset = 12;
|
|
112
|
+
let fmt = null;
|
|
113
|
+
let dataBytes = 0;
|
|
114
|
+
while (offset + 8 <= buffer.length) {
|
|
115
|
+
const id = buffer.toString("ascii", offset, offset + 4);
|
|
116
|
+
const size = buffer.readUInt32LE(offset + 4);
|
|
117
|
+
offset += 8;
|
|
118
|
+
if (id === "fmt ") {
|
|
119
|
+
const audioFormat = buffer.readUInt16LE(offset + 0);
|
|
120
|
+
const numChannels = buffer.readUInt16LE(offset + 2);
|
|
121
|
+
const sampleRate = buffer.readUInt32LE(offset + 4);
|
|
122
|
+
const bitsPerSample = buffer.readUInt16LE(offset + 14);
|
|
123
|
+
fmt = { audioFormat, numChannels, sampleRate, bitsPerSample, dataBytes: 0 };
|
|
124
|
+
}
|
|
125
|
+
else if (id === "data") {
|
|
126
|
+
dataBytes = size;
|
|
127
|
+
}
|
|
128
|
+
offset += size;
|
|
129
|
+
}
|
|
130
|
+
if (!fmt)
|
|
131
|
+
throw new Error("WAV missing fmt chunk");
|
|
132
|
+
if (dataBytes <= 0)
|
|
133
|
+
throw new Error("WAV missing data chunk");
|
|
134
|
+
fmt.dataBytes = dataBytes;
|
|
135
|
+
return fmt;
|
|
136
|
+
}
|
|
137
|
+
function quoteArg(a) {
|
|
138
|
+
if (/^[A-Za-z0-9_\-\.\/]+$/.test(a))
|
|
139
|
+
return a;
|
|
140
|
+
return `'${a.replace(/'/g, "'\\''")}'`;
|
|
141
|
+
}
|
|
142
|
+
export async function runSidToWav(params) {
|
|
143
|
+
const mode = resolveMode(params.mode);
|
|
144
|
+
const limitCycles = resolveLimitCycles(params.limitCycles);
|
|
145
|
+
const binary = resolveBinary(params.binary);
|
|
146
|
+
if (!params.sidPath)
|
|
147
|
+
throw new Error("sidPath is required");
|
|
148
|
+
if (!params.wavPath)
|
|
149
|
+
throw new Error("wavPath is required");
|
|
150
|
+
if (!fs.existsSync(params.sidPath))
|
|
151
|
+
throw new Error(`SID not found: ${params.sidPath}`);
|
|
152
|
+
ensureParentDir(params.wavPath);
|
|
153
|
+
const full = {
|
|
154
|
+
sidPath: params.sidPath,
|
|
155
|
+
wavPath: params.wavPath,
|
|
156
|
+
mode,
|
|
157
|
+
limitCycles,
|
|
158
|
+
tune: typeof params.tune === "number" ? params.tune : undefined,
|
|
159
|
+
binary,
|
|
160
|
+
};
|
|
161
|
+
const binaryPath = which(full.binary);
|
|
162
|
+
if (!binaryPath) {
|
|
163
|
+
const installHint = `sidplayfp not found. Install via e.g. \n` +
|
|
164
|
+
` sudo apt-get install sidplayfp\n` +
|
|
165
|
+
` brew install sidplayfp\n` +
|
|
166
|
+
` choco install sidplayfp\n`;
|
|
167
|
+
const msg = Buffer.from(`${full.binary} not found in PATH.\n${installHint}`);
|
|
168
|
+
throw new SidplayExecutionError({
|
|
169
|
+
message: "sidplayfp binary is not installed",
|
|
170
|
+
command: full.binary,
|
|
171
|
+
exitCode: 127,
|
|
172
|
+
stderr: msg,
|
|
173
|
+
wavExists: false,
|
|
174
|
+
wavSize: 0,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const seconds = cyclesToSeconds(full.limitCycles, full.mode);
|
|
178
|
+
const args = buildSidplayArgs(full, seconds);
|
|
179
|
+
const display = [binaryPath, ...args.map(quoteArg)].join(" ");
|
|
180
|
+
const stdoutChunks = [];
|
|
181
|
+
const stderrChunks = [];
|
|
182
|
+
const exitCode = await new Promise((resolve) => {
|
|
183
|
+
const child = spawn(binaryPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
184
|
+
child.stdout?.on("data", (c) => stdoutChunks.push(Buffer.from(c)));
|
|
185
|
+
child.stderr?.on("data", (c) => stderrChunks.push(Buffer.from(c)));
|
|
186
|
+
child.on("error", (err) => {
|
|
187
|
+
const enoent = (err && typeof err === "object" && "code" in err && err.code === "ENOENT");
|
|
188
|
+
const hint = Buffer.from(String(err?.message || err || "spawn error"));
|
|
189
|
+
stderrChunks.push(hint);
|
|
190
|
+
resolve(enoent ? 127 : 1);
|
|
191
|
+
});
|
|
192
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
193
|
+
});
|
|
194
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
195
|
+
const stderrBuf = Buffer.concat(stderrChunks);
|
|
196
|
+
const stderr = stderrBuf.toString("utf8");
|
|
197
|
+
const wavExists = fs.existsSync(full.wavPath);
|
|
198
|
+
const wavStat = wavExists ? fs.statSync(full.wavPath) : null;
|
|
199
|
+
const wavSize = wavStat?.size ?? 0;
|
|
200
|
+
if (exitCode !== 0) {
|
|
201
|
+
throw new SidplayExecutionError({
|
|
202
|
+
message: "sidplayfp failed (non-zero exit code)",
|
|
203
|
+
command: display,
|
|
204
|
+
exitCode,
|
|
205
|
+
stderr: stderrBuf,
|
|
206
|
+
wavExists,
|
|
207
|
+
wavSize,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (!wavExists || wavSize === 0) {
|
|
211
|
+
throw new SidplayExecutionError({
|
|
212
|
+
message: "sidplayfp did not produce a WAV file",
|
|
213
|
+
command: display,
|
|
214
|
+
exitCode,
|
|
215
|
+
stderr: stderrBuf,
|
|
216
|
+
wavExists,
|
|
217
|
+
wavSize,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const header = fs.readFileSync(full.wavPath);
|
|
221
|
+
const info = parseWavHeader(header);
|
|
222
|
+
if (info.audioFormat !== 1 || info.sampleRate !== 44100 || info.bitsPerSample !== 16) {
|
|
223
|
+
throw new SidplayExecutionError({
|
|
224
|
+
message: `Unexpected WAV format (expected PCM 16-bit @ 44100 Hz); got format=${info.audioFormat}, bits=${info.bitsPerSample}, rate=${info.sampleRate}`,
|
|
225
|
+
command: display,
|
|
226
|
+
exitCode,
|
|
227
|
+
stderr: stderrBuf,
|
|
228
|
+
wavExists,
|
|
229
|
+
wavSize,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
command: display,
|
|
234
|
+
exitCode: exitCode ?? 0,
|
|
235
|
+
stdout,
|
|
236
|
+
stderr,
|
|
237
|
+
wavPath: full.wavPath,
|
|
238
|
+
wavInfo: info,
|
|
239
|
+
};
|
|
240
|
+
}
|
package/dist/sidwave.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
C64 Bridge - SIDWAVE Parser and Types
|
|
3
|
+
Copyright (C) 2025 Christian Gleissner
|
|
4
|
+
|
|
5
|
+
Licensed under the GNU General Public License v2.0 or later.
|
|
6
|
+
See <https://www.gnu.org/licenses/> for details.
|
|
7
|
+
*/
|
|
8
|
+
import pkg from 'yaml';
|
|
9
|
+
const { parse: yamlParse } = pkg;
|
|
10
|
+
export function parseSidwave(input) {
|
|
11
|
+
const obj = typeof input === "string" ? yamlParse(input) : input;
|
|
12
|
+
if (!obj || typeof obj !== "object") {
|
|
13
|
+
throw new Error("Invalid SIDWAVE: not an object");
|
|
14
|
+
}
|
|
15
|
+
// Detect legacy CPG shape vs new SIDWAVE top-level
|
|
16
|
+
const legacy = !!obj.song;
|
|
17
|
+
const header = legacy
|
|
18
|
+
? { title: obj.song?.title, tempo: obj.song?.tempo, mode: obj.song?.mode, length_bars: obj.song?.length_bars, global_fx: obj.song?.global_fx }
|
|
19
|
+
: { title: obj.title, author: obj.author, tempo: obj.tempo, mode: obj.mode, length_bars: obj, global_fx: obj.global_fx };
|
|
20
|
+
const title = header.title ?? "Untitled";
|
|
21
|
+
const tempo = normalizeTempo(header.tempo);
|
|
22
|
+
const mode = header.mode ?? "PAL";
|
|
23
|
+
const length_bars = Math.max(1, Math.floor(header.length_bars ?? inferBarsFromTimeline(obj.timeline) ?? 16));
|
|
24
|
+
const global_fx = header.global_fx ?? {};
|
|
25
|
+
const song = { title, tempo, mode, length_bars, global_fx };
|
|
26
|
+
const voices = (legacy ? obj.voices : obj.voices) ?? [];
|
|
27
|
+
if (!Array.isArray(voices) || voices.length === 0)
|
|
28
|
+
throw new Error("Invalid SIDWAVE: missing voices");
|
|
29
|
+
const normalizedVoices = voices.map((v) => normalizeVoice(v));
|
|
30
|
+
const timeline = ((legacy ? obj.timeline : obj.timeline) ?? []).map((t) => normalizeTimelineSection(t));
|
|
31
|
+
if (!Array.isArray(timeline) || timeline.length === 0)
|
|
32
|
+
throw new Error("Invalid SIDWAVE: missing timeline");
|
|
33
|
+
return { song, voices: normalizedVoices, timeline };
|
|
34
|
+
}
|
|
35
|
+
function inferBarsFromTimeline(t) {
|
|
36
|
+
if (!Array.isArray(t) || t.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
return t.reduce((sum, s) => sum + Math.max(1, Math.floor(s.bars ?? 0)), 0);
|
|
39
|
+
}
|
|
40
|
+
function normalizeTempo(tempo) {
|
|
41
|
+
if (!Number.isFinite(tempo))
|
|
42
|
+
return 100;
|
|
43
|
+
const t = Math.max(30, Math.min(220, Math.floor(tempo)));
|
|
44
|
+
return t;
|
|
45
|
+
}
|
|
46
|
+
function normalizeVoice(v) {
|
|
47
|
+
const waveform = normalizeWaveform(v.waveform ?? "pulse");
|
|
48
|
+
let pulseWidth = v.pulse_width;
|
|
49
|
+
if (typeof pulseWidth === "number") {
|
|
50
|
+
if (pulseWidth > 0 && pulseWidth <= 1) {
|
|
51
|
+
pulseWidth = Math.round(pulseWidth * 4095);
|
|
52
|
+
}
|
|
53
|
+
pulseWidth = Math.max(0, Math.min(4095, Math.floor(pulseWidth)));
|
|
54
|
+
}
|
|
55
|
+
const adsr = (v.adsr ?? [2, 2, 10, 3]).map((n) => Math.max(0, Math.min(15, Math.floor(n))));
|
|
56
|
+
return {
|
|
57
|
+
id: v.id,
|
|
58
|
+
name: v.name ?? `Voice${v.id}`,
|
|
59
|
+
waveform,
|
|
60
|
+
pulse_width: pulseWidth,
|
|
61
|
+
adsr,
|
|
62
|
+
patterns: v.patterns ?? {},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function normalizeWaveform(w) {
|
|
66
|
+
const w0 = (w || "pulse").toLowerCase();
|
|
67
|
+
if (w0 === "saw")
|
|
68
|
+
return "sawtooth";
|
|
69
|
+
if (w0 === "tri")
|
|
70
|
+
return "triangle";
|
|
71
|
+
if (w0 === "pulse" || w0 === "sawtooth" || w0 === "triangle" || w0 === "noise")
|
|
72
|
+
return w0;
|
|
73
|
+
return "pulse";
|
|
74
|
+
}
|
|
75
|
+
function normalizeTimelineSection(t) {
|
|
76
|
+
const bars = Math.max(1, Math.floor(t?.bars ?? 1));
|
|
77
|
+
const layers = {};
|
|
78
|
+
for (const [k, v] of Object.entries(t?.layers ?? {})) {
|
|
79
|
+
layers[k] = String(v ?? "-");
|
|
80
|
+
}
|
|
81
|
+
return { section: t.section ?? "", bars, layers };
|
|
82
|
+
}
|
|
83
|
+
export function noteNameToMidi(note) {
|
|
84
|
+
const m = /^([A-Ga-g])([#b]?)(-?\d+)$/.exec((note || "").trim());
|
|
85
|
+
if (!m)
|
|
86
|
+
return undefined;
|
|
87
|
+
const letter = m[1].toUpperCase();
|
|
88
|
+
const accidental = m[2];
|
|
89
|
+
const octave = Number(m[3]);
|
|
90
|
+
const base = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
91
|
+
let semi = base[letter] ?? 0;
|
|
92
|
+
if (accidental === "#")
|
|
93
|
+
semi += 1;
|
|
94
|
+
if (accidental === "b")
|
|
95
|
+
semi -= 1;
|
|
96
|
+
const midi = (octave + 1) * 12 + semi;
|
|
97
|
+
return midi;
|
|
98
|
+
}
|
|
99
|
+
export function midiToHz(midi) {
|
|
100
|
+
return 440 * Math.pow(2, (midi - 69) / 12);
|
|
101
|
+
}
|