c64bridge 1.0.4 → 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/embeddings_basic.json +1 -1
- package/data/embeddings_hardware.json +1 -1
- package/data/embeddings_mixed.json +1 -1
- package/data/embeddings_other.json +1 -1
- package/data/graphics/character-set.csv +64 -64
- package/data/sound/sidwave.md +113 -36
- package/data/sound/sidwave.schema.json +138 -28
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +144 -150
- package/dist/config.js +93 -39
- package/dist/device.js +252 -130
- package/dist/diagnostics.js +10 -2
- package/dist/knowledge.js +2 -1
- package/dist/mcp-server.js +12 -4
- package/dist/petscii.js +18 -15
- package/dist/petsciiArt.js +40 -14
- package/dist/rag/indexer.js +2 -1
- package/dist/sidwave.js +453 -61
- package/dist/sidwaveCompiler.js +1424 -275
- package/dist/tools/audio.js +97 -18
- package/dist/tools/debug.js +9 -9
- package/dist/tools/developer.js +7 -7
- package/dist/tools/errors.js +19 -1
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/hex.js +9 -0
- package/dist/tools/input.js +21 -18
- package/dist/tools/machineControl.js +6 -6
- package/dist/tools/memory.js +11 -10
- package/dist/tools/meta/audio.js +8 -2
- package/dist/tools/meta/background.js +6 -1
- package/dist/tools/meta/filesystem.js +37 -10
- package/dist/tools/meta/graphics.js +11 -16
- package/dist/tools/meta/memory.js +16 -22
- package/dist/tools/meta/program.js +28 -9
- package/dist/tools/pollValidator.js +30 -2
- package/dist/tools/printer.js +4 -4
- package/dist/tools/programRunners.js +10 -9
- 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 +192 -57
- package/dist/tools/translation/basicTokenizer.js +44 -10
- package/dist/tools/translation/opcodeInventory.js +1 -1
- package/dist/tools/types.js +11 -4
- package/dist/tools/vice.js +1 -1
- package/dist/vicBitmap.js +16 -0
- package/dist/vice/mockServer.js +99 -8
- package/dist/vice/readiness.js +4 -9
- package/dist/vice/viceClient.js +34 -6
- 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_system.schema.json +6 -4
- package/mcp/schemas/c64_vice.schema.json +3 -3
- package/mcp/server.json +1 -1
- package/mcp/tools.json +20 -14
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/mockC64Server.mjs +113 -20
- package/scripts/run-coverage.mjs +31 -1
- package/scripts/update-readme.ts +3 -2
- package/server.json +2 -2
package/dist/sidwaveCompiler.js
CHANGED
|
@@ -6,324 +6,1473 @@ Licensed under the GNU General Public License v2.0 or later.
|
|
|
6
6
|
See <https://www.gnu.org/licenses/> for details.
|
|
7
7
|
*/
|
|
8
8
|
import { Buffer } from "node:buffer";
|
|
9
|
-
import {
|
|
10
|
-
import { midiToHz, noteNameToMidi } from "./sidwave.js";
|
|
9
|
+
import { assemblyToPrgDetailed } from "./tools/translation/assembler.js";
|
|
10
|
+
import { SidwaveValidationError, isRestToken, isTieToken, midiToHz, midiToNoteName, noteNameToMidi, parseFilterMod, parseLayerRef, parseMotif, } from "./sidwave.js";
|
|
11
|
+
/** Standalone PRG start address (`10 SYS2061`). */
|
|
12
|
+
export const SIDWAVE_PRG_ENTRY = 0x080d;
|
|
13
|
+
/** PSID init routine (A = song number, ignored). */
|
|
14
|
+
export const SIDWAVE_INIT_ADDRESS = 0x0810;
|
|
15
|
+
/** PSID play routine, called once per video frame. */
|
|
16
|
+
export const SIDWAVE_PLAY_ADDRESS = 0x0813;
|
|
17
|
+
export const SIDWAVE_LOAD_ADDRESS = 0x0801;
|
|
18
|
+
/** The player and its data must end below the I/O area at $D000. */
|
|
19
|
+
export const SIDWAVE_MEMORY_END = 0xd000;
|
|
20
|
+
/** Frames of gate-off with zeroed ADSR before every new note (hard restart). */
|
|
21
|
+
export const SIDWAVE_HARD_RESTART_FRAMES = 2;
|
|
22
|
+
/** Shortest note: one frame with the gate on plus the hard restart before the next note. */
|
|
23
|
+
export const SIDWAVE_MIN_NOTE_FRAMES = SIDWAVE_HARD_RESTART_FRAMES + 1;
|
|
24
|
+
/** BASIC's memory ends where the BASIC ROM starts. */
|
|
25
|
+
export const SIDWAVE_BASIC_END = 0xa000;
|
|
11
26
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
27
|
+
* Compiles SIDWAVE to a C64 program with a frame-based player.
|
|
28
|
+
*
|
|
29
|
+
* The compiler arranges the timeline into per-voice note events with frame-exact onsets
|
|
30
|
+
* (no tempo drift), then encodes them as compact command streams. The 6502 player reads the
|
|
31
|
+
* streams once per frame and runs the per-note modulation itself: hard restart, fast arpeggio,
|
|
32
|
+
* pitch slide, vibrato, pulse-width sweep, and a global filter with sweep and per-pattern ramp.
|
|
15
33
|
*/
|
|
16
34
|
export function compileSidwaveToPrg(doc) {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
}
|
|
35
|
+
const arrangement = arrangeSidwave(doc);
|
|
36
|
+
const streams = arrangement.voices.map((v) => encodeVoiceStream(v.events, arrangement.totalFrames));
|
|
37
|
+
// Check the size before assembling the streams: the player and its tables are assembled once with
|
|
38
|
+
// empty streams, which gives the exact start of the stream data. Songs larger than 64 KB would
|
|
39
|
+
// otherwise wrap the address counter inside the assembler.
|
|
40
|
+
const layout = assemblyToPrgDetailed(buildPlayerAsm(arrangement, [[OP.LOOP], [OP.LOOP], [OP.LOOP]]), {
|
|
41
|
+
fileName: "sidwave_player.asm",
|
|
42
|
+
loadAddress: SIDWAVE_LOAD_ADDRESS,
|
|
43
|
+
});
|
|
44
|
+
const streamStart = layout.symbols.get("stream0");
|
|
45
|
+
const projectedEnd = streamStart + streams.reduce((sum, st) => sum + st.length, 0);
|
|
46
|
+
// The PRG installer also writes three bytes after the program (an empty BASIC program).
|
|
47
|
+
if (projectedEnd + 3 > SIDWAVE_MEMORY_END) {
|
|
48
|
+
const kb = ((projectedEnd - SIDWAVE_LOAD_ADDRESS) / 1024).toFixed(1);
|
|
49
|
+
throw new SidwaveValidationError("$.timeline", `the compiled song needs ${kb} KB and would end at $${projectedEnd.toString(16).toUpperCase()}, past the $D000 limit (about 50 KB); shorten the timeline or use longer note lengths`);
|
|
140
50
|
}
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const prg =
|
|
144
|
-
|
|
51
|
+
const asm = buildPlayerAsm(arrangement, streams);
|
|
52
|
+
const assembled = assemblyToPrgDetailed(asm, { fileName: "sidwave_player.asm", loadAddress: SIDWAVE_LOAD_ADDRESS });
|
|
53
|
+
const prg = assembled.prg;
|
|
54
|
+
const endAddress = SIDWAVE_LOAD_ADDRESS + prg.length - 2;
|
|
55
|
+
for (const [label, expected] of [["install", SIDWAVE_PRG_ENTRY], ["init_vec", SIDWAVE_INIT_ADDRESS], ["play_vec", SIDWAVE_PLAY_ADDRESS], ["prg_end", endAddress]]) {
|
|
56
|
+
if (assembled.symbols.get(label) !== expected)
|
|
57
|
+
throw new Error(`SIDWAVE player layout error: ${label} is not at $${expected.toString(16)}`);
|
|
58
|
+
}
|
|
59
|
+
// After the PRG's SYS returns, BASIC's program and variables start right behind the player.
|
|
60
|
+
const basicFreeBytes = Math.max(0, SIDWAVE_BASIC_END - (endAddress + 3));
|
|
61
|
+
const warnings = [];
|
|
62
|
+
if (basicFreeBytes < 1024) {
|
|
63
|
+
warnings.push(basicFreeBytes === 0
|
|
64
|
+
? `The PRG ends at $${endAddress.toString(16).toUpperCase()}, past BASIC's memory ($A000): after it returns to BASIC, any BASIC command that needs memory fails with ?OUT OF MEMORY. The music keeps playing.`
|
|
65
|
+
: `The PRG leaves only ${basicFreeBytes} bytes of BASIC memory after it returns to BASIC.`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
prg,
|
|
69
|
+
entryAddress: SIDWAVE_PRG_ENTRY,
|
|
70
|
+
initAddress: SIDWAVE_INIT_ADDRESS,
|
|
71
|
+
playAddress: SIDWAVE_PLAY_ADDRESS,
|
|
72
|
+
loadAddress: SIDWAVE_LOAD_ADDRESS,
|
|
73
|
+
endAddress,
|
|
74
|
+
totalFrames: arrangement.totalFrames,
|
|
75
|
+
framesPerSecond: arrangement.fps,
|
|
76
|
+
instrumentCount: arrangement.instruments.length - 1,
|
|
77
|
+
streamBytes: streams.map((s) => s.length),
|
|
78
|
+
basicFreeBytes,
|
|
79
|
+
warnings,
|
|
80
|
+
};
|
|
145
81
|
}
|
|
146
82
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
83
|
+
* Wraps the compiled PRG in a PSID v2 file. Init and play are the player's fixed entry points;
|
|
84
|
+
* the clock flag follows the document's mode so the host calls play at 50 Hz (PAL) or 60 Hz (NTSC).
|
|
85
|
+
* `entryAddress` is accepted for compatibility and ignored.
|
|
149
86
|
*/
|
|
150
87
|
export function compileSidwaveToSid(doc, prg, options) {
|
|
151
|
-
const headerSize =
|
|
88
|
+
const headerSize = 0x7c;
|
|
152
89
|
const header = Buffer.alloc(headerSize, 0);
|
|
153
90
|
header.write("PSID", 0, "ascii");
|
|
154
91
|
header.writeUInt16BE(2, 4); // version
|
|
155
92
|
header.writeUInt16BE(headerSize, 6); // data offset
|
|
156
|
-
header.writeUInt16BE(0, 8); // load address
|
|
157
|
-
|
|
158
|
-
header.writeUInt16BE(
|
|
159
|
-
header.writeUInt16BE(entry, 12); // play (single entry; routine self-dispatches)
|
|
93
|
+
header.writeUInt16BE(0, 8); // load address taken from the first two data bytes
|
|
94
|
+
header.writeUInt16BE(options?.initAddress ?? SIDWAVE_INIT_ADDRESS, 10);
|
|
95
|
+
header.writeUInt16BE(options?.playAddress ?? SIDWAVE_PLAY_ADDRESS, 12);
|
|
160
96
|
header.writeUInt16BE(1, 14); // songs
|
|
161
97
|
header.writeUInt16BE(1, 16); // start song
|
|
162
|
-
header.writeUInt32BE(0, 18); // speed
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
header
|
|
166
|
-
|
|
167
|
-
header.
|
|
168
|
-
// data is PRG body; if it already has a 2-byte load address, keep it so loaders can use it
|
|
98
|
+
header.writeUInt32BE(0, 18); // speed: vertical blank timing for song 1
|
|
99
|
+
writeLatin1(header, doc.song.title ?? "Untitled", 0x16);
|
|
100
|
+
writeLatin1(header, doc.song.author ?? "SIDWAVE", 0x36);
|
|
101
|
+
writeLatin1(header, "c64bridge SIDWAVE", 0x56);
|
|
102
|
+
const clockBits = doc.song.mode === "NTSC" ? 0b10 : 0b01;
|
|
103
|
+
header.writeUInt16BE(clockBits << 2, 0x76); // flags: built-in player, C64 compatible, clock, SID model unspecified
|
|
169
104
|
const body = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
|
|
170
105
|
return { sid: Buffer.concat([header, body]) };
|
|
171
106
|
}
|
|
107
|
+
function writeLatin1(buf, text, offset) {
|
|
108
|
+
const safe = [...text].map((ch) => (ch.codePointAt(0) <= 0xff ? ch : "?")).join("").slice(0, 32);
|
|
109
|
+
buf.write(safe, offset, 32, "latin1");
|
|
110
|
+
}
|
|
172
111
|
export function hzToSidFrequency(hz, system) {
|
|
173
112
|
const phi2 = system === "PAL" ? 985248 : 1022727;
|
|
174
113
|
const value = Math.round((hz * 16777216) / phi2);
|
|
175
114
|
return Math.max(0, Math.min(0xffff, value));
|
|
176
115
|
}
|
|
116
|
+
const CLOCK_HZ = { PAL: 985248, NTSC: 1022727 };
|
|
117
|
+
/** CPU cycles per video frame: 312 lines x 63 cycles (PAL), 263 lines x 65 cycles (NTSC). */
|
|
118
|
+
const CYCLES_PER_FRAME = { PAL: 312 * 63, NTSC: 263 * 65 };
|
|
119
|
+
/** Video frame rate of a real C64: 50.1245 Hz on PAL, 59.8262 Hz on NTSC. The player runs once per frame. */
|
|
120
|
+
export function sidwaveFrameRate(system) {
|
|
121
|
+
return CLOCK_HZ[system] / CYCLES_PER_FRAME[system];
|
|
122
|
+
}
|
|
123
|
+
/** Highest playable MIDI note for a system: B7 exceeds the 16-bit frequency register at the PAL clock. */
|
|
124
|
+
export function maxSidwaveMidi(system) {
|
|
125
|
+
let midi = 107;
|
|
126
|
+
while (Math.round((midiToHz(midi) * 16777216) / CLOCK_HZ[system]) > 0xffff)
|
|
127
|
+
midi -= 1;
|
|
128
|
+
return midi;
|
|
129
|
+
}
|
|
177
130
|
function waveformToCtrl(w) {
|
|
178
131
|
const wf = (w || "pulse").toLowerCase();
|
|
179
132
|
if (wf.startsWith("tri"))
|
|
180
|
-
return
|
|
133
|
+
return 0x10;
|
|
181
134
|
if (wf.startsWith("saw"))
|
|
182
|
-
return
|
|
183
|
-
if (wf.startsWith("pulse"))
|
|
184
|
-
return 1 << 6;
|
|
135
|
+
return 0x20;
|
|
185
136
|
if (wf.startsWith("noise"))
|
|
186
|
-
return
|
|
187
|
-
return
|
|
137
|
+
return 0x80;
|
|
138
|
+
return 0x40;
|
|
188
139
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
140
|
+
const BEAT_EPS = 1e-9;
|
|
141
|
+
export function arrangeSidwave(doc) {
|
|
142
|
+
const system = doc.song.mode ?? "PAL";
|
|
143
|
+
const fps = sidwaveFrameRate(system);
|
|
144
|
+
const framesPerBeat = (fps * 60) / doc.song.tempo;
|
|
145
|
+
const frameAt = (beat) => Math.round(beat * framesPerBeat + BEAT_EPS);
|
|
146
|
+
const maxMidi = maxSidwaveMidi(system);
|
|
147
|
+
const totalBars = doc.song.length_bars;
|
|
148
|
+
const totalFrames = frameAt(totalBars * 4);
|
|
149
|
+
// Instrument 0 is a neutral placeholder: voices that have not played a note yet use it, so they are not routed to the filter.
|
|
150
|
+
const instruments = [{ ad: 0, sr: 0, wave: 0, pw: 0, pwStep: 0, pwMin: 0, pwMax: 0, vibUp: [0, 0], vibDown: [0, 0], vibRate: 0, vibDelay: 0, arp: [], arpSpeed: 0, slideSemis: 0, slideSpeed: 0, slideRecip: 0, filter: 0 }];
|
|
151
|
+
const instKeys = new Map();
|
|
152
|
+
const instIndex = (inst) => {
|
|
153
|
+
const key = JSON.stringify(inst);
|
|
154
|
+
let idx = instKeys.get(key);
|
|
155
|
+
if (idx === undefined) {
|
|
156
|
+
idx = instruments.length;
|
|
157
|
+
if (idx >= 64)
|
|
158
|
+
throw new SidwaveValidationError("$.voices", "more than 63 distinct voice/pattern/effect combinations; reuse patterns or effects");
|
|
159
|
+
instruments.push(inst);
|
|
160
|
+
instKeys.set(key, idx);
|
|
161
|
+
}
|
|
162
|
+
return idx;
|
|
163
|
+
};
|
|
164
|
+
const gfx = doc.song.global_fx;
|
|
165
|
+
const fenv = gfx.filter_env;
|
|
166
|
+
const filterVoices = new Set(fenv ? (fenv.voices ?? [1, 2, 3]).map(Number) : []);
|
|
167
|
+
let filterUsed = !!fenv;
|
|
168
|
+
const voicesById = new Map(doc.voices.map((v) => [v.id, v]));
|
|
169
|
+
const notes = [[], [], []];
|
|
170
|
+
const globals = [];
|
|
171
|
+
const fadeWindows = [[], [], []];
|
|
172
|
+
const reverb = gfx.reverb_sim;
|
|
173
|
+
// Expand the timeline, cycling it when length_bars exceeds it and truncating when shorter.
|
|
174
|
+
const sections = [];
|
|
175
|
+
for (let bar = 0, i = 0; bar < totalBars; i += 1) {
|
|
176
|
+
const s = doc.timeline[i % doc.timeline.length];
|
|
177
|
+
const bars = Math.min(s.bars, totalBars - bar);
|
|
178
|
+
sections.push({ index: i % doc.timeline.length, startBeat: bar * 4, endBeat: (bar + bars) * 4 });
|
|
179
|
+
bar += bars;
|
|
199
180
|
}
|
|
200
|
-
|
|
181
|
+
for (const sec of sections) {
|
|
182
|
+
const section = doc.timeline[sec.index];
|
|
183
|
+
const secPath = `$.timeline[${sec.index}]`;
|
|
184
|
+
const sf = frameAt(sec.startBeat);
|
|
185
|
+
const ef = frameAt(sec.endBeat);
|
|
186
|
+
const active = [];
|
|
187
|
+
const fading = [];
|
|
188
|
+
for (const id of [1, 2, 3]) {
|
|
189
|
+
const ref = section.layers[`v${id}`];
|
|
190
|
+
if (ref === undefined)
|
|
191
|
+
continue;
|
|
192
|
+
const voice = voicesById.get(id);
|
|
193
|
+
const parsed = parseLayerRef(ref, Object.keys(voice?.patterns ?? {}));
|
|
194
|
+
if (parsed.pattern === null || !voice)
|
|
195
|
+
continue;
|
|
196
|
+
const pattern = voice.patterns[parsed.pattern];
|
|
197
|
+
const patPath = `${secPath}.layers.v${id} (${parsed.pattern})`;
|
|
198
|
+
active.push(id);
|
|
199
|
+
if (parsed.fade) {
|
|
200
|
+
fading.push(id);
|
|
201
|
+
fadeWindows[id - 1].push({ start: sf, end: ef });
|
|
202
|
+
}
|
|
203
|
+
const fxRaw = (pattern.fx ?? {});
|
|
204
|
+
const filterMod = fxRaw.filter_mod ? parseFilterMod(fxRaw.filter_mod, `${patPath}.fx.filter_mod`) : undefined;
|
|
205
|
+
if (filterMod)
|
|
206
|
+
filterUsed = true;
|
|
207
|
+
const routed = filterVoices.has(id) || !!filterMod ? 1 : 0;
|
|
208
|
+
const entries = patternEntries(pattern, patPath);
|
|
209
|
+
const lengthBeats = pattern.length ?? 1;
|
|
210
|
+
const transpositions = pattern.transpositions ?? [0];
|
|
211
|
+
const cycleBeats = entries.length * lengthBeats;
|
|
212
|
+
const out = notes[id - 1];
|
|
213
|
+
for (let c = 0;; c += 1) {
|
|
214
|
+
const cycleStart = sec.startBeat + c * cycleBeats;
|
|
215
|
+
if (cycleStart >= sec.endBeat - BEAT_EPS)
|
|
216
|
+
break;
|
|
217
|
+
const tr = transpositions[c % transpositions.length] + parsed.transpose;
|
|
218
|
+
if (filterMod) {
|
|
219
|
+
const unitFrames = { steps: lengthBeats * framesPerBeat, beats: framesPerBeat, bars: 4 * framesPerBeat, frames: 1 }[filterMod.unit];
|
|
220
|
+
const count = Math.max(1, Math.min(0xffff, Math.round(filterMod.amount * unitFrames)));
|
|
221
|
+
// Offset and step are in cutoff x 32 units and signed 24-bit in the player, so the full ±2047 range ramps smoothly.
|
|
222
|
+
const step = Math.round((filterMod.sweep * 32) / count);
|
|
223
|
+
globals.push({ frame: frameAt(cycleStart), kind: "filtermod", step, count });
|
|
224
|
+
}
|
|
225
|
+
for (let k = 0; k < entries.length; k += 1) {
|
|
226
|
+
const b = cycleStart + k * lengthBeats;
|
|
227
|
+
if (b >= sec.endBeat - BEAT_EPS)
|
|
228
|
+
break;
|
|
229
|
+
const truncated = b + lengthBeats > sec.endBeat + BEAT_EPS;
|
|
230
|
+
const f0 = frameAt(b);
|
|
231
|
+
const f1 = frameAt(Math.min(b + lengthBeats, sec.endBeat));
|
|
232
|
+
const entry = entries[k];
|
|
233
|
+
if (entry.kind === "rest")
|
|
234
|
+
continue;
|
|
235
|
+
if (entry.kind === "tie") {
|
|
236
|
+
// A tie holds the note sounding right before it, also across a section boundary; after a rest it is a rest.
|
|
237
|
+
const last = out[out.length - 1];
|
|
238
|
+
if (last && last.end === f0)
|
|
239
|
+
last.end = f1;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (f1 - f0 < SIDWAVE_MIN_NOTE_FRAMES) {
|
|
243
|
+
if (truncated)
|
|
244
|
+
continue;
|
|
245
|
+
throw new SidwaveValidationError(patPath, `each entry lasts ${(lengthBeats * framesPerBeat).toFixed(2)} frames at ${doc.song.tempo} BPM; the player needs at least ${SIDWAVE_MIN_NOTE_FRAMES} frames per note (one sounding frame plus the ${SIDWAVE_HARD_RESTART_FRAMES}-frame hard restart); raise 'length' or lower the tempo`);
|
|
246
|
+
}
|
|
247
|
+
const base = entry.notes[0] + tr;
|
|
248
|
+
const offsets = entry.notes.map((n) => n - entry.notes[0]);
|
|
249
|
+
for (const off of offsets) {
|
|
250
|
+
const midi = base + off;
|
|
251
|
+
if (midi < 12 || midi > maxMidi) {
|
|
252
|
+
throw new SidwaveValidationError(patPath, `note ${midiToNoteName(midi)} (after transposition ${tr >= 0 ? "+" : ""}${tr}) is outside the playable range C0..${midiToNoteName(maxMidi)} on ${system}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const slideDepth = fxRaw.slide ? Number(fxRaw.slide.depth) : 0;
|
|
256
|
+
if (slideDepth !== 0 && (base - slideDepth < 12 || base - slideDepth > maxMidi)) {
|
|
257
|
+
throw new SidwaveValidationError(`${patPath}.fx.slide.depth`, `the slide into ${midiToNoteName(base)} would start at ${midiToNoteName(base - slideDepth)}, outside the playable range C0..${midiToNoteName(maxMidi)} on ${system}`);
|
|
258
|
+
}
|
|
259
|
+
if (fxRaw.vibrato) {
|
|
260
|
+
// The upward swing must fit the 16-bit frequency register, or it would clip and the vibrato become lopsided.
|
|
261
|
+
const depth = Number(fxRaw.vibrato.depth);
|
|
262
|
+
const top = base + Math.max(...offsets);
|
|
263
|
+
const peak = (midiToHz(top) * Math.pow(2, depth / 12) * 16777216) / CLOCK_HZ[system];
|
|
264
|
+
if (Math.round(peak) > 0xffff) {
|
|
265
|
+
const maxDepth = 12 * Math.log2((0xffff * CLOCK_HZ[system]) / 16777216 / midiToHz(top));
|
|
266
|
+
throw new SidwaveValidationError(`${patPath}.fx.vibrato.depth`, `a vibrato of ${depth} semitones on ${midiToNoteName(top)} would rise above the SID's highest frequency on ${system}; use a depth of at most ${Math.floor(maxDepth * 100) / 100} or a lower note`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const inst = buildInstrument(voice, fxRaw, entry.arp ? offsets : [], entry.arp ? entry.arpSpeed : 0, routed, fps);
|
|
270
|
+
out.push({ start: f0, end: f1, midi: base, inst: instIndex(inst) });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (filterMod)
|
|
274
|
+
globals.push({ frame: ef, kind: "filtermod", step: 0, count: 0 });
|
|
275
|
+
}
|
|
276
|
+
// Master volume fade when every sounding voice of the section fades.
|
|
277
|
+
if (fading.length > 0 && fading.length === active.length) {
|
|
278
|
+
for (let beat = sec.startBeat; beat < sec.endBeat - BEAT_EPS; beat += 1) {
|
|
279
|
+
const f = frameAt(beat);
|
|
280
|
+
globals.push({ frame: f, kind: "volume", value: Math.round(15 * (1 - (f - sf) / (ef - sf))) });
|
|
281
|
+
}
|
|
282
|
+
globals.push({ frame: ef, kind: "volume", value: 15 });
|
|
283
|
+
}
|
|
284
|
+
// Reverb simulation: a free voice echoes the lowest active voice, delayed and quieter.
|
|
285
|
+
if (reverb && active.length > 0 && active.length < 3) {
|
|
286
|
+
const src = Math.min(...active);
|
|
287
|
+
const tgt = [1, 2, 3].find((id) => !active.includes(id));
|
|
288
|
+
const delay = reverb.delay_frames;
|
|
289
|
+
for (const n of notes[src - 1]) {
|
|
290
|
+
if (n.start < sf || n.start >= ef)
|
|
291
|
+
continue;
|
|
292
|
+
const start = n.start + delay;
|
|
293
|
+
const end = Math.min(n.end + delay, ef);
|
|
294
|
+
if (end - start < SIDWAVE_MIN_NOTE_FRAMES)
|
|
295
|
+
continue;
|
|
296
|
+
const srcInst = instruments[n.inst];
|
|
297
|
+
const sustain = Math.round((srcInst.sr >> 4) * reverb.depth);
|
|
298
|
+
const echo = { ...srcInst, sr: (sustain << 4) | (srcInst.sr & 0x0f) };
|
|
299
|
+
notes[tgt - 1].push({ start, end, midi: n.midi, inst: instIndex(echo) });
|
|
300
|
+
}
|
|
301
|
+
if (fading.includes(src))
|
|
302
|
+
fadeWindows[tgt - 1].push({ start: sf, end: ef });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (let v = 0; v < 3; v += 1) {
|
|
306
|
+
notes[v].sort((a, b) => a.start - b.start);
|
|
307
|
+
for (const n of notes[v]) {
|
|
308
|
+
const w = fadeWindows[v].find((fw) => n.start >= fw.start && n.start < fw.end);
|
|
309
|
+
if (w)
|
|
310
|
+
n.fade = w;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const voices = notes.map((list, v) => ({
|
|
314
|
+
notes: list,
|
|
315
|
+
events: buildVoiceEvents(list, instruments, totalFrames, framesPerBeat, v === 0 ? globals : []),
|
|
316
|
+
}));
|
|
317
|
+
const base = fenv ? Math.round(Number(fenv.cutoff_base)) : 1024;
|
|
318
|
+
const sweep = fenv?.sweep !== undefined ? Math.round(Number(fenv.sweep)) : 0;
|
|
319
|
+
const sweepFrames = fenv?.speed !== undefined ? Number(fenv.speed) * framesPerBeat : 0;
|
|
320
|
+
const stepFixed = sweep !== 0 && sweepFrames > 0 ? Math.max(1, Math.round((Math.abs(sweep) * 32) / sweepFrames)) : 0;
|
|
321
|
+
const type = String(fenv?.type ?? "lowpass").toLowerCase();
|
|
322
|
+
const mode = filterUsed ? (type === "bandpass" ? 0x20 : type === "highpass" ? 0x40 : 0x10) : 0;
|
|
323
|
+
const resonance = fenv?.resonance !== undefined ? Number(fenv.resonance) : 8;
|
|
324
|
+
return {
|
|
325
|
+
fps,
|
|
326
|
+
framesPerBeat,
|
|
327
|
+
totalFrames,
|
|
328
|
+
system,
|
|
329
|
+
instruments,
|
|
330
|
+
voices,
|
|
331
|
+
filter: { base, sweep, stepFixed, resonance, mode, used: filterUsed },
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function patternEntries(pattern, path) {
|
|
335
|
+
const p = pattern;
|
|
336
|
+
if (p.type === "arpeggio" || Array.isArray(p.notes)) {
|
|
337
|
+
const midis = p.notes.map((n) => noteNameToMidi(n));
|
|
338
|
+
if (p.frame_rate !== undefined)
|
|
339
|
+
return [{ kind: "note", notes: midis, arp: true, arpSpeed: Number(p.frame_rate) }];
|
|
340
|
+
return midis.map((m) => ({ kind: "note", notes: [m], arp: false, arpSpeed: 0 }));
|
|
341
|
+
}
|
|
342
|
+
if (Array.isArray(p.groove)) {
|
|
343
|
+
return p.groove.map((raw) => {
|
|
344
|
+
const tok = String(raw).trim();
|
|
345
|
+
if (isRestToken(tok))
|
|
346
|
+
return { kind: "rest" };
|
|
347
|
+
if (isTieToken(tok))
|
|
348
|
+
return { kind: "tie" };
|
|
349
|
+
return { kind: "note", notes: [noteNameToMidi(tok)], arp: false, arpSpeed: 0 };
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
const entries = parseMotif(String(p.motif), p.root === undefined ? "C3" : String(p.root), `${path}.motif`);
|
|
353
|
+
return entries.map((e) => (e === null ? { kind: "rest" } : e === "~" ? { kind: "tie" } : { kind: "note", notes: [e], arp: false, arpSpeed: 0 }));
|
|
201
354
|
}
|
|
202
|
-
function
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
355
|
+
function buildInstrument(voice, fx, arp, arpSpeed, filter, fps) {
|
|
356
|
+
const adsr = voice.adsr ?? [2, 2, 10, 3];
|
|
357
|
+
const wave = waveformToCtrl(voice.waveform ?? "pulse");
|
|
358
|
+
const pw = voice.pulse_width ?? 0x800;
|
|
359
|
+
let pwStep = 0;
|
|
360
|
+
let pwMin = pw;
|
|
361
|
+
let pwMax = pw;
|
|
362
|
+
if (fx.pwm_sweep && wave === 0x40) {
|
|
363
|
+
const rawDepth = Number(fx.pwm_sweep.depth);
|
|
364
|
+
const depth = rawDepth <= 1 ? rawDepth * 4095 : rawDepth;
|
|
365
|
+
pwMin = Math.max(16, Math.round(pw - depth));
|
|
366
|
+
pwMax = Math.min(4079, Math.round(pw + depth));
|
|
367
|
+
pwStep = Math.max(1, Math.round((2 * (pwMax - pwMin) * Number(fx.pwm_sweep.speed)) / fps));
|
|
368
|
+
}
|
|
369
|
+
const vib = fx.vibrato;
|
|
370
|
+
const slide = fx.slide;
|
|
371
|
+
const depth = vib ? Number(vib.depth) : 0;
|
|
372
|
+
const slideSpeed = slide ? Number(slide.speed) : 0;
|
|
373
|
+
const slideRecip = slideSpeed > 1 ? Math.round(65536 / slideSpeed) : 0;
|
|
206
374
|
return {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
375
|
+
ad: ((adsr[0] & 0xf) << 4) | (adsr[1] & 0xf),
|
|
376
|
+
sr: ((adsr[2] & 0xf) << 4) | (adsr[3] & 0xf),
|
|
377
|
+
wave,
|
|
378
|
+
pw,
|
|
379
|
+
pwStep,
|
|
380
|
+
pwMin,
|
|
381
|
+
pwMax,
|
|
382
|
+
// The player's triangle peaks at 126/128, so the factors are raised by 128/126 to reach ±depth exactly.
|
|
383
|
+
vibUp: vib ? mantissaShift((128 / 126) * (Math.pow(2, depth / 12) - 1)) : [0, 0],
|
|
384
|
+
vibDown: vib ? mantissaShift((128 / 126) * (1 - Math.pow(2, -depth / 12))) : [0, 0],
|
|
385
|
+
vibRate: vib ? Math.max(1, Math.min(255, Math.round((256 * Number(vib.rate)) / fps))) : 0,
|
|
386
|
+
vibDelay: vib ? Number(vib.delay ?? 0) : 0,
|
|
387
|
+
arp,
|
|
388
|
+
arpSpeed,
|
|
389
|
+
slideSemis: slide ? Number(slide.depth) : 0,
|
|
390
|
+
slideSpeed,
|
|
391
|
+
slideRecip,
|
|
392
|
+
filter,
|
|
210
393
|
};
|
|
211
394
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
395
|
+
/** Represents 0 < k < 1 as mant / 2^(8 + shift) with the largest shift that keeps mant <= 255 (at least 7 bits of precision). */
|
|
396
|
+
function mantissaShift(k) {
|
|
397
|
+
let shift = 0;
|
|
398
|
+
while (shift < 15 && Math.round(k * 2 ** (9 + shift)) <= 255)
|
|
399
|
+
shift += 1;
|
|
400
|
+
return [Math.max(1, Math.round(k * 2 ** (8 + shift))), shift];
|
|
401
|
+
}
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// Command stream
|
|
404
|
+
export const OP = {
|
|
405
|
+
NOTE: 0x00, // + note index 0..95 (C0..B7)
|
|
406
|
+
INST: 0x60, // + instrument 0..63
|
|
407
|
+
WAIT: 0xa0, // + frames-1, 1..64 frames
|
|
408
|
+
GATEOFF: 0xe0,
|
|
409
|
+
HARDRESTART: 0xe1,
|
|
410
|
+
SETSR: 0xe2, // nn
|
|
411
|
+
SETVOL: 0xe3, // nn
|
|
412
|
+
WAITLONG: 0xe4, // nn frames, 0 = 256
|
|
413
|
+
FILTERMOD: 0xe5, // step (signed 24-bit, cutoff x 32), count lo, count hi
|
|
414
|
+
LOOP: 0xe6,
|
|
415
|
+
};
|
|
416
|
+
const ORDER = { gate: 0, filterReset: 1, inst: 2, note: 3, sustain: 4, filterStart: 5, volume: 6 };
|
|
417
|
+
function buildVoiceEvents(notes, instruments, total, framesPerBeat, globals) {
|
|
418
|
+
const events = [];
|
|
419
|
+
const wrap = (f) => (f >= total ? f - total : f);
|
|
420
|
+
let currentInst = -1;
|
|
421
|
+
for (let i = 0; i < notes.length; i += 1) {
|
|
422
|
+
const n = notes[i];
|
|
423
|
+
const next = i + 1 < notes.length ? notes[i + 1].start : notes[0].start + total;
|
|
424
|
+
const hr = Math.max(n.start + 1, next - SIDWAVE_HARD_RESTART_FRAMES);
|
|
425
|
+
const gateOff = Math.min(n.end, hr);
|
|
426
|
+
if (gateOff < hr)
|
|
427
|
+
events.push({ frame: wrap(gateOff), order: ORDER.gate, bytes: [OP.GATEOFF] });
|
|
428
|
+
events.push({ frame: wrap(hr), order: ORDER.gate, bytes: [OP.HARDRESTART] });
|
|
429
|
+
if (n.inst !== currentInst) {
|
|
430
|
+
events.push({ frame: n.start, order: ORDER.inst, bytes: [OP.INST + n.inst] });
|
|
431
|
+
currentInst = n.inst;
|
|
432
|
+
}
|
|
433
|
+
events.push({ frame: n.start, order: ORDER.note, bytes: [OP.NOTE + (n.midi - 12)] });
|
|
434
|
+
if (n.fade) {
|
|
435
|
+
const inst = instruments[n.inst];
|
|
436
|
+
const sustain = inst.sr >> 4;
|
|
437
|
+
const level = (f) => (Math.round(sustain * Math.max(0, 1 - (f - n.fade.start) / (n.fade.end - n.fade.start))) << 4) | (inst.sr & 0x0f);
|
|
438
|
+
events.push({ frame: n.start, order: ORDER.sustain, bytes: [OP.SETSR, level(n.start)] });
|
|
439
|
+
const firstBeat = Math.ceil((n.start + 1) / framesPerBeat - BEAT_EPS);
|
|
440
|
+
for (let beat = firstBeat;; beat += 1) {
|
|
441
|
+
const f = Math.round(beat * framesPerBeat + BEAT_EPS);
|
|
442
|
+
if (f >= gateOff)
|
|
443
|
+
break;
|
|
444
|
+
if (f > n.start)
|
|
445
|
+
events.push({ frame: f, order: ORDER.sustain, bytes: [OP.SETSR, level(f)] });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
254
448
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
449
|
+
for (const g of globals) {
|
|
450
|
+
if (g.kind === "volume") {
|
|
451
|
+
events.push({ frame: wrap(g.frame), order: ORDER.volume, bytes: [OP.SETVOL, g.value] });
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
const step = g.step & 0xffffff;
|
|
455
|
+
events.push({
|
|
456
|
+
frame: wrap(g.frame),
|
|
457
|
+
order: g.count === 0 ? ORDER.filterReset : ORDER.filterStart,
|
|
458
|
+
bytes: [OP.FILTERMOD, step & 0xff, (step >> 8) & 0xff, step >> 16, g.count & 0xff, g.count >> 8],
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return events;
|
|
463
|
+
}
|
|
464
|
+
export function encodeVoiceStream(events, total) {
|
|
465
|
+
const sorted = [...events].sort((a, b) => a.frame - b.frame || a.order - b.order);
|
|
466
|
+
const out = [];
|
|
467
|
+
const pushWait = (frames) => {
|
|
468
|
+
let left = frames;
|
|
469
|
+
while (left > 256) {
|
|
470
|
+
out.push(OP.WAITLONG, 0);
|
|
471
|
+
left -= 256;
|
|
472
|
+
}
|
|
473
|
+
if (left <= 0)
|
|
474
|
+
return;
|
|
475
|
+
if (left <= 64)
|
|
476
|
+
out.push(OP.WAIT + left - 1);
|
|
477
|
+
else
|
|
478
|
+
out.push(OP.WAITLONG, left & 0xff);
|
|
479
|
+
};
|
|
480
|
+
let cursor = 0;
|
|
481
|
+
for (const e of sorted) {
|
|
482
|
+
if (e.frame > cursor) {
|
|
483
|
+
pushWait(e.frame - cursor);
|
|
484
|
+
cursor = e.frame;
|
|
286
485
|
}
|
|
287
|
-
|
|
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);
|
|
486
|
+
out.push(...e.bytes);
|
|
294
487
|
}
|
|
488
|
+
pushWait(total - cursor);
|
|
489
|
+
out.push(OP.LOOP);
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
// 6502 player
|
|
494
|
+
function buildPlayerAsm(arr, streams) {
|
|
495
|
+
const L = [];
|
|
496
|
+
const insts = arr.instruments;
|
|
497
|
+
const clockIndex = arr.system === "NTSC" ? 1 : 0;
|
|
498
|
+
// The PRG's CIA timer reproduces the song's video frame rate on whichever clock the machine has.
|
|
499
|
+
const ciaLatch = (clock) => Math.round(clock / arr.fps) - 1;
|
|
500
|
+
const f = arr.filter;
|
|
501
|
+
const fBase = Math.max(0, Math.min(2047, f.base)) * 32;
|
|
502
|
+
const fEnd = Math.max(0, Math.min(2047, f.base + f.sweep)) * 32;
|
|
503
|
+
const fMin = Math.min(fBase, fEnd);
|
|
504
|
+
const fMax = Math.max(fBase, fEnd);
|
|
505
|
+
const fDir = f.sweep < 0 ? 1 : 0;
|
|
506
|
+
L.push(`ptr = $FB`);
|
|
507
|
+
L.push(`* = $0801`);
|
|
508
|
+
L.push(` .byte $0B,$08,$0A,$00,$9E,$32,$30,$36,$31,$00,$00,$00 ; 10 SYS2061`);
|
|
509
|
+
L.push(`install:`);
|
|
510
|
+
L.push(` jmp do_install`);
|
|
511
|
+
L.push(`init_vec:`);
|
|
512
|
+
L.push(` jmp init`);
|
|
513
|
+
L.push(`play_vec:`);
|
|
514
|
+
L.push(` jmp play`);
|
|
515
|
+
L.push(PLAYER_CODE);
|
|
516
|
+
// Constants
|
|
517
|
+
L.push(`doc_clock: .byte ${clockIndex}`);
|
|
518
|
+
L.push(`cia_lo: .byte <${ciaLatch(CLOCK_HZ.PAL)}, <${ciaLatch(CLOCK_HZ.NTSC)}`);
|
|
519
|
+
L.push(`cia_hi: .byte >${ciaLatch(CLOCK_HZ.PAL)}, >${ciaLatch(CLOCK_HZ.NTSC)}`);
|
|
520
|
+
L.push(`fenv_init: .byte <${fBase}, >${fBase}, ${fDir}`);
|
|
521
|
+
L.push(`fenv_step: .byte <${f.stepFixed}, >${f.stepFixed}`);
|
|
522
|
+
L.push(`fenv_min: .byte <${fMin}, >${fMin}`);
|
|
523
|
+
L.push(`fenv_max: .byte <${fMax}, >${fMax}`);
|
|
524
|
+
L.push(`res_route: .byte ${(f.resonance & 0x0f) << 4}`);
|
|
525
|
+
L.push(`fmode: .byte ${f.mode}`);
|
|
526
|
+
L.push(`voff: .byte 0, 7, 14`);
|
|
527
|
+
L.push(`vbit: .byte 1, 2, 4`);
|
|
528
|
+
L.push(`start_lo: .byte <stream0, <stream1, <stream2`);
|
|
529
|
+
L.push(`start_hi: .byte >stream0, >stream1, >stream2`);
|
|
530
|
+
L.push(`cmd_tab:`);
|
|
531
|
+
for (const name of ["c_gateoff", "c_hr", "c_sr", "c_vol", "c_waitl", "c_fmod", "c_loop"])
|
|
532
|
+
L.push(` .byte <(${name}-1), >(${name}-1)`);
|
|
533
|
+
// Instrument tables
|
|
534
|
+
const arpTable = [];
|
|
535
|
+
const arpStart = insts.map((inst) => {
|
|
536
|
+
if (inst.arp.length === 0)
|
|
537
|
+
return 0;
|
|
538
|
+
for (let s = 0; s + inst.arp.length <= arpTable.length; s += 1) {
|
|
539
|
+
if (inst.arp.every((v, i) => arpTable[s + i] === v))
|
|
540
|
+
return s;
|
|
541
|
+
}
|
|
542
|
+
const s = arpTable.length;
|
|
543
|
+
arpTable.push(...inst.arp);
|
|
544
|
+
return s;
|
|
545
|
+
});
|
|
546
|
+
if (arpTable.length > 256)
|
|
547
|
+
throw new SidwaveValidationError("$.voices", "arpeggio tables exceed 256 entries");
|
|
548
|
+
const col = (name, values) => pushByteRows(L, name, values.map((v) => `$${hex2(v)}`));
|
|
549
|
+
col("i_ad", insts.map((i) => i.ad));
|
|
550
|
+
col("i_sr", insts.map((i) => i.sr));
|
|
551
|
+
col("i_wave", insts.map((i) => i.wave));
|
|
552
|
+
col("i_pwlo", insts.map((i) => i.pw & 0xff));
|
|
553
|
+
col("i_pwhi", insts.map((i) => i.pw >> 8));
|
|
554
|
+
col("i_pwslo", insts.map((i) => i.pwStep & 0xff));
|
|
555
|
+
col("i_pwshi", insts.map((i) => i.pwStep >> 8));
|
|
556
|
+
col("i_pwminlo", insts.map((i) => i.pwMin & 0xff));
|
|
557
|
+
col("i_pwminhi", insts.map((i) => i.pwMin >> 8));
|
|
558
|
+
col("i_pwmaxlo", insts.map((i) => i.pwMax & 0xff));
|
|
559
|
+
col("i_pwmaxhi", insts.map((i) => i.pwMax >> 8));
|
|
560
|
+
col("i_vupm", insts.map((i) => i.vibUp[0]));
|
|
561
|
+
col("i_vupsh", insts.map((i) => i.vibUp[1]));
|
|
562
|
+
col("i_vdnm", insts.map((i) => i.vibDown[0]));
|
|
563
|
+
col("i_vdnsh", insts.map((i) => i.vibDown[1]));
|
|
564
|
+
col("i_vibrate", insts.map((i) => i.vibRate));
|
|
565
|
+
col("i_vibdel", insts.map((i) => i.vibDelay));
|
|
566
|
+
col("i_arpst", arpStart);
|
|
567
|
+
col("i_arplen", insts.map((i) => i.arp.length));
|
|
568
|
+
col("i_arpspd", insts.map((i) => Math.max(1, i.arpSpeed)));
|
|
569
|
+
col("i_slsemi", insts.map((i) => i.slideSemis & 0xff));
|
|
570
|
+
col("i_slspd", insts.map((i) => i.slideSpeed));
|
|
571
|
+
col("i_slreclo", insts.map((i) => i.slideRecip & 0xff));
|
|
572
|
+
col("i_slrechi", insts.map((i) => i.slideRecip >> 8));
|
|
573
|
+
col("i_filt", insts.map((i) => i.filter));
|
|
574
|
+
col("arptab", arpTable.length ? arpTable.map((v) => v & 0xff) : [0]);
|
|
575
|
+
// Quarter-square table for the 8x8 multiply: sq(x) = floor(x * x / 4), x = 0..511.
|
|
576
|
+
const squares = Array.from({ length: 512 }, (_, x) => Math.floor((x * x) / 4));
|
|
577
|
+
col("sq_lo", squares.map((v) => v & 0xff));
|
|
578
|
+
col("sq_hi", squares.map((v) => (v >> 8) & 0xff));
|
|
579
|
+
// Frequency tables C0..B7 for both clocks; init copies the right one into ftlo/fthi.
|
|
580
|
+
for (const sys of ["PAL", "NTSC"]) {
|
|
581
|
+
const values = Array.from({ length: 96 }, (_, n) => hzToSidFrequency(midiToHz(n + 12), sys));
|
|
582
|
+
col(`${sys.toLowerCase()}_lo`, values.map((v) => v & 0xff));
|
|
583
|
+
col(`${sys.toLowerCase()}_hi`, values.map((v) => v >> 8));
|
|
584
|
+
}
|
|
585
|
+
// RAM
|
|
586
|
+
L.push(`ftlo: .res 96`);
|
|
587
|
+
L.push(`fthi: .res 96`);
|
|
588
|
+
L.push(`vars:`);
|
|
589
|
+
for (const name of VOICE_VARS)
|
|
590
|
+
L.push(`v_${name}: .res 3`);
|
|
591
|
+
for (const name of GLOBAL_VARS)
|
|
592
|
+
L.push(`${name}: .res 1`);
|
|
593
|
+
L.push(`vars_end:`);
|
|
594
|
+
streams.forEach((s, i) => pushByteRows(L, `stream${i}`, s.map((b) => `$${hex2(b)}`)));
|
|
595
|
+
L.push(`prg_end:`);
|
|
295
596
|
return L.join("\n");
|
|
296
597
|
}
|
|
598
|
+
const VOICE_VARS = [
|
|
599
|
+
"ptrlo", "ptrhi", "wait", "note", "inst", "ctrl", "ad", "sr", "pwlo", "pwhi", "pwdir", "arppos", "arpcnt",
|
|
600
|
+
"vibph", "vibdel", "vuplo", "vuphi", "vdnlo", "vdnhi", "sofflo", "soffmd", "soffhi", "sstlo", "sstmd", "ssthi", "sdir", "scnt",
|
|
601
|
+
];
|
|
602
|
+
const GLOBAL_VARS = [
|
|
603
|
+
"g_vol", "g_cutlo", "g_cuthi", "g_edir", "g_mofflo", "g_moffmd", "g_moffhi", "g_mstlo", "g_mstmd", "g_msthi", "g_mcntlo", "g_mcnthi",
|
|
604
|
+
"f_lo", "f_hi", "p0", "p1", "p2", "m_lo", "m_hi", "m_fac", "n_lo", "n_hi", "q0", "q1", "q2", "q3",
|
|
605
|
+
"ma", "mb", "r_lo", "r_hi", "ysave",
|
|
606
|
+
"tmp", "t_lo", "t_hi", "t_xx",
|
|
607
|
+
];
|
|
608
|
+
// The player. X holds the voice index (0..2) inside the voice routine; Y is scratch.
|
|
609
|
+
const PLAYER_CODE = String.raw `
|
|
610
|
+
do_install:
|
|
611
|
+
sei
|
|
612
|
+
det1:
|
|
613
|
+
lda $D012
|
|
614
|
+
det2:
|
|
615
|
+
cmp $D012
|
|
616
|
+
beq det2
|
|
617
|
+
bmi det1
|
|
618
|
+
ldx #0
|
|
619
|
+
cmp #$20
|
|
620
|
+
bcs det_pal
|
|
621
|
+
ldx #1
|
|
622
|
+
det_pal:
|
|
623
|
+
lda cia_lo,x
|
|
624
|
+
sta $DC04
|
|
625
|
+
lda cia_hi,x
|
|
626
|
+
sta $DC05
|
|
627
|
+
lda $DC0E
|
|
628
|
+
and #$80
|
|
629
|
+
ora #$11
|
|
630
|
+
sta $DC0E
|
|
631
|
+
txa
|
|
632
|
+
jsr init_core
|
|
633
|
+
; Move BASIC's program and variables behind the player (like NEW with a raised start of BASIC),
|
|
634
|
+
; so BASIC stays usable after the SYS returns and new program lines cannot overwrite the player.
|
|
635
|
+
lda #0
|
|
636
|
+
sta prg_end
|
|
637
|
+
sta prg_end+1
|
|
638
|
+
sta prg_end+2
|
|
639
|
+
lda #<(prg_end+1)
|
|
640
|
+
sta $2B
|
|
641
|
+
lda #>(prg_end+1)
|
|
642
|
+
sta $2C
|
|
643
|
+
lda #<(prg_end+3)
|
|
644
|
+
sta $2D
|
|
645
|
+
sta $2F
|
|
646
|
+
sta $31
|
|
647
|
+
lda #>(prg_end+3)
|
|
648
|
+
sta $2E
|
|
649
|
+
sta $30
|
|
650
|
+
sta $32
|
|
651
|
+
lda #<irq
|
|
652
|
+
sta $0314
|
|
653
|
+
lda #>irq
|
|
654
|
+
sta $0315
|
|
655
|
+
cli
|
|
656
|
+
rts
|
|
657
|
+
|
|
658
|
+
irq:
|
|
659
|
+
jsr play
|
|
660
|
+
jmp $EA31
|
|
661
|
+
|
|
662
|
+
init:
|
|
663
|
+
lda doc_clock
|
|
664
|
+
init_core:
|
|
665
|
+
tax
|
|
666
|
+
lda $01
|
|
667
|
+
pha
|
|
668
|
+
lda #$36
|
|
669
|
+
sta $01
|
|
670
|
+
ldy #95
|
|
671
|
+
cpx #0
|
|
672
|
+
bne ini_ntsc
|
|
673
|
+
ini_pal:
|
|
674
|
+
lda pal_lo,y
|
|
675
|
+
sta ftlo,y
|
|
676
|
+
lda pal_hi,y
|
|
677
|
+
sta fthi,y
|
|
678
|
+
dey
|
|
679
|
+
bpl ini_pal
|
|
680
|
+
jmp ini_state
|
|
681
|
+
ini_ntsc:
|
|
682
|
+
lda ntsc_lo,y
|
|
683
|
+
sta ftlo,y
|
|
684
|
+
lda ntsc_hi,y
|
|
685
|
+
sta fthi,y
|
|
686
|
+
dey
|
|
687
|
+
bpl ini_ntsc
|
|
688
|
+
ini_state:
|
|
689
|
+
lda #0
|
|
690
|
+
ldx #$18
|
|
691
|
+
ini_sid:
|
|
692
|
+
sta $D400,x
|
|
693
|
+
dex
|
|
694
|
+
bpl ini_sid
|
|
695
|
+
ldx #vars_end-vars-1
|
|
696
|
+
ini_vars:
|
|
697
|
+
sta vars,x
|
|
698
|
+
dex
|
|
699
|
+
bpl ini_vars
|
|
700
|
+
ldx #2
|
|
701
|
+
ini_voice:
|
|
702
|
+
lda #1
|
|
703
|
+
sta v_wait,x
|
|
704
|
+
lda start_lo,x
|
|
705
|
+
sta v_ptrlo,x
|
|
706
|
+
lda start_hi,x
|
|
707
|
+
sta v_ptrhi,x
|
|
708
|
+
dex
|
|
709
|
+
bpl ini_voice
|
|
710
|
+
lda #15
|
|
711
|
+
sta g_vol
|
|
712
|
+
jsr env_reset
|
|
713
|
+
pla
|
|
714
|
+
sta $01
|
|
715
|
+
rts
|
|
716
|
+
|
|
717
|
+
play:
|
|
718
|
+
lda $01
|
|
719
|
+
pha
|
|
720
|
+
lda #$36
|
|
721
|
+
sta $01
|
|
722
|
+
lda ptr
|
|
723
|
+
pha
|
|
724
|
+
lda ptr+1
|
|
725
|
+
pha
|
|
726
|
+
ldx #0
|
|
727
|
+
jsr voice
|
|
728
|
+
ldx #1
|
|
729
|
+
jsr voice
|
|
730
|
+
ldx #2
|
|
731
|
+
jsr voice
|
|
732
|
+
jsr globals
|
|
733
|
+
pla
|
|
734
|
+
sta ptr+1
|
|
735
|
+
pla
|
|
736
|
+
sta ptr
|
|
737
|
+
pla
|
|
738
|
+
sta $01
|
|
739
|
+
rts
|
|
740
|
+
|
|
741
|
+
fetch:
|
|
742
|
+
ldy #0
|
|
743
|
+
lda (ptr),y
|
|
744
|
+
inc ptr
|
|
745
|
+
bne fetch_x
|
|
746
|
+
inc ptr+1
|
|
747
|
+
fetch_x:
|
|
748
|
+
rts
|
|
749
|
+
|
|
750
|
+
voice:
|
|
751
|
+
dec v_wait,x
|
|
752
|
+
beq v_seq
|
|
753
|
+
jmp v_regs
|
|
754
|
+
v_seq:
|
|
755
|
+
lda v_ptrlo,x
|
|
756
|
+
sta ptr
|
|
757
|
+
lda v_ptrhi,x
|
|
758
|
+
sta ptr+1
|
|
759
|
+
v_read:
|
|
760
|
+
jsr fetch
|
|
761
|
+
cmp #$60
|
|
762
|
+
bcs v_not_note
|
|
763
|
+
jmp note_on
|
|
764
|
+
v_not_note:
|
|
765
|
+
cmp #$A0
|
|
766
|
+
bcs v_not_inst
|
|
767
|
+
sbc #$5F ; carry is clear here, so this subtracts $60
|
|
768
|
+
sta v_inst,x
|
|
769
|
+
jmp v_read
|
|
770
|
+
v_not_inst:
|
|
771
|
+
cmp #$E0
|
|
772
|
+
bcs v_cmd
|
|
773
|
+
sbc #$9E ; carry is clear here, so this subtracts $9F (WAIT $A0 = 1 frame)
|
|
774
|
+
sta v_wait,x
|
|
775
|
+
jmp v_save
|
|
776
|
+
v_cmd:
|
|
777
|
+
sec
|
|
778
|
+
sbc #$E0
|
|
779
|
+
asl
|
|
780
|
+
tay
|
|
781
|
+
lda cmd_tab+1,y
|
|
782
|
+
pha
|
|
783
|
+
lda cmd_tab,y
|
|
784
|
+
pha
|
|
785
|
+
rts
|
|
786
|
+
|
|
787
|
+
c_gateoff:
|
|
788
|
+
lda v_ctrl,x
|
|
789
|
+
and #$FE
|
|
790
|
+
sta v_ctrl,x
|
|
791
|
+
jmp v_read
|
|
792
|
+
c_hr:
|
|
793
|
+
lda v_ctrl,x
|
|
794
|
+
and #$FE
|
|
795
|
+
sta v_ctrl,x
|
|
796
|
+
lda #0
|
|
797
|
+
sta v_ad,x
|
|
798
|
+
sta v_sr,x
|
|
799
|
+
jmp v_read
|
|
800
|
+
c_sr:
|
|
801
|
+
jsr fetch
|
|
802
|
+
sta v_sr,x
|
|
803
|
+
jmp v_read
|
|
804
|
+
c_vol:
|
|
805
|
+
jsr fetch
|
|
806
|
+
sta g_vol
|
|
807
|
+
jmp v_read
|
|
808
|
+
c_waitl:
|
|
809
|
+
jsr fetch
|
|
810
|
+
sta v_wait,x
|
|
811
|
+
jmp v_save
|
|
812
|
+
c_fmod:
|
|
813
|
+
jsr fetch
|
|
814
|
+
sta g_mstlo
|
|
815
|
+
jsr fetch
|
|
816
|
+
sta g_mstmd
|
|
817
|
+
jsr fetch
|
|
818
|
+
sta g_msthi
|
|
819
|
+
jsr fetch
|
|
820
|
+
sta g_mcntlo
|
|
821
|
+
jsr fetch
|
|
822
|
+
sta g_mcnthi
|
|
823
|
+
lda #0
|
|
824
|
+
sta g_mofflo
|
|
825
|
+
sta g_moffmd
|
|
826
|
+
sta g_moffhi
|
|
827
|
+
jmp v_read
|
|
828
|
+
c_loop:
|
|
829
|
+
lda start_lo,x
|
|
830
|
+
sta ptr
|
|
831
|
+
lda start_hi,x
|
|
832
|
+
sta ptr+1
|
|
833
|
+
cpx #0
|
|
834
|
+
bne c_loop_x
|
|
835
|
+
jsr env_reset
|
|
836
|
+
c_loop_x:
|
|
837
|
+
jmp v_read
|
|
838
|
+
|
|
839
|
+
; the filter sweep restarts with the song so every pass sounds the same
|
|
840
|
+
env_reset:
|
|
841
|
+
lda fenv_init
|
|
842
|
+
sta g_cutlo
|
|
843
|
+
lda fenv_init+1
|
|
844
|
+
sta g_cuthi
|
|
845
|
+
lda fenv_init+2
|
|
846
|
+
sta g_edir
|
|
847
|
+
rts
|
|
848
|
+
|
|
849
|
+
v_save:
|
|
850
|
+
lda ptr
|
|
851
|
+
sta v_ptrlo,x
|
|
852
|
+
lda ptr+1
|
|
853
|
+
sta v_ptrhi,x
|
|
854
|
+
jmp v_regs
|
|
855
|
+
|
|
856
|
+
; ---- note on: A = note index ----
|
|
857
|
+
note_on:
|
|
858
|
+
sta v_note,x
|
|
859
|
+
ldy v_inst,x
|
|
860
|
+
lda i_ad,y
|
|
861
|
+
sta v_ad,x
|
|
862
|
+
lda i_sr,y
|
|
863
|
+
sta v_sr,x
|
|
864
|
+
lda i_wave,y
|
|
865
|
+
ora #1
|
|
866
|
+
sta v_ctrl,x
|
|
867
|
+
lda i_pwlo,y
|
|
868
|
+
sta v_pwlo,x
|
|
869
|
+
lda i_pwhi,y
|
|
870
|
+
sta v_pwhi,x
|
|
871
|
+
lda i_arpspd,y
|
|
872
|
+
sta v_arpcnt,x
|
|
873
|
+
lda i_vibdel,y
|
|
874
|
+
sta v_vibdel,x
|
|
875
|
+
lda i_slspd,y
|
|
876
|
+
sta v_scnt,x
|
|
877
|
+
lda #0
|
|
878
|
+
sta v_pwdir,x
|
|
879
|
+
sta v_arppos,x
|
|
880
|
+
sta v_vibph,x
|
|
881
|
+
sta v_vuplo,x
|
|
882
|
+
sta v_vuphi,x
|
|
883
|
+
sta v_vdnlo,x
|
|
884
|
+
sta v_vdnhi,x
|
|
885
|
+
; vibrato amplitudes above and below the note: ft[n] * mant >> (8 + shift)
|
|
886
|
+
lda i_vupm,y
|
|
887
|
+
beq vn_slide
|
|
888
|
+
ldy v_note,x
|
|
889
|
+
lda ftlo,y
|
|
890
|
+
sta m_lo
|
|
891
|
+
lda fthi,y
|
|
892
|
+
sta m_hi
|
|
893
|
+
ldy v_inst,x
|
|
894
|
+
lda i_vupm,y
|
|
895
|
+
sta m_fac
|
|
896
|
+
jsr mul
|
|
897
|
+
lda i_vupsh,y
|
|
898
|
+
jsr shr_p
|
|
899
|
+
lda p1
|
|
900
|
+
sta v_vuplo,x
|
|
901
|
+
lda p2
|
|
902
|
+
sta v_vuphi,x
|
|
903
|
+
lda i_vdnm,y
|
|
904
|
+
sta m_fac
|
|
905
|
+
jsr mul
|
|
906
|
+
lda i_vdnsh,y
|
|
907
|
+
jsr shr_p
|
|
908
|
+
lda p1
|
|
909
|
+
sta v_vdnlo,x
|
|
910
|
+
lda p2
|
|
911
|
+
sta v_vdnhi,x
|
|
912
|
+
vn_slide:
|
|
913
|
+
ldy v_inst,x
|
|
914
|
+
lda i_slsemi,y
|
|
915
|
+
bne vn_sl_on
|
|
916
|
+
jmp v_read
|
|
917
|
+
vn_sl_on:
|
|
918
|
+
; the slide starts at note - semis (the compiler checks that it is in range)
|
|
919
|
+
sta tmp
|
|
920
|
+
lda v_note,x
|
|
921
|
+
sec
|
|
922
|
+
sbc tmp
|
|
923
|
+
tay
|
|
924
|
+
lda tmp
|
|
925
|
+
bmi vn_sl_above
|
|
926
|
+
; start below: magnitude = ft[note] - ft[start]
|
|
927
|
+
lda #0
|
|
928
|
+
sta v_sdir,x
|
|
929
|
+
sty tmp
|
|
930
|
+
ldy v_note,x
|
|
931
|
+
sec
|
|
932
|
+
lda ftlo,y
|
|
933
|
+
ldy tmp
|
|
934
|
+
sbc ftlo,y
|
|
935
|
+
sta m_lo
|
|
936
|
+
ldy v_note,x
|
|
937
|
+
lda fthi,y
|
|
938
|
+
ldy tmp
|
|
939
|
+
sbc fthi,y
|
|
940
|
+
sta m_hi
|
|
941
|
+
jmp vn_sl_step
|
|
942
|
+
vn_sl_above:
|
|
943
|
+
; start above: magnitude = ft[start] - ft[note]
|
|
944
|
+
lda #1
|
|
945
|
+
sta v_sdir,x
|
|
946
|
+
sty tmp
|
|
947
|
+
sec
|
|
948
|
+
lda ftlo,y
|
|
949
|
+
ldy v_note,x
|
|
950
|
+
sbc ftlo,y
|
|
951
|
+
sta m_lo
|
|
952
|
+
ldy tmp
|
|
953
|
+
lda fthi,y
|
|
954
|
+
ldy v_note,x
|
|
955
|
+
sbc fthi,y
|
|
956
|
+
sta m_hi
|
|
957
|
+
vn_sl_step:
|
|
958
|
+
; offset = magnitude (16.8 fixed point); step per frame = magnitude * round(65536 / speed) / 256
|
|
959
|
+
lda #0
|
|
960
|
+
sta v_sofflo,x
|
|
961
|
+
lda m_lo
|
|
962
|
+
sta v_soffmd,x
|
|
963
|
+
lda m_hi
|
|
964
|
+
sta v_soffhi,x
|
|
965
|
+
ldy v_inst,x
|
|
966
|
+
lda i_slreclo,y
|
|
967
|
+
sta n_lo
|
|
968
|
+
lda i_slrechi,y
|
|
969
|
+
sta n_hi
|
|
970
|
+
jsr mul16
|
|
971
|
+
lda q1
|
|
972
|
+
sta v_sstlo,x
|
|
973
|
+
lda q2
|
|
974
|
+
sta v_sstmd,x
|
|
975
|
+
lda q3
|
|
976
|
+
sta v_ssthi,x
|
|
977
|
+
jmp v_read
|
|
978
|
+
|
|
979
|
+
; ---- per-frame register output ----
|
|
980
|
+
v_regs:
|
|
981
|
+
ldy v_inst,x
|
|
982
|
+
lda i_arplen,y
|
|
983
|
+
beq vr_noarp
|
|
984
|
+
lda i_arpst,y
|
|
985
|
+
clc
|
|
986
|
+
adc v_arppos,x
|
|
987
|
+
tay
|
|
988
|
+
lda arptab,y
|
|
989
|
+
clc
|
|
990
|
+
adc v_note,x
|
|
991
|
+
jmp vr_note
|
|
992
|
+
vr_noarp:
|
|
993
|
+
lda v_note,x
|
|
994
|
+
vr_note:
|
|
995
|
+
tay
|
|
996
|
+
lda ftlo,y
|
|
997
|
+
sta f_lo
|
|
998
|
+
lda fthi,y
|
|
999
|
+
sta f_hi
|
|
1000
|
+
; slide: the remaining offset, applied below or above the note
|
|
1001
|
+
lda v_scnt,x
|
|
1002
|
+
bne vr_sl_on
|
|
1003
|
+
jmp vr_vib
|
|
1004
|
+
vr_sl_on:
|
|
1005
|
+
lda v_soffmd,x
|
|
1006
|
+
sta t_lo
|
|
1007
|
+
lda v_soffhi,x
|
|
1008
|
+
sta t_hi
|
|
1009
|
+
vr_sl_apply:
|
|
1010
|
+
lda v_sdir,x
|
|
1011
|
+
bne vr_sl_up
|
|
1012
|
+
sec
|
|
1013
|
+
lda f_lo
|
|
1014
|
+
sbc t_lo
|
|
1015
|
+
sta f_lo
|
|
1016
|
+
lda f_hi
|
|
1017
|
+
sbc t_hi
|
|
1018
|
+
sta f_hi
|
|
1019
|
+
bcs vr_vib
|
|
1020
|
+
lda #0
|
|
1021
|
+
sta f_lo
|
|
1022
|
+
sta f_hi
|
|
1023
|
+
jmp vr_vib
|
|
1024
|
+
vr_sl_up:
|
|
1025
|
+
clc
|
|
1026
|
+
lda f_lo
|
|
1027
|
+
adc t_lo
|
|
1028
|
+
sta f_lo
|
|
1029
|
+
lda f_hi
|
|
1030
|
+
adc t_hi
|
|
1031
|
+
sta f_hi
|
|
1032
|
+
bcc vr_vib
|
|
1033
|
+
lda #$FF
|
|
1034
|
+
sta f_lo
|
|
1035
|
+
sta f_hi
|
|
1036
|
+
vr_vib:
|
|
1037
|
+
lda v_vuplo,x
|
|
1038
|
+
ora v_vuphi,x
|
|
1039
|
+
beq vr_novib
|
|
1040
|
+
lda v_vibdel,x
|
|
1041
|
+
beq vr_vib_on
|
|
1042
|
+
vr_novib:
|
|
1043
|
+
jmp vr_write
|
|
1044
|
+
vr_vib_on:
|
|
1045
|
+
lda v_vibph,x
|
|
1046
|
+
and #$7F
|
|
1047
|
+
cmp #$40
|
|
1048
|
+
bcc vr_tri
|
|
1049
|
+
eor #$7F
|
|
1050
|
+
vr_tri:
|
|
1051
|
+
asl
|
|
1052
|
+
sta m_fac
|
|
1053
|
+
lda v_vibph,x
|
|
1054
|
+
bmi vr_vib_dn
|
|
1055
|
+
lda v_vuplo,x
|
|
1056
|
+
sta m_lo
|
|
1057
|
+
lda v_vuphi,x
|
|
1058
|
+
sta m_hi
|
|
1059
|
+
jsr mul
|
|
1060
|
+
asl p0
|
|
1061
|
+
rol p1
|
|
1062
|
+
rol p2
|
|
1063
|
+
; add round(amplitude * tri / 128): the carry holds the rounding bit
|
|
1064
|
+
lda p0
|
|
1065
|
+
cmp #$80
|
|
1066
|
+
lda f_lo
|
|
1067
|
+
adc p1
|
|
1068
|
+
sta f_lo
|
|
1069
|
+
lda f_hi
|
|
1070
|
+
adc p2
|
|
1071
|
+
sta f_hi
|
|
1072
|
+
bcc vr_write
|
|
1073
|
+
lda #$FF
|
|
1074
|
+
sta f_lo
|
|
1075
|
+
sta f_hi
|
|
1076
|
+
jmp vr_write
|
|
1077
|
+
vr_vib_dn:
|
|
1078
|
+
lda v_vdnlo,x
|
|
1079
|
+
sta m_lo
|
|
1080
|
+
lda v_vdnhi,x
|
|
1081
|
+
sta m_hi
|
|
1082
|
+
jsr mul
|
|
1083
|
+
asl p0
|
|
1084
|
+
rol p1
|
|
1085
|
+
rol p2
|
|
1086
|
+
; subtract round(amplitude * tri / 128): a clear carry borrows the rounding bit
|
|
1087
|
+
lda #$7F
|
|
1088
|
+
cmp p0
|
|
1089
|
+
lda f_lo
|
|
1090
|
+
sbc p1
|
|
1091
|
+
sta f_lo
|
|
1092
|
+
lda f_hi
|
|
1093
|
+
sbc p2
|
|
1094
|
+
sta f_hi
|
|
1095
|
+
bcs vr_write
|
|
1096
|
+
lda #0
|
|
1097
|
+
sta f_lo
|
|
1098
|
+
sta f_hi
|
|
1099
|
+
vr_write:
|
|
1100
|
+
ldy voff,x
|
|
1101
|
+
lda f_lo
|
|
1102
|
+
sta $D400,y
|
|
1103
|
+
lda f_hi
|
|
1104
|
+
sta $D401,y
|
|
1105
|
+
lda v_pwlo,x
|
|
1106
|
+
sta $D402,y
|
|
1107
|
+
lda v_pwhi,x
|
|
1108
|
+
sta $D403,y
|
|
1109
|
+
lda v_ad,x
|
|
1110
|
+
sta $D405,y
|
|
1111
|
+
lda v_sr,x
|
|
1112
|
+
sta $D406,y
|
|
1113
|
+
lda v_ctrl,x
|
|
1114
|
+
sta $D404,y
|
|
1115
|
+
|
|
1116
|
+
; ---- advance modulators for the next frame ----
|
|
1117
|
+
ldy v_inst,x
|
|
1118
|
+
lda i_arplen,y
|
|
1119
|
+
beq va_slide
|
|
1120
|
+
dec v_arpcnt,x
|
|
1121
|
+
bne va_slide
|
|
1122
|
+
lda i_arpspd,y
|
|
1123
|
+
sta v_arpcnt,x
|
|
1124
|
+
lda v_arppos,x
|
|
1125
|
+
clc
|
|
1126
|
+
adc #1
|
|
1127
|
+
cmp i_arplen,y
|
|
1128
|
+
bcc va_arp_ok
|
|
1129
|
+
lda #0
|
|
1130
|
+
va_arp_ok:
|
|
1131
|
+
sta v_arppos,x
|
|
1132
|
+
va_slide:
|
|
1133
|
+
lda v_scnt,x
|
|
1134
|
+
beq va_vib
|
|
1135
|
+
dec v_scnt,x
|
|
1136
|
+
sec
|
|
1137
|
+
lda v_sofflo,x
|
|
1138
|
+
sbc v_sstlo,x
|
|
1139
|
+
sta v_sofflo,x
|
|
1140
|
+
lda v_soffmd,x
|
|
1141
|
+
sbc v_sstmd,x
|
|
1142
|
+
sta v_soffmd,x
|
|
1143
|
+
lda v_soffhi,x
|
|
1144
|
+
sbc v_ssthi,x
|
|
1145
|
+
sta v_soffhi,x
|
|
1146
|
+
bcs va_vib
|
|
1147
|
+
lda #0
|
|
1148
|
+
sta v_soffmd,x
|
|
1149
|
+
sta v_soffhi,x
|
|
1150
|
+
va_vib:
|
|
1151
|
+
lda v_vibdel,x
|
|
1152
|
+
beq va_vib_run
|
|
1153
|
+
dec v_vibdel,x
|
|
1154
|
+
jmp va_pw
|
|
1155
|
+
va_vib_run:
|
|
1156
|
+
lda v_vibph,x
|
|
1157
|
+
clc
|
|
1158
|
+
adc i_vibrate,y
|
|
1159
|
+
sta v_vibph,x
|
|
1160
|
+
va_pw:
|
|
1161
|
+
lda i_pwslo,y
|
|
1162
|
+
ora i_pwshi,y
|
|
1163
|
+
beq va_done
|
|
1164
|
+
lda v_pwdir,x
|
|
1165
|
+
bne va_pw_down
|
|
1166
|
+
clc
|
|
1167
|
+
lda v_pwlo,x
|
|
1168
|
+
adc i_pwslo,y
|
|
1169
|
+
sta v_pwlo,x
|
|
1170
|
+
lda v_pwhi,x
|
|
1171
|
+
adc i_pwshi,y
|
|
1172
|
+
sta v_pwhi,x
|
|
1173
|
+
cmp i_pwmaxhi,y
|
|
1174
|
+
bcc va_done
|
|
1175
|
+
bne va_pw_max
|
|
1176
|
+
lda v_pwlo,x
|
|
1177
|
+
cmp i_pwmaxlo,y
|
|
1178
|
+
bcc va_done
|
|
1179
|
+
va_pw_max:
|
|
1180
|
+
lda i_pwmaxlo,y
|
|
1181
|
+
sta v_pwlo,x
|
|
1182
|
+
lda i_pwmaxhi,y
|
|
1183
|
+
sta v_pwhi,x
|
|
1184
|
+
lda #1
|
|
1185
|
+
sta v_pwdir,x
|
|
1186
|
+
rts
|
|
1187
|
+
va_pw_down:
|
|
1188
|
+
sec
|
|
1189
|
+
lda v_pwlo,x
|
|
1190
|
+
sbc i_pwslo,y
|
|
1191
|
+
sta v_pwlo,x
|
|
1192
|
+
lda v_pwhi,x
|
|
1193
|
+
sbc i_pwshi,y
|
|
1194
|
+
sta v_pwhi,x
|
|
1195
|
+
bcc va_pw_min
|
|
1196
|
+
cmp i_pwminhi,y
|
|
1197
|
+
bcc va_pw_min
|
|
1198
|
+
bne va_done
|
|
1199
|
+
lda v_pwlo,x
|
|
1200
|
+
cmp i_pwminlo,y
|
|
1201
|
+
beq va_pw_min
|
|
1202
|
+
bcs va_done
|
|
1203
|
+
va_pw_min:
|
|
1204
|
+
lda i_pwminlo,y
|
|
1205
|
+
sta v_pwlo,x
|
|
1206
|
+
lda i_pwminhi,y
|
|
1207
|
+
sta v_pwhi,x
|
|
1208
|
+
lda #0
|
|
1209
|
+
sta v_pwdir,x
|
|
1210
|
+
va_done:
|
|
1211
|
+
rts
|
|
1212
|
+
|
|
1213
|
+
; ---- p2:p1 = round(p2:p1:p0 / 2^(8 + A)) (A = 0..15) ----
|
|
1214
|
+
shr_p:
|
|
1215
|
+
sta tmp
|
|
1216
|
+
shr_l:
|
|
1217
|
+
lda tmp
|
|
1218
|
+
beq shr_x
|
|
1219
|
+
lsr p2
|
|
1220
|
+
ror p1
|
|
1221
|
+
ror p0
|
|
1222
|
+
dec tmp
|
|
1223
|
+
jmp shr_l
|
|
1224
|
+
shr_x:
|
|
1225
|
+
lda p0
|
|
1226
|
+
cmp #$80
|
|
1227
|
+
lda p1
|
|
1228
|
+
adc #0
|
|
1229
|
+
sta p1
|
|
1230
|
+
lda p2
|
|
1231
|
+
adc #0
|
|
1232
|
+
sta p2
|
|
1233
|
+
rts
|
|
1234
|
+
|
|
1235
|
+
; ---- 8x8 multiply: ma * mb -> r_hi:r_lo, using a*b = sq(a+b) - sq(|a-b|) with sq(x) = floor(x*x/4) ----
|
|
1236
|
+
; Clobbers A and Y; preserves X, ma and mb.
|
|
1237
|
+
mul8:
|
|
1238
|
+
lda ma
|
|
1239
|
+
clc
|
|
1240
|
+
adc mb
|
|
1241
|
+
tay
|
|
1242
|
+
bcc mul8_low
|
|
1243
|
+
lda sq_lo+256,y
|
|
1244
|
+
sta r_lo
|
|
1245
|
+
lda sq_hi+256,y
|
|
1246
|
+
sta r_hi
|
|
1247
|
+
jmp mul8_diff
|
|
1248
|
+
mul8_low:
|
|
1249
|
+
lda sq_lo,y
|
|
1250
|
+
sta r_lo
|
|
1251
|
+
lda sq_hi,y
|
|
1252
|
+
sta r_hi
|
|
1253
|
+
mul8_diff:
|
|
1254
|
+
lda ma
|
|
1255
|
+
sec
|
|
1256
|
+
sbc mb
|
|
1257
|
+
bcs mul8_pos
|
|
1258
|
+
eor #$FF
|
|
1259
|
+
adc #1
|
|
1260
|
+
mul8_pos:
|
|
1261
|
+
tay
|
|
1262
|
+
sec
|
|
1263
|
+
lda r_lo
|
|
1264
|
+
sbc sq_lo,y
|
|
1265
|
+
sta r_lo
|
|
1266
|
+
lda r_hi
|
|
1267
|
+
sbc sq_hi,y
|
|
1268
|
+
sta r_hi
|
|
1269
|
+
rts
|
|
1270
|
+
|
|
1271
|
+
; ---- 16x8 multiply: m_hi:m_lo * m_fac -> p2:p1:p0 (preserves X, Y and m) ----
|
|
1272
|
+
mul:
|
|
1273
|
+
sty ysave
|
|
1274
|
+
lda m_fac
|
|
1275
|
+
sta mb
|
|
1276
|
+
lda m_lo
|
|
1277
|
+
sta ma
|
|
1278
|
+
jsr mul8
|
|
1279
|
+
lda r_lo
|
|
1280
|
+
sta p0
|
|
1281
|
+
lda r_hi
|
|
1282
|
+
sta p1
|
|
1283
|
+
lda m_hi
|
|
1284
|
+
sta ma
|
|
1285
|
+
jsr mul8
|
|
1286
|
+
clc
|
|
1287
|
+
lda p1
|
|
1288
|
+
adc r_lo
|
|
1289
|
+
sta p1
|
|
1290
|
+
lda r_hi
|
|
1291
|
+
adc #0
|
|
1292
|
+
sta p2
|
|
1293
|
+
ldy ysave
|
|
1294
|
+
rts
|
|
1295
|
+
|
|
1296
|
+
; ---- 16x16 multiply: m_hi:m_lo * n_hi:n_lo -> q3:q2:q1:q0 (preserves X, Y, m and n) ----
|
|
1297
|
+
mul16:
|
|
1298
|
+
sty ysave
|
|
1299
|
+
lda m_lo
|
|
1300
|
+
sta ma
|
|
1301
|
+
lda n_lo
|
|
1302
|
+
sta mb
|
|
1303
|
+
jsr mul8
|
|
1304
|
+
lda r_lo
|
|
1305
|
+
sta q0
|
|
1306
|
+
lda r_hi
|
|
1307
|
+
sta q1
|
|
1308
|
+
lda m_hi
|
|
1309
|
+
sta ma
|
|
1310
|
+
jsr mul8
|
|
1311
|
+
clc
|
|
1312
|
+
lda q1
|
|
1313
|
+
adc r_lo
|
|
1314
|
+
sta q1
|
|
1315
|
+
lda r_hi
|
|
1316
|
+
adc #0
|
|
1317
|
+
sta q2
|
|
1318
|
+
lda n_hi
|
|
1319
|
+
sta mb
|
|
1320
|
+
jsr mul8
|
|
1321
|
+
clc
|
|
1322
|
+
lda q2
|
|
1323
|
+
adc r_lo
|
|
1324
|
+
sta q2
|
|
1325
|
+
lda r_hi
|
|
1326
|
+
adc #0
|
|
1327
|
+
sta q3
|
|
1328
|
+
lda m_lo
|
|
1329
|
+
sta ma
|
|
1330
|
+
jsr mul8
|
|
1331
|
+
clc
|
|
1332
|
+
lda q1
|
|
1333
|
+
adc r_lo
|
|
1334
|
+
sta q1
|
|
1335
|
+
lda q2
|
|
1336
|
+
adc r_hi
|
|
1337
|
+
sta q2
|
|
1338
|
+
lda q3
|
|
1339
|
+
adc #0
|
|
1340
|
+
sta q3
|
|
1341
|
+
ldy ysave
|
|
1342
|
+
rts
|
|
1343
|
+
|
|
1344
|
+
; ---- filter, routing and volume ----
|
|
1345
|
+
globals:
|
|
1346
|
+
; cutoff = sweep position (unsigned 16-bit) + filter_mod offset (signed 24-bit), clamped to 0..2047 x 32
|
|
1347
|
+
clc
|
|
1348
|
+
lda g_cutlo
|
|
1349
|
+
adc g_mofflo
|
|
1350
|
+
sta t_lo
|
|
1351
|
+
lda g_cuthi
|
|
1352
|
+
adc g_moffmd
|
|
1353
|
+
sta t_hi
|
|
1354
|
+
lda #0
|
|
1355
|
+
adc g_moffhi
|
|
1356
|
+
sta t_xx
|
|
1357
|
+
bmi gl_low
|
|
1358
|
+
beq gl_out
|
|
1359
|
+
lda #$E0
|
|
1360
|
+
sta t_lo
|
|
1361
|
+
lda #$FF
|
|
1362
|
+
sta t_hi
|
|
1363
|
+
jmp gl_out
|
|
1364
|
+
gl_low:
|
|
1365
|
+
lda #0
|
|
1366
|
+
sta t_lo
|
|
1367
|
+
sta t_hi
|
|
1368
|
+
gl_out:
|
|
1369
|
+
lda t_lo
|
|
1370
|
+
lsr
|
|
1371
|
+
lsr
|
|
1372
|
+
lsr
|
|
1373
|
+
lsr
|
|
1374
|
+
lsr
|
|
1375
|
+
sta $D415
|
|
1376
|
+
lda t_hi
|
|
1377
|
+
sta $D416
|
|
1378
|
+
lda #0
|
|
1379
|
+
sta tmp
|
|
1380
|
+
ldx #2
|
|
1381
|
+
gl_route:
|
|
1382
|
+
ldy v_inst,x
|
|
1383
|
+
lda i_filt,y
|
|
1384
|
+
beq gl_route_n
|
|
1385
|
+
lda vbit,x
|
|
1386
|
+
ora tmp
|
|
1387
|
+
sta tmp
|
|
1388
|
+
gl_route_n:
|
|
1389
|
+
dex
|
|
1390
|
+
bpl gl_route
|
|
1391
|
+
lda tmp
|
|
1392
|
+
ora res_route
|
|
1393
|
+
sta $D417
|
|
1394
|
+
lda g_vol
|
|
1395
|
+
ora fmode
|
|
1396
|
+
sta $D418
|
|
1397
|
+
; filter ramp from filter_mod
|
|
1398
|
+
lda g_mcntlo
|
|
1399
|
+
ora g_mcnthi
|
|
1400
|
+
beq gl_env
|
|
1401
|
+
clc
|
|
1402
|
+
lda g_mofflo
|
|
1403
|
+
adc g_mstlo
|
|
1404
|
+
sta g_mofflo
|
|
1405
|
+
lda g_moffmd
|
|
1406
|
+
adc g_mstmd
|
|
1407
|
+
sta g_moffmd
|
|
1408
|
+
lda g_moffhi
|
|
1409
|
+
adc g_msthi
|
|
1410
|
+
sta g_moffhi
|
|
1411
|
+
lda g_mcntlo
|
|
1412
|
+
bne gl_dec
|
|
1413
|
+
dec g_mcnthi
|
|
1414
|
+
gl_dec:
|
|
1415
|
+
dec g_mcntlo
|
|
1416
|
+
gl_env:
|
|
1417
|
+
lda fenv_step
|
|
1418
|
+
ora fenv_step+1
|
|
1419
|
+
beq gl_done
|
|
1420
|
+
lda g_edir
|
|
1421
|
+
bne gl_env_down
|
|
1422
|
+
clc
|
|
1423
|
+
lda g_cutlo
|
|
1424
|
+
adc fenv_step
|
|
1425
|
+
sta g_cutlo
|
|
1426
|
+
lda g_cuthi
|
|
1427
|
+
adc fenv_step+1
|
|
1428
|
+
sta g_cuthi
|
|
1429
|
+
bcs gl_env_top
|
|
1430
|
+
cmp fenv_max+1
|
|
1431
|
+
bcc gl_done
|
|
1432
|
+
bne gl_env_top
|
|
1433
|
+
lda g_cutlo
|
|
1434
|
+
cmp fenv_max
|
|
1435
|
+
bcc gl_done
|
|
1436
|
+
gl_env_top:
|
|
1437
|
+
lda fenv_max
|
|
1438
|
+
sta g_cutlo
|
|
1439
|
+
lda fenv_max+1
|
|
1440
|
+
sta g_cuthi
|
|
1441
|
+
lda #1
|
|
1442
|
+
sta g_edir
|
|
1443
|
+
rts
|
|
1444
|
+
gl_env_down:
|
|
1445
|
+
sec
|
|
1446
|
+
lda g_cutlo
|
|
1447
|
+
sbc fenv_step
|
|
1448
|
+
sta g_cutlo
|
|
1449
|
+
lda g_cuthi
|
|
1450
|
+
sbc fenv_step+1
|
|
1451
|
+
sta g_cuthi
|
|
1452
|
+
bcc gl_env_bot
|
|
1453
|
+
cmp fenv_min+1
|
|
1454
|
+
bcc gl_env_bot
|
|
1455
|
+
bne gl_done
|
|
1456
|
+
lda g_cutlo
|
|
1457
|
+
cmp fenv_min
|
|
1458
|
+
beq gl_env_bot
|
|
1459
|
+
bcs gl_done
|
|
1460
|
+
gl_env_bot:
|
|
1461
|
+
lda fenv_min
|
|
1462
|
+
sta g_cutlo
|
|
1463
|
+
lda fenv_min+1
|
|
1464
|
+
sta g_cuthi
|
|
1465
|
+
lda #0
|
|
1466
|
+
sta g_edir
|
|
1467
|
+
gl_done:
|
|
1468
|
+
rts
|
|
1469
|
+
`;
|
|
297
1470
|
function pushByteRows(out, label, items, perLine = 16) {
|
|
298
|
-
out.push("");
|
|
299
1471
|
out.push(`${label}:`);
|
|
300
1472
|
for (let i = 0; i < items.length; i += perLine) {
|
|
301
|
-
|
|
302
|
-
out.push(` .byte ${row}`);
|
|
1473
|
+
out.push(` .byte ${items.slice(i, i + perLine).join(",")}`);
|
|
303
1474
|
}
|
|
304
1475
|
}
|
|
305
1476
|
function hex2(n) {
|
|
306
1477
|
return (n & 0xff).toString(16).toUpperCase().padStart(2, "0");
|
|
307
1478
|
}
|
|
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
|
-
}
|