// 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, 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, pub lp: Option<(usize, usize)>, pub on: bool, } #[derive(Clone)] pub struct Instrument { pub samples: Vec, 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, } pub struct Module { #[allow(dead_code)] // shown by tools/debugging pub name: String, pub channels: usize, pub order: Vec, pub restart: usize, pub patterns: Vec, pub instruments: Vec, pub linear: bool, pub speed: u32, pub bpm: u32, pub pans: Vec, // 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 { if b.len() > 17 && &b[0..17] == b"Extended Module: " { load_xm(b) } else { load_mod(b) } } pub fn load_xm(b: &[u8]) -> Result { 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 = (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 { 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 = (0..128).map(|i| byte(b, 952 + i)).collect(); let order: Vec = 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 = 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, 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, 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, rate: f32, speed: u32, bpm: u32, tick: u32, row: usize, pos: usize, pdelay: u32, pdelay_active: bool, global: i32, global_slide: u8, chans: Vec, samples_left: f32, jump: Option, brk: Option, loop_jump: Option, 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, 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) { 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 = buf.iter().flat_map(|v| v.to_le_bytes()).collect(); std::fs::write(format!("{}/{}.f32", dir, n), bytes).unwrap(); } } }