Files
dungeoncraft/src/xm.rs
T

1174 lines
40 KiB
Rust

// Tracker module replayer for FastTracker 2 XM and ProTracker MOD files.
//
// Both formats are loaded into one internal representation (MOD notes are converted from
// periods to XM note numbers). Supported: instruments with sample maps, volume and panning
// envelopes, fadeout, auto-vibrato, linear and Amiga frequency tables, volume column effects
// and the common effects (arpeggio, portamento, tone portamento, vibrato, tremolo, panning,
// sample offset, volume slides, position jump, pattern break/loop/delay, retrigger, note
// cut/delay, speed/tempo, global volume, key off, panning slide, extra fine portamento).
#[derive(Clone, Default)]
pub struct Sample {
pub data: Vec<f32>,
pub loop_start: usize,
pub loop_len: usize,
pub loop_type: u8, // 0 none, 1 forward, 2 ping-pong
pub volume: u8,
pub finetune: i8,
pub panning: u8,
pub relnote: i8,
}
#[derive(Clone, Default)]
pub struct Envelope {
pub points: Vec<(u16, u16)>,
pub sustain: Option<usize>,
pub lp: Option<(usize, usize)>,
pub on: bool,
}
#[derive(Clone)]
pub struct Instrument {
pub samples: Vec<Sample>,
pub map: [u8; 96],
pub vol_env: Envelope,
pub pan_env: Envelope,
pub fadeout: u16,
pub vib_type: u8,
pub vib_sweep: u8,
pub vib_depth: u8,
pub vib_rate: u8,
}
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub struct Cell {
pub note: u8, // 0 none, 1..96 notes (C-0 = 1), 97 key off
pub inst: u8,
pub vol: u8, // volume column
pub fx: u8,
pub param: u8,
}
pub struct Pattern {
pub rows: usize,
pub cells: Vec<Cell>,
}
pub struct Module {
#[allow(dead_code)] // shown by tools/debugging
pub name: String,
pub channels: usize,
pub order: Vec<u8>,
pub restart: usize,
pub patterns: Vec<Pattern>,
pub instruments: Vec<Instrument>,
pub linear: bool,
pub speed: u32,
pub bpm: u32,
pub pans: Vec<u8>, // initial channel panning
}
// ------------------------------------------------------------------ loading
fn u16le(b: &[u8], o: usize) -> u16 {
b.get(o..o + 2).map_or(0, |s| u16::from_le_bytes([s[0], s[1]]))
}
fn u32le(b: &[u8], o: usize) -> u32 {
b.get(o..o + 4).map_or(0, |s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
}
fn u16be(b: &[u8], o: usize) -> u16 {
b.get(o..o + 2).map_or(0, |s| u16::from_be_bytes([s[0], s[1]]))
}
fn byte(b: &[u8], o: usize) -> u8 {
b.get(o).copied().unwrap_or(0)
}
pub fn load(b: &[u8]) -> Result<Module, String> {
if b.len() > 17 && &b[0..17] == b"Extended Module: " {
load_xm(b)
} else {
load_mod(b)
}
}
pub fn load_xm(b: &[u8]) -> Result<Module, String> {
if b.len() < 80 || &b[0..17] != b"Extended Module: " {
return Err("not an XM file".into());
}
let name = String::from_utf8_lossy(&b[17..37]).trim_end().to_string();
let hdr_size = u32le(b, 60) as usize;
let song_len = u16le(b, 64) as usize;
let restart = u16le(b, 66) as usize;
let channels = (u16le(b, 68) as usize).clamp(1, 32);
let npat = u16le(b, 70) as usize;
let ninst = u16le(b, 72) as usize;
let flags = u16le(b, 74);
let speed = u16le(b, 76).max(1) as u32;
let bpm = u16le(b, 78).max(32) as u32;
let order: Vec<u8> = (0..song_len.min(256)).map(|i| byte(b, 80 + i)).collect();
let mut o = 60 + hdr_size;
let mut patterns = Vec::with_capacity(npat);
for _ in 0..npat {
let plen = u32le(b, o) as usize;
let rows = (u16le(b, o + 5) as usize).clamp(1, 256);
let dsize = u16le(b, o + 7) as usize;
o += plen;
let mut cells = vec![Cell::default(); rows * channels];
if dsize > 0 {
let data = b.get(o..o + dsize).ok_or("truncated pattern")?;
let mut i = 0;
for c in cells.iter_mut() {
if i >= data.len() {
break;
}
let first = data[i];
i += 1;
let mut next = |cond: bool| -> u8 {
if cond && i < data.len() {
i += 1;
data[i - 1]
} else {
0
}
};
if first & 0x80 != 0 {
c.note = next(first & 1 != 0);
c.inst = next(first & 2 != 0);
c.vol = next(first & 4 != 0);
c.fx = next(first & 8 != 0);
c.param = next(first & 16 != 0);
} else {
c.note = first;
c.inst = next(true);
c.vol = next(true);
c.fx = next(true);
c.param = next(true);
}
}
}
o += dsize;
patterns.push(Pattern { rows, cells });
}
let mut instruments = Vec::with_capacity(ninst);
for _ in 0..ninst {
let isize = u32le(b, o) as usize;
let nsmp = u16le(b, o + 27) as usize;
let mut ins = Instrument {
samples: Vec::new(),
map: [0; 96],
vol_env: Envelope::default(),
pan_env: Envelope::default(),
fadeout: 0,
vib_type: 0,
vib_sweep: 0,
vib_depth: 0,
vib_rate: 0,
};
if nsmp == 0 {
o += isize.max(29);
instruments.push(ins);
continue;
}
let shsize = u32le(b, o + 29) as usize;
for k in 0..96 {
ins.map[k] = byte(b, o + 33 + k);
}
let env = |base: usize, n: usize, sus: u8, ls: u8, le: u8, typ: u8| -> Envelope {
let n = n.min(12);
Envelope {
points: (0..n).map(|k| (u16le(b, base + k * 4), u16le(b, base + k * 4 + 2).min(64))).collect(),
sustain: if typ & 2 != 0 { Some(sus as usize) } else { None },
lp: if typ & 4 != 0 { Some((ls as usize, le as usize)) } else { None },
on: typ & 1 != 0 && n > 0,
}
};
ins.vol_env = env(o + 129, byte(b, o + 225) as usize, byte(b, o + 227), byte(b, o + 228), byte(b, o + 229), byte(b, o + 233));
ins.pan_env = env(o + 177, byte(b, o + 226) as usize, byte(b, o + 230), byte(b, o + 231), byte(b, o + 232), byte(b, o + 234));
ins.vib_type = byte(b, o + 235);
ins.vib_sweep = byte(b, o + 236);
ins.vib_depth = byte(b, o + 237);
ins.vib_rate = byte(b, o + 238);
ins.fadeout = u16le(b, o + 239);
o += isize;
// sample headers, then sample data
let mut heads = Vec::new();
for _ in 0..nsmp {
heads.push((
u32le(b, o) as usize,
u32le(b, o + 4) as usize,
u32le(b, o + 8) as usize,
byte(b, o + 12),
byte(b, o + 13) as i8,
byte(b, o + 14),
byte(b, o + 15),
byte(b, o + 16) as i8,
));
o += shsize.max(40);
}
for (len, ls, ll, vol, ft, typ, pan, rel) in heads {
let bits16 = typ & 0x10 != 0;
let raw = b.get(o..(o + len).min(b.len())).unwrap_or(&[]);
o += len;
let mut data = Vec::new();
if bits16 {
let mut acc: i16 = 0;
for k in 0..raw.len() / 2 {
acc = acc.wrapping_add(i16::from_le_bytes([raw[k * 2], raw[k * 2 + 1]]));
data.push(acc as f32 / 32768.0);
}
} else {
let mut acc: i8 = 0;
for &v in raw {
acc = acc.wrapping_add(v as i8);
data.push(acc as f32 / 128.0);
}
}
let div = if bits16 { 2 } else { 1 };
let mut s = Sample {
loop_start: ls / div,
loop_len: ll / div,
loop_type: typ & 3,
volume: vol.min(64),
finetune: ft,
panning: pan,
relnote: rel,
data,
};
if s.loop_type == 3 {
s.loop_type = 1;
}
if s.loop_len == 0 || s.loop_start >= s.data.len() {
s.loop_type = 0;
}
s.loop_len = s.loop_len.min(s.data.len() - s.loop_start.min(s.data.len()));
ins.samples.push(s);
}
instruments.push(ins);
}
Ok(Module {
name,
channels,
order,
restart,
patterns,
instruments,
linear: flags & 1 != 0,
speed,
bpm,
pans: vec![128; channels],
})
}
// ProTracker periods for octaves 1..3 (finetune 0), C-1 = 856
const PT_PERIODS: [u16; 36] = [
856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453, 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226, 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113,
];
pub fn load_mod(b: &[u8]) -> Result<Module, String> {
if b.len() < 1084 {
return Err("not a MOD file".into());
}
let tag = &b[1080..1084];
let channels = match tag {
b"M.K." | b"M!K!" | b"FLT4" | b"4CHN" => 4,
b"6CHN" => 6,
b"8CHN" | b"OCTA" | b"CD81" | b"FLT8" => 8,
t if t[2] == b'C' && t[3] == b'H' && t[0].is_ascii_digit() && t[1].is_ascii_digit() => ((t[0] - b'0') * 10 + (t[1] - b'0')) as usize,
t if &t[1..] == b"CHN" && t[0].is_ascii_digit() => (t[0] - b'0') as usize,
_ => return Err("unknown MOD format".into()),
};
let channels = channels.clamp(1, 32);
let name = String::from_utf8_lossy(&b[0..20]).trim_end_matches('\0').trim_end().to_string();
let song_len = (byte(b, 950) as usize).clamp(1, 128);
let restart = byte(b, 951) as usize;
let all_orders: Vec<u8> = (0..128).map(|i| byte(b, 952 + i)).collect();
let order: Vec<u8> = all_orders[..song_len].to_vec();
let npat = *all_orders.iter().max().unwrap_or(&0) as usize + 1;
let mut o = 1084;
let mut patterns = Vec::with_capacity(npat);
for _ in 0..npat {
let mut cells = vec![Cell::default(); 64 * channels];
for c in cells.iter_mut() {
let (b0, b1, b2, b3) = (byte(b, o), byte(b, o + 1), byte(b, o + 2), byte(b, o + 3));
o += 4;
let period = (((b0 & 0x0F) as u16) << 8) | b1 as u16;
c.inst = (b0 & 0xF0) | (b2 >> 4);
c.fx = b2 & 0x0F;
c.param = b3;
if period > 0 {
let (mut best, mut bd) = (0usize, u16::MAX);
for (k, &p) in PT_PERIODS.iter().enumerate() {
let d = p.abs_diff(period);
if d < bd {
bd = d;
best = k;
}
}
c.note = (best + 37) as u8; // PT C-1 -> XM C-3
}
// MOD "set volume" via effect C stays an effect
if c.fx == 0 && c.param == 0 {
c.fx = 0;
}
}
patterns.push(Pattern { rows: 64, cells });
}
let mut instruments = Vec::with_capacity(31);
for s in 0..31 {
let h = 20 + s * 30;
let len = u16be(b, h + 22) as usize * 2;
let ft = (byte(b, h + 24) & 0x0F) as i8;
let ft = if ft > 7 { ft - 16 } else { ft };
let vol = byte(b, h + 25).min(64);
let ls = u16be(b, h + 26) as usize * 2;
let ll = u16be(b, h + 28) as usize * 2;
let raw = b.get(o..(o + len).min(b.len())).unwrap_or(&[]);
o += len;
let data: Vec<f32> = raw.iter().map(|&v| v as i8 as f32 / 128.0).collect();
let looped = ll > 2 && ls < data.len();
let mut smp = Sample {
loop_start: if looped { ls } else { 0 },
loop_len: if looped { ll.min(data.len() - ls) } else { 0 },
loop_type: if looped { 1 } else { 0 },
volume: vol,
finetune: ft * 16,
panning: 128,
relnote: 0,
data,
};
if smp.loop_len == 0 {
smp.loop_type = 0;
}
instruments.push(Instrument {
samples: vec![smp],
map: [0; 96],
vol_env: Envelope::default(),
pan_env: Envelope::default(),
fadeout: 0,
vib_type: 0,
vib_sweep: 0,
vib_depth: 0,
vib_rate: 0,
});
}
// Amiga LRRL panning, softened
let pans = (0..channels).map(|c| if c % 4 == 0 || c % 4 == 3 { 64 } else { 192 }).collect();
Ok(Module { name, channels, order, restart: if restart < song_len { restart } else { 0 }, patterns, instruments, linear: false, speed: 6, bpm: 125, pans })
}
// ------------------------------------------------------------------ playback
#[derive(Clone, Default)]
struct Chan {
inst: usize, // 1-based, 0 = none
smp: Option<usize>,
note: i32, // real note (0 = C-0) incl. relnote
finetune: i32,
period: f32,
target: f32,
pos: f64,
backwards: bool,
active: bool,
vol: i32,
pan: i32,
key_on: bool,
fade: i32,
venv_pos: u16,
penv_pos: u16,
// effect state
cell: Cell,
arp: u8,
porta_up: u8,
porta_down: u8,
fine_up: u8,
fine_down: u8,
xfine_up: u8,
xfine_down: u8,
tone_speed: u8,
vib_speed: u8,
vib_depth: u8,
vib_pos: u8,
trem_speed: u8,
trem_depth: u8,
trem_pos: u8,
vol_slide: u8,
pan_slide: u8,
offset: u8,
retrig: u8,
loop_row: usize,
loop_count: u8,
delayed: Option<Cell>,
av_pos: u8,
av_sweep: u32,
// per-tick output
vib_off: f32,
trem_off: i32,
step: f64,
out_l: f32,
out_r: f32,
cur_l: f32,
cur_r: f32,
}
pub struct Player {
pub module: std::rc::Rc<Module>,
rate: f32,
speed: u32,
bpm: u32,
tick: u32,
row: usize,
pos: usize,
pdelay: u32,
pdelay_active: bool,
global: i32,
global_slide: u8,
chans: Vec<Chan>,
samples_left: f32,
jump: Option<usize>,
brk: Option<usize>,
loop_jump: Option<usize>,
pub loops: u32, // how many times the song wrapped around
}
fn sine(pos: u8) -> f32 {
(pos as f32 / 64.0 * std::f32::consts::TAU).sin()
}
impl Player {
pub fn new(module: std::rc::Rc<Module>, rate: f32) -> Player {
let n = module.channels;
let mut chans = vec![Chan::default(); n];
for (c, ch) in chans.iter_mut().enumerate() {
ch.pan = module.pans.get(c).copied().unwrap_or(128) as i32;
ch.fade = 65536;
}
let mut p = Player {
speed: module.speed,
bpm: module.bpm,
module,
rate,
tick: 0,
row: 0,
pos: 0,
pdelay: 0,
pdelay_active: false,
global: 64,
global_slide: 0,
chans,
samples_left: 0.0,
jump: None,
brk: None,
loop_jump: None,
loops: 0,
};
p.skip_invalid_orders();
p
}
fn skip_invalid_orders(&mut self) {
let m = &self.module;
let mut guard = 0;
while guard < 300 && (self.pos >= m.order.len() || m.order[self.pos] as usize >= m.patterns.len()) {
if self.pos >= m.order.len() {
self.pos = if m.restart < m.order.len() { m.restart } else { 0 };
self.loops += 1;
} else {
self.pos += 1;
}
guard += 1;
}
}
fn period_of(&self, note: i32, finetune: i32) -> f32 {
if self.module.linear {
(7680 - note * 64 - finetune / 2) as f32
} else {
1712.0 * 2f32.powf((48.0 - note as f32 - finetune as f32 / 128.0) / 12.0)
}
}
fn freq_of(&self, period: f32) -> f32 {
if self.module.linear {
8363.0 * 2f32.powf((4608.0 - period) / 768.0)
} else if period > 0.0 {
8363.0 * 1712.0 / period
} else {
0.0
}
}
fn sample<'a>(m: &'a Module, ch: &Chan) -> Option<&'a Sample> {
let ins = m.instruments.get(ch.inst.checked_sub(1)?)?;
ins.samples.get(ch.smp?)
}
fn env_value(env: &Envelope, pos: u16) -> f32 {
let pts = &env.points;
if pts.is_empty() {
return 64.0;
}
for k in 0..pts.len() - 1 {
let (x0, y0) = pts[k];
let (x1, y1) = pts[k + 1];
if pos >= x0 && pos <= x1 {
if x1 == x0 {
return y1 as f32;
}
let t = (pos - x0) as f32 / (x1 - x0) as f32;
return y0 as f32 + (y1 as f32 - y0 as f32) * t;
}
}
pts.last().unwrap().1 as f32
}
fn env_advance(env: &Envelope, pos: &mut u16, key_on: bool) {
if !env.on || env.points.is_empty() {
return;
}
if let Some(s) = env.sustain {
if key_on && s < env.points.len() && *pos == env.points[s].0 {
return;
}
}
*pos = pos.saturating_add(1);
if let Some((ls, le)) = env.lp {
if le < env.points.len() && ls <= le && *pos >= env.points[le].0 {
*pos = env.points[ls].0;
}
}
let last = env.points.last().unwrap().0;
if *pos > last {
*pos = last;
}
}
// A note starts (or restarts) on a channel
fn trigger(&mut self, c: usize, note: u8, offset: Option<u8>) {
let m = self.module.clone();
let ch = &mut self.chans[c];
let Some(ins) = m.instruments.get(ch.inst.wrapping_sub(1)) else {
ch.active = false;
return;
};
let si = ins.map[(note as usize - 1).min(95)] as usize;
let Some(s) = ins.samples.get(si) else {
ch.active = false;
return;
};
ch.smp = Some(si);
ch.note = note as i32 - 1 + s.relnote as i32;
ch.finetune = s.finetune as i32;
let p = self.period_of(self.chans[c].note, self.chans[c].finetune);
let ch = &mut self.chans[c];
ch.period = p;
ch.target = p;
ch.pos = offset.map_or(0.0, |o| o as f64 * 256.0);
if ch.pos >= s.data.len() as f64 {
ch.active = false;
return;
}
ch.backwards = false;
ch.active = true;
ch.vib_pos = 0;
ch.trem_pos = 0;
}
fn reset_instrument(&mut self, c: usize) {
let m = self.module.clone();
let ch = &mut self.chans[c];
if let Some(s) = Self::sample(&m, ch) {
ch.vol = s.volume as i32;
ch.pan = s.panning as i32;
}
ch.key_on = true;
ch.fade = 65536;
ch.venv_pos = 0;
ch.penv_pos = 0;
ch.av_pos = 0;
ch.av_sweep = 0;
}
fn key_off(&mut self, c: usize) {
let m = self.module.clone();
let ch = &mut self.chans[c];
ch.key_on = false;
let env_on = m.instruments.get(ch.inst.wrapping_sub(1)).map_or(false, |i| i.vol_env.on);
if !env_on {
ch.vol = 0;
}
}
fn apply_cell(&mut self, c: usize, cell: Cell) {
let m = self.module.clone();
let tone_porta = cell.fx == 3 || cell.fx == 5 || cell.vol >= 0xF0;
let offset = if cell.fx == 9 {
if cell.param > 0 {
self.chans[c].offset = cell.param;
}
Some(self.chans[c].offset)
} else {
None
};
if cell.inst > 0 && (cell.inst as usize) <= m.instruments.len() {
self.chans[c].inst = cell.inst as usize;
}
if cell.note == 97 {
self.key_off(c);
} else if (1..=96).contains(&cell.note) {
if tone_porta && self.chans[c].active {
let ins = m.instruments.get(self.chans[c].inst.wrapping_sub(1));
let rel = ins.and_then(|i| i.samples.get(self.chans[c].smp.unwrap_or(0))).map_or(0, |s| s.relnote as i32);
let ft = self.chans[c].finetune;
self.chans[c].target = self.period_of(cell.note as i32 - 1 + rel, ft);
} else {
self.trigger(c, cell.note, offset);
}
}
if cell.inst > 0 && cell.note != 97 {
self.reset_instrument(c);
}
// volume column (tick 0 part)
let v = cell.vol;
let ch = &mut self.chans[c];
match v {
0x10..=0x50 => ch.vol = (v - 0x10) as i32,
0x80..=0x8F => ch.vol = (ch.vol - (v & 15) as i32).max(0),
0x90..=0x9F => ch.vol = (ch.vol + (v & 15) as i32).min(64),
0xA0..=0xAF => ch.vib_speed = v & 15,
0xB0..=0xBF => {
if v & 15 > 0 {
ch.vib_depth = v & 15;
}
}
0xC0..=0xCF => ch.pan = ((v & 15) as i32) * 17,
0xF0..=0xFF => {
if v & 15 > 0 {
ch.tone_speed = (v & 15) << 4;
}
}
_ => {}
}
// effects (tick 0 part)
let p = cell.param;
let (hi, lo) = (p >> 4, p & 15);
match cell.fx {
0 => self.chans[c].arp = p,
1 => {
if p > 0 {
self.chans[c].porta_up = p;
}
}
2 => {
if p > 0 {
self.chans[c].porta_down = p;
}
}
3 => {
if p > 0 {
self.chans[c].tone_speed = p;
}
}
4 => {
if hi > 0 {
self.chans[c].vib_speed = hi;
}
if lo > 0 {
self.chans[c].vib_depth = lo;
}
}
5 | 6 | 0xA => {
if p > 0 {
self.chans[c].vol_slide = p;
}
}
7 => {
if hi > 0 {
self.chans[c].trem_speed = hi;
}
if lo > 0 {
self.chans[c].trem_depth = lo;
}
}
8 => self.chans[c].pan = p as i32,
0xB => {
self.jump = Some(p as usize);
self.brk.get_or_insert(0);
}
0xC => self.chans[c].vol = (p as i32).min(64),
0xD => {
self.brk = Some((hi as usize * 10 + lo as usize).min(255));
if self.jump.is_none() {
self.jump = Some(self.pos + 1);
}
}
0xE => match hi {
1 => {
if lo > 0 {
self.chans[c].fine_up = lo;
}
self.chans[c].period -= self.chans[c].fine_up as f32 * 4.0;
}
2 => {
if lo > 0 {
self.chans[c].fine_down = lo;
}
self.chans[c].period += self.chans[c].fine_down as f32 * 4.0;
}
6 => {
if lo == 0 {
self.chans[c].loop_row = self.row;
} else if self.chans[c].loop_count == 0 {
self.chans[c].loop_count = lo;
self.loop_jump = Some(self.chans[c].loop_row);
} else {
self.chans[c].loop_count -= 1;
if self.chans[c].loop_count > 0 {
self.loop_jump = Some(self.chans[c].loop_row);
}
}
}
9 => self.chans[c].retrig = lo,
0xA => self.chans[c].vol = (self.chans[c].vol + lo as i32).min(64),
0xB => self.chans[c].vol = (self.chans[c].vol - lo as i32).max(0),
0xC => {
if lo == 0 {
self.chans[c].vol = 0;
}
}
0xE => {
if !self.pdelay_active {
self.pdelay = lo as u32;
}
}
_ => {}
},
0xF => {
if p > 0 && p < 32 {
self.speed = p as u32;
} else if p >= 32 {
self.bpm = p as u32;
}
}
16 => self.global = (p as i32).min(64),
17 => {
if p > 0 {
self.global_slide = p;
}
}
20 => {
if p == 0 {
self.key_off(c);
}
}
25 => {
if p > 0 {
self.chans[c].pan_slide = p;
}
}
27 => {
if lo > 0 {
self.chans[c].retrig = lo;
}
}
33 => {
match hi {
1 => {
if lo > 0 {
self.chans[c].xfine_up = lo;
}
self.chans[c].period -= self.chans[c].xfine_up as f32;
}
2 => {
if lo > 0 {
self.chans[c].xfine_down = lo;
}
self.chans[c].period += self.chans[c].xfine_down as f32;
}
_ => {}
}
}
_ => {}
}
}
fn process_row(&mut self) {
let m = self.module.clone();
let pat = &m.patterns[m.order[self.pos] as usize];
let row = self.row.min(pat.rows - 1);
for c in 0..m.channels {
let cell = pat.cells[row * m.channels + c];
self.chans[c].cell = cell;
self.chans[c].vib_off = 0.0;
self.chans[c].trem_off = 0;
if cell.fx == 0xE && cell.param >> 4 == 0xD && cell.param & 15 > 0 {
self.chans[c].delayed = Some(cell);
continue;
}
self.chans[c].delayed = None;
self.apply_cell(c, cell);
}
}
fn effects_tick(&mut self) {
let tick = self.tick;
for c in 0..self.chans.len() {
let cell = self.chans[c].cell;
if let Some(d) = self.chans[c].delayed {
if tick == (d.param & 15) as u32 {
self.chans[c].delayed = None;
self.apply_cell(c, d);
}
continue;
}
let p = cell.param;
let (hi, lo) = (p >> 4, p & 15);
match cell.fx {
17 => {
let s = self.global_slide;
if s >> 4 > 0 {
self.global = (self.global + (s >> 4) as i32).min(64);
} else {
self.global = (self.global - (s & 15) as i32).max(0);
}
}
20 => {
if tick == p as u32 {
self.key_off(c);
}
}
_ => {}
}
let ch = &mut self.chans[c];
// volume column (per tick)
let v = cell.vol;
match v {
0x60..=0x6F => ch.vol = (ch.vol - (v & 15) as i32).max(0),
0x70..=0x7F => ch.vol = (ch.vol + (v & 15) as i32).min(64),
0xB0..=0xBF => {
ch.vib_pos = ch.vib_pos.wrapping_add(ch.vib_speed * 4);
ch.vib_off = sine(ch.vib_pos) * ch.vib_depth as f32 * 8.0;
}
0xD0..=0xDF => ch.pan = (ch.pan - (v & 15) as i32).max(0),
0xE0..=0xEF => ch.pan = (ch.pan + (v & 15) as i32).min(255),
0xF0..=0xFF => tone_porta(ch),
_ => {}
}
match cell.fx {
1 => ch.period = (ch.period - ch.porta_up as f32 * 4.0).max(1.0),
2 => ch.period += ch.porta_down as f32 * 4.0,
3 => tone_porta(ch),
4 => {
ch.vib_pos = ch.vib_pos.wrapping_add(ch.vib_speed * 4);
ch.vib_off = sine(ch.vib_pos) * ch.vib_depth as f32 * 8.0;
}
5 => {
tone_porta(ch);
vol_slide(ch);
}
6 => {
ch.vib_pos = ch.vib_pos.wrapping_add(ch.vib_speed * 4);
ch.vib_off = sine(ch.vib_pos) * ch.vib_depth as f32 * 8.0;
vol_slide(ch);
}
7 => {
ch.trem_pos = ch.trem_pos.wrapping_add(ch.trem_speed * 4);
ch.trem_off = (sine(ch.trem_pos) * ch.trem_depth as f32 * 4.0) as i32;
}
0xA => vol_slide(ch),
0xE => match hi {
9 => {
if lo > 0 && tick % lo as u32 == 0 {
ch.pos = 0.0;
ch.active = ch.smp.is_some();
}
}
0xC => {
if tick == lo as u32 {
ch.vol = 0;
}
}
_ => {}
},
25 => {
let s = ch.pan_slide;
if s >> 4 > 0 {
ch.pan = (ch.pan + (s >> 4) as i32).min(255);
} else {
ch.pan = (ch.pan - (s & 15) as i32).max(0);
}
}
27 => {
if ch.retrig > 0 && tick % ch.retrig as u32 == 0 {
ch.pos = 0.0;
ch.active = ch.smp.is_some();
}
}
_ => {}
}
}
fn vol_slide(ch: &mut Chan) {
let s = ch.vol_slide;
if s >> 4 > 0 {
ch.vol = (ch.vol + (s >> 4) as i32).min(64);
} else {
ch.vol = (ch.vol - (s & 15) as i32).max(0);
}
}
fn tone_porta(ch: &mut Chan) {
let sp = ch.tone_speed as f32 * 4.0;
if ch.period < ch.target {
ch.period = (ch.period + sp).min(ch.target);
} else if ch.period > ch.target {
ch.period = (ch.period - sp).max(ch.target);
}
}
}
// Envelopes, auto-vibrato, arpeggio -> final step and volumes for this tick
fn update_outputs(&mut self) {
let m = self.module.clone();
let tick = self.tick;
let global = self.global as f32 / 64.0;
for c in 0..self.chans.len() {
let ins = m.instruments.get(self.chans[c].inst.wrapping_sub(1));
let mut period = self.chans[c].period + self.chans[c].vib_off;
// arpeggio
let arp = if self.chans[c].cell.fx == 0 { self.chans[c].arp } else { 0 };
if arp != 0 {
let semis = match tick % 3 {
1 => (arp >> 4) as i32,
2 => (arp & 15) as i32,
_ => 0,
};
if self.module.linear {
period -= semis as f32 * 64.0;
} else {
period /= 2f32.powf(semis as f32 / 12.0);
}
}
let mut venv = 64.0;
let mut penv = 32.0;
if let Some(ins) = ins {
let ch = &mut self.chans[c];
// auto-vibrato
if ins.vib_depth > 0 && ins.vib_rate > 0 {
let sweep = if ins.vib_sweep == 0 { 1.0 } else { (ch.av_sweep as f32 / ins.vib_sweep as f32).min(1.0) };
ch.av_sweep += 1;
ch.av_pos = ch.av_pos.wrapping_add(ins.vib_rate);
let w = (ch.av_pos as f32 / 256.0 * std::f32::consts::TAU).sin();
period += w * ins.vib_depth as f32 * 2.0 * sweep;
}
if ins.vol_env.on {
venv = Self::env_value(&ins.vol_env, ch.venv_pos);
Self::env_advance(&ins.vol_env, &mut ch.venv_pos, ch.key_on);
}
if ins.pan_env.on {
penv = Self::env_value(&ins.pan_env, ch.penv_pos);
Self::env_advance(&ins.pan_env, &mut ch.penv_pos, ch.key_on);
}
if !ch.key_on && ins.vol_env.on {
ch.fade = (ch.fade - ins.fadeout as i32).max(0);
}
}
let freq = self.freq_of(period.max(1.0));
let ch = &mut self.chans[c];
ch.step = freq as f64 / self.rate as f64;
let vol = ((ch.vol + ch.trem_off).clamp(0, 64)) as f32 / 64.0 * venv / 64.0 * ch.fade as f32 / 65536.0 * global;
let pan = ch.pan as f32 + (penv - 32.0) * (128.0 - (ch.pan as f32 - 128.0).abs()) / 32.0;
let pan = (pan / 255.0).clamp(0.0, 1.0);
ch.out_l = vol * (1.0 - pan).sqrt();
ch.out_r = vol * pan.sqrt();
if !ch.active {
ch.out_l = 0.0;
ch.out_r = 0.0;
}
}
}
fn next_row(&mut self) {
let m = self.module.clone();
if self.pdelay > 0 {
self.pdelay -= 1;
self.pdelay_active = true;
return;
}
self.pdelay_active = false;
if let Some(r) = self.loop_jump.take() {
self.row = r;
return;
}
if self.jump.is_some() || self.brk.is_some() {
let new_pos = self.jump.take().unwrap_or(self.pos + 1);
let new_row = self.brk.take().unwrap_or(0);
if new_pos <= self.pos {
self.loops += 1;
}
self.pos = new_pos;
self.row = new_row;
for ch in self.chans.iter_mut() {
ch.loop_row = 0;
ch.loop_count = 0;
}
} else {
self.row += 1;
let rows = m.patterns.get(*m.order.get(self.pos).unwrap_or(&0) as usize).map_or(64, |p| p.rows);
if self.row >= rows {
self.row = 0;
self.pos += 1;
for ch in self.chans.iter_mut() {
ch.loop_row = 0;
ch.loop_count = 0;
}
}
}
if self.pos >= m.order.len() {
self.pos = if m.restart < m.order.len() { m.restart } else { 0 };
self.loops += 1;
}
self.skip_invalid_orders();
let rows = m.patterns[m.order[self.pos] as usize].rows;
if self.row >= rows {
self.row = 0;
}
}
fn do_tick(&mut self) {
if self.tick == 0 {
if !self.pdelay_active {
self.process_row();
}
} else {
self.effects_tick();
}
self.update_outputs();
self.tick += 1;
if self.tick >= self.speed {
self.tick = 0;
self.next_row();
}
}
// Mix into an interleaved stereo buffer (adds to it)
pub fn render(&mut self, out: &mut [f32], gain: f32) {
let m = self.module.clone();
let frames = out.len() / 2;
let mut f = 0;
while f < frames {
if self.samples_left <= 0.0 {
self.do_tick();
self.samples_left += self.rate * 2.5 / self.bpm.max(1) as f32;
}
let n = (self.samples_left.ceil() as usize).clamp(1, frames - f);
for ch in self.chans.iter_mut() {
// short volume ramp towards the tick's target (no clicks)
let ramp = (n as f32).min(64.0);
let dl = (ch.out_l - ch.cur_l) / ramp;
let dr = (ch.out_r - ch.cur_r) / ramp;
if !ch.active && ch.cur_l.abs() < 1e-4 && ch.cur_r.abs() < 1e-4 {
continue;
}
let Some(s) = Self::sample(&m, ch) else { continue };
let len = s.data.len();
if len == 0 {
ch.active = false;
continue;
}
let lend = (s.loop_start + s.loop_len) as f64;
for k in 0..n {
if (k as f32) < ramp {
ch.cur_l += dl;
ch.cur_r += dr;
}
if !ch.active {
continue;
}
let i = ch.pos as usize;
let fr = (ch.pos - i as f64) as f32;
let a = s.data[i.min(len - 1)];
let next = if s.loop_type != 0 && i + 1 >= lend as usize { s.data[s.loop_start] } else { s.data[(i + 1).min(len - 1)] };
let v = (a + (next - a) * fr) * gain;
let o = (f + k) * 2;
out[o] += v * ch.cur_l;
out[o + 1] += v * ch.cur_r;
if ch.backwards {
ch.pos -= ch.step;
if ch.pos < s.loop_start as f64 {
ch.pos = 2.0 * s.loop_start as f64 - ch.pos;
ch.backwards = false;
}
} else {
ch.pos += ch.step;
match s.loop_type {
1 if ch.pos >= lend => {
ch.pos -= s.loop_len as f64 * ((ch.pos - lend) / s.loop_len as f64 + 1.0).floor();
}
2 if ch.pos >= lend => {
ch.pos = (2.0 * lend - ch.pos - 1.0).max(s.loop_start as f64);
ch.backwards = true;
}
0 if ch.pos >= len as f64 => ch.active = false,
_ => {}
}
}
}
}
self.samples_left -= n as f32;
f += n;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::rc::Rc;
#[test]
fn render_all_songs() {
for (k, (name, _, data)) in crate::tracks::TRACKS.iter().enumerate() {
let m = Rc::new(load(data).expect("load"));
assert!(m.channels == 8 || (m.channels == 10 && name.starts_with("surface")), "{} {}", name, m.channels);
let mut p = Player::new(m, 48000.0);
let mut buf = vec![0.0f32; 48000 * 2];
let mut peak = 0.0f32;
let mut sum = 0.0f64;
for _ in 0..20 {
buf.iter_mut().for_each(|v| *v = 0.0);
p.render(&mut buf, 1.0);
for &v in &buf {
assert!(v.is_finite());
peak = peak.max(v.abs());
sum += (v * v) as f64;
}
}
let rms = (sum / (20.0 * 96000.0)).sqrt();
println!("{:2} {:14}: peak {:.3} rms {:.4} loops {}", k, name, peak, rms, p.loops);
assert!(rms > 0.005, "song {} is silent", name);
}
}
}
// Writes 30 s renders of all songs as raw stereo f32 files when DC_RENDER_DIR is set
// (used to compare against a reference player).
#[cfg(test)]
mod render_dump {
use super::*;
use std::rc::Rc;
#[test]
fn dump_renders() {
let Ok(dir) = std::env::var("DC_RENDER_DIR") else { return };
for (n, _, _) in crate::tracks::TRACKS.iter() {
let data = std::fs::read(format!("assets/music/{}.xm", n)).unwrap();
let mut p = Player::new(Rc::new(load(&data).unwrap()), 48000.0);
let mut buf = vec![0.0f32; 48000 * 2 * 30];
p.render(&mut buf, 1.0);
let bytes: Vec<u8> = buf.iter().flat_map(|v| v.to_le_bytes()).collect();
std::fs::write(format!("{}/{}.f32", dir, n), bytes).unwrap();
}
}
}