1174 lines
40 KiB
Rust
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();
|
|
}
|
|
}
|
|
}
|