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.
Files changed (138) hide show
  1. package/.github/prompts/README.md +6 -0
  2. package/.github/prompts/assembly-routine-session.prompt.md +13 -0
  3. package/.github/prompts/basic-program-session.prompt.md +13 -0
  4. package/.github/prompts/drive-management-session.prompt.md +13 -0
  5. package/.github/prompts/petscii-art-session.prompt.md +13 -0
  6. package/.github/prompts/play-classic-game.prompt.md +13 -0
  7. package/.github/prompts/sid-music-session.prompt.md +13 -0
  8. package/AGENTS.md +173 -0
  9. package/LICENSE +339 -0
  10. package/README.md +582 -0
  11. package/data/README.md +16 -0
  12. package/data/api/basic-api-spec.md +154 -0
  13. package/data/api/kernal-api-spec.md +68 -0
  14. package/data/assembly/assembly-spec.md +196 -0
  15. package/data/assembly/examples/effects/sinewave.asm +5 -0
  16. package/data/assembly/examples/graphics/sprite/mover.asm +15 -0
  17. package/data/assembly/examples/sid-file-structure.md +8 -0
  18. package/data/assembly/examples/sid-overview.md +11 -0
  19. package/data/audio/examples/Biosphere.sid +0 -0
  20. package/data/audio/sid-file-structure.md +91 -0
  21. package/data/audio/sid-programming-best-practices.md +198 -0
  22. package/data/audio/sid-spec.md +166 -0
  23. package/data/audio/sidwave.md +131 -0
  24. package/data/audio/sidwave.schema.json +64 -0
  25. package/data/basic/basic-spec.md +230 -0
  26. package/data/basic/examples/audio/alle-meine-entchen-expressive.bas +41 -0
  27. package/data/basic/examples/audio/c-major-scale-pleasant.bas +29 -0
  28. package/data/basic/examples/io/joystick.bas +5 -0
  29. package/data/basic/examples/video/bounce.bas +8 -0
  30. package/data/basic/examples/video/entchen-petscii.bas +16 -0
  31. package/data/basic/examples/video/hello-world.bas +4 -0
  32. package/data/basic/examples/video/wave.bas +7 -0
  33. package/data/context/bootstrap.md +148 -0
  34. package/data/context/chat.md +36 -0
  35. package/data/drive/drive-spec.md +215 -0
  36. package/data/embeddings_asm.json +1 -0
  37. package/data/embeddings_basic.json +1 -0
  38. package/data/embeddings_hardware.json +1 -0
  39. package/data/embeddings_mixed.json +1 -0
  40. package/data/embeddings_other.json +1 -0
  41. package/data/io/cia-spec.md +115 -0
  42. package/data/io/io-spec.md +124 -0
  43. package/data/memory/kernal-memory-map.md +227 -0
  44. package/data/memory/low-memory-map.md +133 -0
  45. package/data/memory/memory-map.md +186 -0
  46. package/data/printer/printer-commodore-bitmap.md +31 -0
  47. package/data/printer/printer-commodore.md +28 -0
  48. package/data/printer/printer-epson-bitmap.md +29 -0
  49. package/data/printer/printer-epson.md +30 -0
  50. package/data/printer/printer-prompts.md +29 -0
  51. package/data/printer/printer-spec.md +180 -0
  52. package/data/video/character-set.csv +257 -0
  53. package/data/video/vic-spec.md +276 -0
  54. package/dist/assemblyConverter.js +1299 -0
  55. package/dist/audio/record_and_analyze_audio.js +292 -0
  56. package/dist/basicConverter.js +193 -0
  57. package/dist/bootstrap/stdio-logger.js +32 -0
  58. package/dist/c64Client.js +1052 -0
  59. package/dist/chargen.js +197 -0
  60. package/dist/config.js +142 -0
  61. package/dist/context.js +157 -0
  62. package/dist/device.js +413 -0
  63. package/dist/index.js +7 -0
  64. package/dist/knowledge.js +454 -0
  65. package/dist/logger.js +182 -0
  66. package/dist/loggingHttpClient.js +88 -0
  67. package/dist/mcp-server.js +426 -0
  68. package/dist/petscii.js +135 -0
  69. package/dist/petsciiArt.js +356 -0
  70. package/dist/platform.js +82 -0
  71. package/dist/prompts/registry.js +560 -0
  72. package/dist/rag/discover.config.json +41 -0
  73. package/dist/rag/discover.js +401 -0
  74. package/dist/rag/embeddings.js +60 -0
  75. package/dist/rag/externalFetcher.js +934 -0
  76. package/dist/rag/indexer.js +770 -0
  77. package/dist/rag/init.js +139 -0
  78. package/dist/rag/knowledgeIndex.js +361 -0
  79. package/dist/rag/loggingRetriever.js +46 -0
  80. package/dist/rag/rateLimiter.js +127 -0
  81. package/dist/rag/retriever.js +74 -0
  82. package/dist/rag/sources.csv +31 -0
  83. package/dist/rag/types.js +5 -0
  84. package/dist/rag/urlUtils.js +38 -0
  85. package/dist/sidplayRunner.js +240 -0
  86. package/dist/sidwave.js +101 -0
  87. package/dist/sidwaveCompiler.js +329 -0
  88. package/dist/tools/audio.js +944 -0
  89. package/dist/tools/developer.js +515 -0
  90. package/dist/tools/errors.js +59 -0
  91. package/dist/tools/graphics.js +469 -0
  92. package/dist/tools/machineControl.js +267 -0
  93. package/dist/tools/memory.js +224 -0
  94. package/dist/tools/printer.js +482 -0
  95. package/dist/tools/programRunners.js +315 -0
  96. package/dist/tools/rag.js +153 -0
  97. package/dist/tools/registry.js +58 -0
  98. package/dist/tools/responses.js +36 -0
  99. package/dist/tools/schema.js +304 -0
  100. package/dist/tools/storage.js +740 -0
  101. package/dist/tools/streaming.js +132 -0
  102. package/dist/tools/types.js +99 -0
  103. package/dist/viceRunner.js +239 -0
  104. package/doc/MCP_SETUP.md +153 -0
  105. package/doc/README.md +7 -0
  106. package/doc/developer.md +312 -0
  107. package/doc/img/logo.png +0 -0
  108. package/doc/img/prompts/duck_petscii.png +0 -0
  109. package/doc/img/prompts/duck_song.png +0 -0
  110. package/doc/img/vscode/vscode-copilot-c64-chat-mode.png +0 -0
  111. package/doc/img/vscode/vscode-copilot-hello-world.png +0 -0
  112. package/doc/prompts/README.md +96 -0
  113. package/doc/prompts/user-prompt-ideas.md +147 -0
  114. package/doc/rest/c64-openapi.yaml +979 -0
  115. package/doc/rest/c64-rest-api.md +106 -0
  116. package/doc/troubleshooting-mcp.md +177 -0
  117. package/generated/c64/index.js +857 -0
  118. package/mcp.json +106 -0
  119. package/package.json +79 -0
  120. package/scripts/c64-cli.mjs +142 -0
  121. package/scripts/c64-tool.sh +83 -0
  122. package/scripts/check-package.mjs +89 -0
  123. package/scripts/cli.js +61 -0
  124. package/scripts/fetch-external-sources.mjs +67 -0
  125. package/scripts/generate-changelog.mjs +131 -0
  126. package/scripts/postbuild.mjs +101 -0
  127. package/scripts/prepare-release.mjs +58 -0
  128. package/scripts/rag-rebuild.mjs +53 -0
  129. package/scripts/register-ts-node.mjs +6 -0
  130. package/scripts/run-mcp-check.sh +104 -0
  131. package/scripts/run-tests.mjs +195 -0
  132. package/scripts/sid2wav.sh +94 -0
  133. package/scripts/smoke-run.sh +65 -0
  134. package/scripts/start-mock-c64.mjs +51 -0
  135. package/scripts/start-mock.mjs +31 -0
  136. package/scripts/start.mjs +72 -0
  137. package/scripts/update-readme.ts +164 -0
  138. package/scripts/verify-package.mjs +147 -0
@@ -0,0 +1,329 @@
1
+ /*
2
+ C64 Bridge - SIDWAVE to PRG Compiler
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 { Buffer } from "node:buffer";
9
+ import { assemblyToPrg } from "./assemblyConverter.js";
10
+ import { midiToHz, noteNameToMidi } from "./sidwave.js";
11
+ /**
12
+ * Small SIDWAVE to PRG compiler: generates a 50/60Hz player that writes SID registers from a precomputed frame table.
13
+ * It currently supports three voices with per-frame frequency, waveform, pulse width, and ADSR gate on/off.
14
+ * Effects are not fully implemented; basic PWM sweep from pulse_width and simple transpositions are handled.
15
+ */
16
+ export function compileSidwaveToPrg(doc) {
17
+ const system = (doc.song.mode ?? "PAL");
18
+ const fps = system === "PAL" ? 50 : 60;
19
+ // Estimate total frames by bars at 4/4 with quarter note = 1 beat = 1/ (tempo/60) seconds.
20
+ const beatsPerBar = 4;
21
+ const secondsPerBeat = 60 / (doc.song.tempo ?? 100);
22
+ const framesPerBeat = Math.max(1, Math.round(secondsPerBeat * fps));
23
+ const totalBeats = (doc.song.length_bars ?? 16) * beatsPerBar;
24
+ const totalFrames = Math.max(framesPerBeat * totalBeats, fps * 15); // ensure at least ~15s of content
25
+ const cappedFrames = Math.min(totalFrames, 255); // v1 player index Y wraps at 255
26
+ // Build per-voice frame data arrays.
27
+ const voices = [1, 2, 3].map((id) => doc.voices.find((v) => v.id === id) ?? { id, name: `Voice${id}`, waveform: "triangle", adsr: [2, 2, 10, 3], pulse_width: 2048, patterns: {} });
28
+ const freqFrames = [new Uint16Array(cappedFrames), new Uint16Array(cappedFrames), new Uint16Array(cappedFrames)];
29
+ const ctrlFrames = [new Uint8Array(cappedFrames), new Uint8Array(cappedFrames), new Uint8Array(cappedFrames)];
30
+ const pwFrames = [new Uint16Array(cappedFrames), new Uint16Array(cappedFrames), new Uint16Array(cappedFrames)];
31
+ const adFrames = [new Uint8Array(cappedFrames), new Uint8Array(cappedFrames), new Uint8Array(cappedFrames)];
32
+ const srFrames = [new Uint8Array(cappedFrames), new Uint8Array(cappedFrames), new Uint8Array(cappedFrames)];
33
+ // Build frames from timeline/patterns where possible
34
+ const framesForVoice = [[], [], []];
35
+ // Precompute frames-per-bar
36
+ const framesPerBar = framesPerBeat * beatsPerBar;
37
+ for (let vi = 0; vi < 3; vi += 1) {
38
+ const v = voices[vi];
39
+ const adsr = v.adsr ?? [2, 2, 10, 3];
40
+ const ad = ((adsr[0] & 0xf) << 4) | (adsr[1] & 0xf);
41
+ const sr = ((adsr[2] & 0xf) << 4) | (adsr[3] & 0xf);
42
+ adFrames[vi].fill(ad);
43
+ srFrames[vi].fill(sr);
44
+ const waveformCtrl = waveformToCtrl(v.waveform ?? "pulse");
45
+ const basePw = Math.max(0, Math.min(0x0fff, Math.floor(v.pulse_width ?? 0x0800)));
46
+ // Collect a note per frame using timeline mapping (fallback to simple arp)
47
+ const notesForSong = [];
48
+ const patterns = (v.patterns ?? {});
49
+ const voiceKeyCandidates = [
50
+ `v${v.id}`,
51
+ `voice${v.id}`,
52
+ `${v.id}`,
53
+ ];
54
+ for (const section of doc.timeline ?? []) {
55
+ // Pick the first matching layer key that maps to this voice
56
+ let patternName;
57
+ for (const key of voiceKeyCandidates) {
58
+ if (section.layers && section.layers[key]) {
59
+ patternName = String(section.layers[key]);
60
+ break;
61
+ }
62
+ }
63
+ const bars = Math.max(1, Math.floor(section.bars ?? 1));
64
+ const totalFramesInSection = bars * framesPerBar;
65
+ const pat = (patternName ? patterns[patternName] : undefined);
66
+ if (!pat) {
67
+ // Fill section with sustained default note
68
+ for (let i = 0; i < totalFramesInSection; i += 1)
69
+ notesForSong.push("C3");
70
+ continue;
71
+ }
72
+ if (pat.type === "arpeggio" && Array.isArray(pat.notes) && pat.notes.length) {
73
+ const stepFrames = Math.max(1, Math.round((pat.frame_rate ? fps / pat.frame_rate : framesPerBeat)));
74
+ let idx = 0;
75
+ for (let f = 0; f < totalFramesInSection; f += 1) {
76
+ if (f % stepFrames === 0)
77
+ idx = (idx + 1) % pat.notes.length;
78
+ notesForSong.push(String(pat.notes[idx]));
79
+ }
80
+ }
81
+ else if (Array.isArray(pat.groove) && pat.groove.length) {
82
+ const stepFrames = framesPerBeat; // one note per beat
83
+ let gi = 0;
84
+ for (let f = 0; f < totalFramesInSection; f += 1) {
85
+ if (f % stepFrames === 0)
86
+ gi = (gi + 1) % pat.groove.length;
87
+ notesForSong.push(String(pat.groove[gi]));
88
+ }
89
+ }
90
+ else if (typeof pat.motif === "string" && pat.motif.trim().length > 0) {
91
+ const intervals = pat.motif
92
+ .split(/[\s,]+/)
93
+ .map((x) => Number.parseInt(x, 10))
94
+ .filter((n) => Number.isFinite(n));
95
+ const lengthBeats = Math.max(1, Math.floor(pat.length ?? 1));
96
+ const stepFrames = framesPerBeat * lengthBeats;
97
+ const baseNote = "C3";
98
+ let mi = 0;
99
+ for (let f = 0; f < totalFramesInSection; f += 1) {
100
+ if (f % stepFrames === 0)
101
+ mi = (mi + 1) % Math.max(1, intervals.length);
102
+ const iv = intervals.length ? intervals[mi] : 0;
103
+ notesForSong.push(transposeNote(baseNote, iv));
104
+ }
105
+ }
106
+ else {
107
+ // Unknown pattern shape; fill with default sustained
108
+ for (let i = 0; i < totalFramesInSection; i += 1)
109
+ notesForSong.push("C3");
110
+ }
111
+ }
112
+ // Ensure we have at least cappedFrames entries
113
+ if (notesForSong.length < cappedFrames) {
114
+ const fallback = inferNotesForVoice(v) ?? ["C3", "E3", "G3", "B2"];
115
+ let i = 0;
116
+ while (notesForSong.length < cappedFrames) {
117
+ notesForSong.push(fallback[i % fallback.length]);
118
+ i += 1;
119
+ }
120
+ }
121
+ const fx = inferVoiceFx(v);
122
+ const vibDepthSemis = fx.vibrato?.depth ?? 0;
123
+ const vibRateHz = fx.vibrato?.rate ?? 0;
124
+ const pwmDepth = fx.pwm_sweep?.depth ?? 0;
125
+ const pwmSpeed = fx.pwm_sweep?.speed ?? 0;
126
+ for (let f = 0; f < cappedFrames; f += 1) {
127
+ const note = notesForSong[f] ?? notesForSong[notesForSong.length - 1] ?? "C3";
128
+ const midi = noteNameToMidi(note) ?? 48;
129
+ const vib = vibDepthSemis > 0 && vibRateHz > 0 ? vibDepthSemis * Math.sin((2 * Math.PI * vibRateHz * f) / fps) : 0;
130
+ const midiWithVib = midi + vib;
131
+ const hz = midiToHz(midiWithVib);
132
+ const sidFreq = hzToSidFrequency(hz, system);
133
+ freqFrames[vi][f] = sidFreq;
134
+ ctrlFrames[vi][f] = waveformCtrl | 0x01; // GATE on
135
+ const baseSweep = pwmDepth > 0 && pwmSpeed > 0 ? pwmDepth * Math.sin((2 * Math.PI * pwmSpeed * f) / fps) : 0;
136
+ const lfo = (String(v.waveform).toLowerCase().startsWith("pulse")) ? baseSweep : 0;
137
+ const pw = (String(v.waveform).toLowerCase().startsWith("pulse") ? Math.max(0, Math.min(0x0fff, Math.floor(basePw + lfo))) : basePw) & 0x0fff;
138
+ pwFrames[vi][f] = pw;
139
+ }
140
+ }
141
+ const entryAddress = 0x0810;
142
+ const asm = buildPlayerAsm({ system, totalFrames: cappedFrames, freqFrames, ctrlFrames, pwFrames, adFrames, srFrames, entryAddress });
143
+ const prg = assemblyToPrg(asm, { fileName: "sidwave_player.asm", loadAddress: 0x0801 });
144
+ return { prg, entryAddress };
145
+ }
146
+ /**
147
+ * Build a minimal PSID v2 header and embed the PRG body, so clients can POST via sidplay attachment.
148
+ * The PRG must contain a 2-byte load address followed by code. We keep loadAddress=0 (derive from data), init/play=$0810.
149
+ */
150
+ export function compileSidwaveToSid(doc, prg, options) {
151
+ const headerSize = 124; // PSID v2 header size
152
+ const header = Buffer.alloc(headerSize, 0);
153
+ header.write("PSID", 0, "ascii");
154
+ header.writeUInt16BE(2, 4); // version
155
+ header.writeUInt16BE(headerSize, 6); // data offset
156
+ header.writeUInt16BE(0, 8); // load address (0 => take from data)
157
+ const entry = options?.entryAddress ?? 0x0810;
158
+ header.writeUInt16BE(entry, 10); // init
159
+ header.writeUInt16BE(entry, 12); // play (single entry; routine self-dispatches)
160
+ header.writeUInt16BE(1, 14); // songs
161
+ header.writeUInt16BE(1, 16); // start song
162
+ header.writeUInt32BE(0, 18); // speed
163
+ // strings (32 bytes each)
164
+ const title = (doc.song.title ?? "Untitled").slice(0, 31);
165
+ header.write(title, 22, "ascii");
166
+ header.write("SIDWAVE", 54, "ascii");
167
+ header.write("MCP", 86, "ascii");
168
+ // data is PRG body; if it already has a 2-byte load address, keep it so loaders can use it
169
+ const body = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
170
+ return { sid: Buffer.concat([header, body]) };
171
+ }
172
+ function hzToSidFrequency(hz, system) {
173
+ const phi2 = system === "PAL" ? 985248 : 1022727;
174
+ const value = Math.round((hz * 65536) / phi2);
175
+ return Math.max(0, Math.min(0xffff, value));
176
+ }
177
+ function waveformToCtrl(w) {
178
+ const wf = (w || "pulse").toLowerCase();
179
+ if (wf.startsWith("tri"))
180
+ return 1 << 4;
181
+ if (wf.startsWith("saw"))
182
+ return 1 << 5;
183
+ if (wf.startsWith("pulse"))
184
+ return 1 << 6;
185
+ if (wf.startsWith("noise"))
186
+ return 1 << 7;
187
+ return 1 << 6;
188
+ }
189
+ function inferNotesForVoice(v) {
190
+ const pnames = Object.keys(v.patterns ?? {});
191
+ for (const name of pnames) {
192
+ const p = v.patterns[name];
193
+ if (!p)
194
+ continue;
195
+ if (Array.isArray(p.notes))
196
+ return p.notes;
197
+ if (Array.isArray(p.groove))
198
+ return p.groove;
199
+ }
200
+ return undefined;
201
+ }
202
+ function inferVoiceFx(v) {
203
+ const patterns = v.patterns ?? {};
204
+ const first = Object.values(patterns)[0];
205
+ const fx = (first && typeof first === "object" && first.fx) ? first.fx : {};
206
+ return {
207
+ vibrato: fx?.vibrato,
208
+ slide: fx?.slide,
209
+ pwm_sweep: fx?.pwm_sweep,
210
+ };
211
+ }
212
+ function buildPlayerAsm(args) {
213
+ const L = [];
214
+ const entryHex = hex16(args.entryAddress);
215
+ L.push(`* = $${entryHex}`);
216
+ L.push("; --- PSID single-entry init/play routine ---");
217
+ L.push("entry:");
218
+ L.push(" LDA inited");
219
+ L.push(" BNE do_play");
220
+ L.push(" JSR init");
221
+ L.push(" RTS");
222
+ L.push("do_play:");
223
+ L.push(" JSR play_frame");
224
+ L.push(" RTS");
225
+ L.push("");
226
+ L.push("init:");
227
+ L.push(" LDA #$0F");
228
+ L.push(" STA $D418");
229
+ L.push(" LDA #$00");
230
+ L.push(" STA frame_idx");
231
+ L.push(" LDA #$01");
232
+ L.push(" STA inited");
233
+ L.push(" RTS");
234
+ L.push("");
235
+ L.push("play_frame:");
236
+ L.push(" LDY frame_idx");
237
+ for (let v = 0; v < 3; v += 1) {
238
+ const base = 0xd400 + v * 7;
239
+ L.push(` ; voice ${v + 1}`);
240
+ L.push(` LDA v${v + 1}_freq_lo,Y`);
241
+ L.push(` STA $${hex16(base)}`);
242
+ L.push(` LDA v${v + 1}_freq_hi,Y`);
243
+ L.push(` STA $${hex16(base + 1)}`);
244
+ L.push(` LDA v${v + 1}_pw_lo,Y`);
245
+ L.push(` STA $${hex16(base + 2)}`);
246
+ L.push(` LDA v${v + 1}_pw_hi,Y`);
247
+ L.push(` STA $${hex16(base + 3)}`);
248
+ L.push(` LDA v${v + 1}_ctrl,Y`);
249
+ L.push(` STA $${hex16(base + 4)}`);
250
+ L.push(` LDA v${v + 1}_ad,Y`);
251
+ L.push(` STA $${hex16(base + 5)}`);
252
+ L.push(` LDA v${v + 1}_sr,Y`);
253
+ L.push(` STA $${hex16(base + 6)}`);
254
+ }
255
+ L.push(" INY");
256
+ L.push(` CPY #$${hex2(args.totalFrames & 0xff)}`);
257
+ L.push(" BCC store_idx");
258
+ L.push(" LDY #$00");
259
+ L.push("store_idx:");
260
+ L.push(" STY frame_idx");
261
+ L.push(" RTS");
262
+ L.push("");
263
+ L.push("; --- Zero page state ---");
264
+ L.push("frame_idx = $FB");
265
+ L.push("inited = $FC");
266
+ L.push("");
267
+ // Data tables
268
+ for (let v = 0; v < 3; v += 1) {
269
+ const fLo = [];
270
+ const fHi = [];
271
+ const pwLo = [];
272
+ const pwHi = [];
273
+ const ctrl = [];
274
+ const ad = [];
275
+ const sr = [];
276
+ for (let i = 0; i < args.totalFrames; i += 1) {
277
+ const f = args.freqFrames[v][i];
278
+ fLo.push(`$${hex2(f & 0xff)}`);
279
+ fHi.push(`$${hex2((f >> 8) & 0xff)}`);
280
+ const pw = args.pwFrames[v][i];
281
+ pwLo.push(`$${hex2(pw & 0xff)}`);
282
+ pwHi.push(`$${hex2((pw >> 8) & 0x0f)}`);
283
+ ctrl.push(`$${hex2(args.ctrlFrames[v][i])}`);
284
+ ad.push(`$${hex2(args.adFrames[v][i])}`);
285
+ sr.push(`$${hex2(args.srFrames[v][i])}`);
286
+ }
287
+ pushByteRows(L, `v${v + 1}_freq_lo`, fLo);
288
+ pushByteRows(L, `v${v + 1}_freq_hi`, fHi);
289
+ pushByteRows(L, `v${v + 1}_pw_lo`, pwLo);
290
+ pushByteRows(L, `v${v + 1}_pw_hi`, pwHi);
291
+ pushByteRows(L, `v${v + 1}_ctrl`, ctrl);
292
+ pushByteRows(L, `v${v + 1}_ad`, ad);
293
+ pushByteRows(L, `v${v + 1}_sr`, sr);
294
+ }
295
+ return L.join("\n");
296
+ }
297
+ function pushByteRows(out, label, items, perLine = 16) {
298
+ out.push("");
299
+ out.push(`${label}:`);
300
+ for (let i = 0; i < items.length; i += perLine) {
301
+ const row = items.slice(i, i + perLine).join(",");
302
+ out.push(` .byte ${row}`);
303
+ }
304
+ }
305
+ function hex2(n) {
306
+ return (n & 0xff).toString(16).toUpperCase().padStart(2, "0");
307
+ }
308
+ function hex16(n) {
309
+ return (n & 0xffff).toString(16).toUpperCase().padStart(4, "0");
310
+ }
311
+ function transposeNote(note, semitones) {
312
+ const m = /^([A-Ga-g])([#b]?)(-?\d+)$/.exec((note || "").trim());
313
+ if (!m)
314
+ return note;
315
+ const letter = m[1].toUpperCase();
316
+ const accidental = m[2];
317
+ const octave = Number(m[3]);
318
+ const map = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
319
+ let semi = map[letter] ?? 0;
320
+ if (accidental === "#")
321
+ semi += 1;
322
+ if (accidental === "b")
323
+ semi -= 1;
324
+ let midi = (octave + 1) * 12 + semi + semitones;
325
+ const letters = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
326
+ const newOct = Math.floor(midi / 12) - 1;
327
+ const name = letters[((midi % 12) + 12) % 12];
328
+ return `${name}${newOct}`;
329
+ }