1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
254
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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
//! Parsed SID voice layout: the tables one `.sid` file unpacks
//! into, plus the cycled voice-grid producer consumed by
//! [`super::sid_module::SidModule::to_modules`].
//!
//! - `phrases: Vec<Vec<u8>>` — raw phrase byte streams. The SID
//! format calls these "tracks", but the DAW layer has its own
//! [`crate::core::daw::track::Track`] concept, so we say "phrase" here.
//! - `orderings: Vec<Vec<u8>>` — per-voice orderings (sequences of
//! phrase indices).
//! - `songs: Vec<Vec<usize>>` — per sub-song, the three indices into
//! `orderings` for the three SID voices.
//! - `version: usize` — SID variant (10, 15, 20+) that selects the
//! phrase-byte decoder.
use crate::prelude::*;
use crate::tracker::import::patternslot::PatternSlot;
use alloc::{vec, vec::Vec};
#[derive(Debug)]
pub struct Voices {
pub version: usize,
pub songs: Vec<Vec<usize>>,
pub orderings: Vec<Vec<u8>>,
pub phrases: Vec<Vec<u8>>,
/// Per-tune note-table overflow substitutions `(raw_note, real_note)`,
/// consulted by [`decode_phrase`] before the shared default fixup. Needed
/// because a note byte > 95 reads engine work-RAM whose value is tune- and
/// frame-dependent (see `decode_phrase`); empty for tunes that use the
/// shared default.
pub overflow_overrides: &'static [(u8, u8)],
}
impl Voices {
/// Assemble the parsed SID voice layout from the decoded tables: replayer
/// `version`, per-sub-song order indices (`songs`), per-channel order lists
/// (`orderings`), the shared phrase/pattern pool (`phrases`), and the
/// per-tune overflow-note overrides.
pub fn new(
version: usize,
songs: Vec<Vec<usize>>,
orderings: Vec<Vec<u8>>,
phrases: Vec<Vec<u8>>,
overflow_overrides: &'static [(u8, u8)],
) -> Self {
Self {
version,
songs,
orderings,
phrases,
overflow_overrides,
}
}
/// Cycle each voice through its phrase ordering until every
/// voice has completed at least one full pass, and return the
/// resulting `(rows × 3 voices)` grid of [`PatternSlot`]s.
///
/// Bounded by [`Self::MAX_GRID_ROWS`]. SID v30 (Delta) does not
/// emit a 254 end-of-song marker in every ordering and uses
/// phrase-level compression / per-channel pattern loops that
/// the v10/v20 decoder doesn't model — the natural
/// "all voices wrapped once" terminator can fail to fire and
/// the row generator runs away. The cap matches `MAX_ROWS` in
/// [`crate::tracker::import::build`] for symmetry.
/// Each voice's full single-pass row sequence: the concatenation of every
/// phrase in that voice's ordering (until the `254` end-of-song marker or
/// the ordering ends). Voices are **not** cycled into a common LCM grid —
/// each one is its own length and loops independently via a `ChannelLoop`,
/// exactly as the three SID voices run on the real hardware (no global
/// realignment). This replaced the old `get_voice_grid` LCM hack, which
/// materialised `lcm(voice lengths)` rows (thousands per song) to fit a
/// single pattern-style grid — only needed before the DAW layer had
/// per-lane loops.
/// `short_gate_instrs[i]` true ⇒ instrument `i` is drum-flagged (fxmask
/// bit0) and uses a 1-row gate in the v30 replayer — see [`decode_phrase`].
/// Empty (or all-false) reproduces the pre-fix behaviour exactly.
pub fn voice_passes(
&self,
song_number: usize,
short_gate_instrs: &[bool],
) -> Vec<Vec<PatternSlot>> {
let phrases = self.decode_all_phrases(short_gate_instrs);
self.songs[song_number]
.iter()
.map(|&oi| {
let mut rows: Vec<PatternSlot> = Vec::new();
for &p in &self.orderings[oi] {
if p == 254 {
break; // original SID end-of-song marker
}
rows.extend_from_slice(pick_phrase(&phrases, p));
}
rows
})
.collect()
}
fn decode_all_phrases(&self, short_gate_instrs: &[bool]) -> Vec<Vec<PatternSlot>> {
let mut sink = Vec::new();
self.phrases
.iter()
.map(|p| {
decode_phrase(
self.version,
p,
self.overflow_overrides,
short_gate_instrs,
&mut sink,
)
})
.collect()
}
/// Diagnostic: every raw note byte > 95 (a `RH_FREQ_HEX` overflow) the
/// decoder encounters, as `(phrase_index, raw_note)`. Used by the
/// `sid_overflow_scan` example to list which tunes still need
/// `overflow_overrides` entries (see [`decode_phrase`]).
pub fn raw_overflow_notes(&self) -> Vec<(usize, u8)> {
let mut out = Vec::new();
for (pi, p) in self.phrases.iter().enumerate() {
let mut raw = Vec::new();
// Decode with no overrides so we see the original note bytes.
let _ = decode_phrase(self.version, p, &[], &[], &mut raw);
for n in raw {
out.push((pi, n));
}
}
out
}
}
/// Look up phrase index `idx`. The hardcoded fallbacks (`111 → 3`,
/// `112 → 2`) reproduce a quirk of `commando.sid`: the original
/// player reads memory locations that happen to map to those
/// phrases.
fn pick_phrase(phrases: &[Vec<PatternSlot>], idx: u8) -> &Vec<PatternSlot> {
let i = idx as usize;
if i < phrases.len() {
return &phrases[i];
}
let fallback = match idx {
111 => 3,
112 => 2,
_ => 0,
};
&phrases[fallback.min(phrases.len() - 1)]
}
fn decode_phrase(
version: usize,
source: &[u8],
overflow_overrides: &[(u8, u8)],
short_gate_instrs: &[bool],
raw_overflows: &mut Vec<u8>,
) -> Vec<PatternSlot> {
// Treat OOB reads as the 0xFF sentinel: a truncated phrase
// stream terminates instead of panicking.
let byte = |idx: usize| -> u8 { source.get(idx).copied().unwrap_or(0xFF) };
let mut phrase: Vec<PatternSlot> = vec![];
let mut index: usize = 0;
let mut last_instr: Option<usize> = None;
while byte(index) != 255 {
let mut current = PatternSlot::default();
let length = byte(index) & 0b0001_1111; // 0-31
let release = (byte(index) & 0b0010_0000) == 0;
let append = (byte(index) & 0b0100_0000) == 0;
let instr_or_portamento = (byte(index) & 0b1000_0000) != 0;
if append {
index += 1;
if instr_or_portamento {
match version {
10 => {
if byte(index) & 0b1000_0000 == 0 {
current.instrument = Some((byte(index) & 0b0111_1111) as usize);
last_instr = current.instrument;
} else {
// Portamento: bit 7 = portamento flag, bits 1-6 = speed,
// bit 0 = direction (0=up, 1=down).
// Speed is raw 16-bit freq delta per frame, >> 1 to scale for XM effects.
let p = byte(index) & 0b0111_1110;
if p != 0 {
current.effect_parameter = p >> 1;
if byte(index) & 1 == 0 {
current.effect_type = 1; // portamento up
} else {
current.effect_type = 2; // portamento down
}
}
}
}
15 => {
// Spellbound variant: byte 2 is always instrument, never portamento
if byte(index) & 0b1000_0000 == 0 {
current.instrument = Some((byte(index) & 0b0111_1111) as usize);
last_instr = current.instrument;
}
}
_ => {
// Version 20+: portamento uses 14-bit value across 2 bytes
// Byte 1: bit 7=portamento, bit 6=direction, bits 5-0=high 6 bits
// Byte 2: low 8 bits of portamento speed
if byte(index) & 0b1000_0000 == 0 {
current.instrument = Some((byte(index) & 0b0111_1111) as usize);
last_instr = current.instrument;
} else if index + 2 >= source.len() {
// International Karate v20 has phrase bytes that
// request a 2-byte portamento payload but only one
// byte remains. Roll back two positions so the
// outer loop's `index += 1` (at the phrase tail)
// lands on a sentinel — defensive but matches the
// observed playback in the original engine.
index = index.saturating_sub(2);
} else {
// Ghidra IK `play` $ae0c (note-fetch): the 2-byte
// portamento is a per-frame 16-bit FREQ-REGISTER delta.
// First byte `b2f1` = direction (bit0: 0=up/add,
// 1=down/sub) + LOW-byte step (bits1-6) + the bit7 porta
// flag; second byte `b2f4` = HIGH-byte step. The slide
// adds/subtracts `(b2f4<<8) | (b2f1 & 0x7e)` to $d400/01
// each gated frame. (The earlier guess — direction in
// bit6, value `(b2&0x3f)<<8 | next` — both mis-combined
// the bytes AND took the wrong direction bit, inflating
// the step ~15× and railing IK's lead to a screech.)
let b2f1 = byte(index) as u16;
let b2f4 = byte(index + 1) as u16;
let direction_down = byte(index) & 1 != 0;
let p: u16 = (b2f4 << 8) | (b2f1 & 0b0111_1110);
index += 1;
if p != 0 {
// Per-frame freq-register delta → 8-bit lane param
// (the player recovers `reg_delta = param << 6`; the
// >>6 / <<4 round-trip drops the low 6 bits, ≲2 %).
current.effect_parameter = (p >> 6).min(255) as u8;
current.effect_type = if direction_down { 2 } else { 1 };
}
}
}
}
index += 1;
}
// Note byte: bits 0-6 = note number. The frequency table
// `RH_FREQ_HEX` (192 bytes @ 0x5428 in commando) holds only 96
// notes (0-95); a note byte > 95 makes the original replayer index
// *past* the table into adjacent engine work-RAM, so its "pitch" is
// an overflow artefact, not a real note — the replayer reads
// whatever byte happens to live at `RH_FREQ_HEX + note*2`. We
// reproduce that at import time by substituting the note the
// overflow actually sounds, so the produced track carries the right
// notes.
//
// The default `match` below is the shared, empirically-tuned fixup.
// But the overflow target is engine **work-RAM** (e.g. note 104 in
// commando lands on `voice_ctrl`), so its value is tune- *and*
// frame-dependent: verified via `sidplay --regdump` of the 6502+SID,
// note 104 reads ~0x00 (freq-high 0, a near-DC hard-sync slave) in
// commando but ~0x41 (a mid note) in monty. A single global value
// can't be right for both, so `overflow_overrides` lets a specific
// tune (keyed in `OneSid`) substitute its own correct note first —
// commando overrides 104/105 → 0 (`RH_FREQ_HEX[0]` = 0x0116, the
// closest representable note to its 0x00xx overflow) instead of the
// shared default 65 (F-5), which had made its sync-slave voice 1
// sound an audible mid tone.
let n = byte(index) & 0b0111_1111;
// The freq table `RH_FREQ_HEX` holds 96 notes, indices 0..=95, so the
// first overflow is note 96 (`>= 8 * 12`), NOT 97. The earlier `n > 96`
// guard let raw note 96 through as a real pitch — and Pitch 96 maps to
// ~4.2 kHz, which clamps the SID freq register to 0xFFFF (a screeching
// rail, e.g. Thing on a Spring's voice-1 noise drum). It also left the
// `96 => …` overflow arm below permanently dead. `>=` routes 96 into
// the overflow fixup like every other out-of-table index.
let note = if n >= 8 * 12 {
raw_overflows.push(n); // diagnostic: record the raw overflow note
if let Some(&(_, sub)) = overflow_overrides.iter().find(|&&(k, _)| k == n) {
sub
} else {
match n {
96 => 0,
97 => 0,
98 => 12,
100 => 3,
104 => 65,
105 => 65,
107 => 6,
127 => 0,
_ => n & 0b0011_1111, // clamp to 0-63 for unmapped overflows
}
}
} else {
n
};
current.note = match Pitch::try_from(note) {
Ok(p) => CellNote::Play(p),
Err(_) => CellNote::Empty,
};
} else {
// Appended note (bit 6 set): the replayer re-programs the voice
// with the gate cleared (`ctrl & 0xfe`) at the *same* pitch, i.e.
// it RELEASES the held note (it never reads a new pitch). Emit a
// KeyOff so the voice stops instead of sustaining forever — without
// this a note followed by appended entries is held indefinitely
// (e.g. Monty's voice 1 F-5 + 7×`0x7f`).
current.note = CellNote::KeyOff;
current.instrument = last_instr;
}
// Bit 7 of the note byte is unused in versions 10/15/20.
// Version 30 (Delta) may use it as a reset-effect flag, but
// no disassembly confirms this — left unimplemented.
if release {
if matches!(current.note, CellNote::Empty) {
current.note = CellNote::KeyOff;
} else {
current.volume = 0x50; // set volume 64 (XM encoding)
}
current.instrument = last_instr;
}
// Replicate the replayer's release: a note with the release bit set
// (`0x20` clear) is killed when its length expires — at `lengthleft==0`,
// i.e. on the LAST of its `length` empty rows, the player gates the
// voice off (Rob-Hubbard `ctrl &= 0xfe`, `sustain_release = 0`). Without
// this the note holds at full sustain until the next note retriggers,
// leaving sustained passages too loud.
// v30 drum-flagged instruments (fxmask bit0) use a SHORT, 1-row gate.
// The real replayer (Ghidra Delta `play` FUN_c110 $c124) clears the
// gate as soon as `lenleft` drops below its initial value — i.e. after
// the FIRST row — independent of the note's total `length` and of the
// release bit, then rests for the remaining rows. Modelling only the
// general `lenleft==0` release (KeyOff on the LAST row) held these
// percussive lead accents as sustained notes: Delta voice 1's periodic
// "breathing" rests were filled in, flattening the dynamics. So for a
// short-gate instrument, emit the KeyOff on the FIRST empty row.
let eff_instr = current.instrument.or(last_instr);
let is_short_gate = version == 30
&& eff_instr.is_some_and(|i| short_gate_instrs.get(i).copied().unwrap_or(false));
let note_is_play = matches!(current.note, CellNote::Play(_));
let emit_kill = note_is_play && (release || is_short_gate);
// 0-based index into the trailing empty rows at which the gate releases.
let kill_row = if is_short_gate {
0
} else {
length.saturating_sub(1)
};
// v30 freq-slide ($c338) continuation: the replayer applies the
// per-frame slide EVERY frame the note is gated, not just on the
// trigger frame. We decode it as a portamento (effect_type 1/2) on the
// note cell, but a porta only runs while the effect is present, so the
// trailing empty rows would freeze the pitch (the sweep would last one
// row instead of the whole note — e.g. Delta sub-song 11 voice 1's
// rising sweep stayed dead-flat). Re-emit the same porta on every held
// row so the slide runs for the note's full length, matching the chip.
// v30 only: v30 puts the $c338 slide on effect 1/2; v10/v20 portamento
// is single-shot per the original decoders and must stay bit-identical.
let continue_slide = if version == 30 {
match current.effect_type {
1 | 2 => Some((current.effect_type, current.effect_parameter)),
_ => None,
}
} else {
None
};
index += 1;
phrase.push(current);
if length != 0 {
for k in 0..length {
if emit_kill && k == kill_row {
let mut kill = PatternSlot::default();
kill.note = CellNote::KeyOff;
phrase.push(kill);
} else if let Some((ft, fp)) = continue_slide {
let mut held = PatternSlot::default();
held.effect_type = ft;
held.effect_parameter = fp;
phrase.push(held);
} else {
phrase.push(PatternSlot::default());
}
}
}
}
phrase
}