use crate::header::{ModHeader, PATTERN_ROWS};
use crate::samples::SampleBody;
pub const PAULA_CLOCK: f32 = 7_093_789.2 / 2.0;
pub const DEFAULT_SPEED: u8 = 6;
pub const DEFAULT_BPM: u8 = 125;
pub const CHANNELS_PER_MOD: usize = 4;
#[derive(Clone, Copy, Debug, Default)]
pub struct Note {
pub period: u16,
pub sample: u8,
pub effect: u8,
pub effect_param: u8,
}
impl Note {
fn decode(raw: [u8; 4]) -> Self {
let period = (((raw[0] & 0x0F) as u16) << 8) | raw[1] as u16;
let sample = (raw[0] & 0xF0) | (raw[2] >> 4);
let effect = raw[2] & 0x0F;
let effect_param = raw[3];
Note {
period,
sample,
effect,
effect_param,
}
}
}
#[derive(Clone, Debug)]
pub struct Pattern {
pub rows: Vec<Vec<Note>>, }
pub fn parse_patterns(header: &ModHeader, bytes: &[u8]) -> Vec<Pattern> {
let channels = header.channels as usize;
let mut patterns = Vec::with_capacity(header.n_patterns as usize);
let base = header.pattern_data_offset();
for p in 0..header.n_patterns as usize {
let mut rows = Vec::with_capacity(PATTERN_ROWS);
for r in 0..PATTERN_ROWS {
let mut row = Vec::with_capacity(channels);
for c in 0..channels {
let off = base + (p * PATTERN_ROWS + r) * channels * 4 + c * 4;
let raw = if off + 4 <= bytes.len() {
[bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]]
} else {
[0; 4]
};
row.push(Note::decode(raw));
}
rows.push(row);
}
patterns.push(Pattern { rows });
}
patterns
}
#[derive(Clone, Debug, Default)]
pub struct Channel {
pub sample_index: u8,
pub sample_pos: f32,
pub period: u16,
pub volume: u8,
pub active: bool,
pub arp_base_period: u16,
pub effect: u8,
pub effect_param: u8,
}
impl Channel {
fn mix_one(&mut self, samples: &[SampleBody], out_rate: f32) -> f32 {
if !self.active || self.period == 0 {
return 0.0;
}
let idx = self.sample_index as usize;
if idx == 0 || idx > samples.len() {
return 0.0;
}
let body = &samples[idx - 1];
if body.pcm.is_empty() {
return 0.0;
}
let pos = self.sample_pos;
let len = body.pcm.len() as f32;
if pos >= len {
if body.is_looped() {
let loop_end = (body.loop_start + body.loop_length) as f32;
let loop_start = body.loop_start as f32;
let span = loop_end - loop_start;
if span > 0.0 {
let over = pos - loop_start;
self.sample_pos = loop_start + over.rem_euclid(span);
} else {
self.active = false;
return 0.0;
}
} else {
self.active = false;
return 0.0;
}
}
let i = self.sample_pos as usize;
let frac = self.sample_pos - i as f32;
let s0 = body.pcm[i.min(body.pcm.len() - 1)] as f32 / 128.0;
let s1_idx = if i + 1 < body.pcm.len() {
i + 1
} else if body.is_looped() {
body.loop_start as usize
} else {
i
};
let s1 = body.pcm[s1_idx.min(body.pcm.len() - 1)] as f32 / 128.0;
let interp = s0 + (s1 - s0) * frac;
let out = interp * (self.volume as f32 / 64.0);
let chan_rate = PAULA_CLOCK / self.period as f32;
let step = chan_rate / out_rate;
self.sample_pos += step;
out
}
}
pub struct PlayerState {
pub samples: Vec<SampleBody>,
pub patterns: Vec<Pattern>,
pub order: Vec<u8>,
pub song_length: u8,
pub channels: Vec<Channel>,
pub speed: u8,
pub bpm: u8,
pub order_index: u8,
pub row: u8,
pub tick: u8,
pub tick_sample_cursor: u32,
pub sample_rate: u32,
pub ended: bool,
pending_jump: Option<Jump>,
}
#[derive(Clone, Copy, Debug)]
struct Jump {
order: Option<u8>,
row: u8,
}
impl PlayerState {
pub fn new(
header: &ModHeader,
samples: Vec<SampleBody>,
patterns: Vec<Pattern>,
sample_rate: u32,
) -> Self {
let channels = (0..header.channels)
.map(|_| Channel::default())
.collect::<Vec<_>>();
PlayerState {
samples,
patterns,
order: header.order.clone(),
song_length: header.song_length,
channels,
speed: DEFAULT_SPEED,
bpm: DEFAULT_BPM,
order_index: 0,
row: 0,
tick: 0,
tick_sample_cursor: 0,
sample_rate,
ended: false,
pending_jump: None,
}
}
pub fn samples_per_tick(&self) -> u32 {
((self.sample_rate as f32) * 2.5 / self.bpm as f32) as u32
}
fn enter_row(&mut self) {
let pattern_idx = self
.order
.get(self.order_index as usize)
.copied()
.unwrap_or(0) as usize;
if pattern_idx >= self.patterns.len() {
self.ended = true;
return;
}
let row_notes: Vec<Note> = self.patterns[pattern_idx].rows[self.row as usize].clone();
for (ch_idx, note) in row_notes.iter().enumerate() {
if ch_idx >= self.channels.len() {
break;
}
let ch = &mut self.channels[ch_idx];
ch.effect = note.effect;
ch.effect_param = note.effect_param;
if note.sample != 0 {
ch.sample_index = note.sample;
if let Some(body) = self.samples.get(note.sample as usize - 1) {
ch.volume = body.volume;
}
}
if note.period != 0 {
ch.period = note.period;
ch.sample_pos = 0.0;
ch.active = true;
ch.arp_base_period = note.period;
} else {
ch.arp_base_period = ch.period;
}
apply_tick0_effect(ch, note.effect, note.effect_param, &mut self.pending_jump);
}
for ch in &self.channels {
if ch.effect == 0xF {
let p = ch.effect_param;
if p == 0 {
} else if p < 0x20 {
self.speed = p;
} else {
self.bpm = p;
}
}
}
}
fn advance_tick(&mut self) {
if self.tick == 0 {
self.enter_row();
} else {
for ch in &mut self.channels {
apply_tickn_effect(ch, self.tick);
}
}
}
fn next_row(&mut self) {
if let Some(jump) = self.pending_jump.take() {
if let Some(order) = jump.order {
self.order_index = order;
} else {
self.order_index = self.order_index.saturating_add(1);
}
self.row = jump.row;
} else {
self.row += 1;
if self.row as usize >= PATTERN_ROWS {
self.row = 0;
self.order_index = self.order_index.saturating_add(1);
}
}
if self.order_index >= self.song_length {
self.ended = true;
}
}
pub fn channel_is_left(i: usize) -> bool {
matches!(i % 4, 0 | 3)
}
fn sample_all_channels(&mut self, per_channel: &mut [f32]) -> (f32, f32) {
let out_rate = self.sample_rate as f32;
let mut l = 0.0f32;
let mut r = 0.0f32;
let n_ch = self.channels.len();
for (i, ch) in self.channels.iter_mut().enumerate() {
let s = ch.mix_one(&self.samples, out_rate);
per_channel[i] = s;
if Self::channel_is_left(i) {
l += s;
} else {
r += s;
}
}
let norm = (n_ch as f32 / 2.0).max(1.0);
(l / norm, r / norm)
}
fn render_one(&mut self, out: &mut [i16]) {
let mut per_channel = vec![0.0f32; self.channels.len()];
let (l, r) = self.sample_all_channels(&mut per_channel);
let l = l.clamp(-1.0, 1.0);
let r = r.clamp(-1.0, 1.0);
out[0] = (l * 32767.0) as i16;
out[1] = (r * 32767.0) as i16;
}
pub fn render(&mut self, dst: &mut [i16]) -> usize {
assert!(dst.len() % 2 == 0);
let mut produced = 0usize;
let total_frames = dst.len() / 2;
while produced < total_frames {
if self.ended {
break;
}
if self.tick_sample_cursor == 0 {
self.advance_tick();
}
let spt = self.samples_per_tick().max(1);
let remaining_in_tick = spt.saturating_sub(self.tick_sample_cursor);
let want = (total_frames - produced).min(remaining_in_tick as usize);
for _ in 0..want {
let off = produced * 2;
self.render_one(&mut dst[off..off + 2]);
produced += 1;
}
self.tick_sample_cursor += want as u32;
if self.tick_sample_cursor >= spt {
self.tick_sample_cursor = 0;
self.tick += 1;
if self.tick >= self.speed {
self.tick = 0;
self.next_row();
}
}
}
produced
}
pub fn render_per_channel(&mut self, planes: &mut [&mut [i16]], n_frames: usize) -> usize {
assert_eq!(
planes.len(),
self.channels.len(),
"render_per_channel: plane count must equal MOD channel count"
);
for p in planes.iter() {
assert!(
p.len() >= n_frames,
"render_per_channel: every plane must hold at least n_frames samples"
);
}
let mut produced = 0usize;
let mut scratch = vec![0.0f32; self.channels.len()];
while produced < n_frames {
if self.ended {
break;
}
if self.tick_sample_cursor == 0 {
self.advance_tick();
}
let spt = self.samples_per_tick().max(1);
let remaining_in_tick = spt.saturating_sub(self.tick_sample_cursor);
let want = (n_frames - produced).min(remaining_in_tick as usize);
for _ in 0..want {
let _ = self.sample_all_channels(&mut scratch);
for (ch_idx, plane) in planes.iter_mut().enumerate() {
let v = scratch[ch_idx].clamp(-1.0, 1.0);
plane[produced] = (v * 32767.0) as i16;
}
produced += 1;
}
self.tick_sample_cursor += want as u32;
if self.tick_sample_cursor >= spt {
self.tick_sample_cursor = 0;
self.tick += 1;
if self.tick >= self.speed {
self.tick = 0;
self.next_row();
}
}
}
produced
}
}
fn apply_tick0_effect(ch: &mut Channel, effect: u8, param: u8, pending_jump: &mut Option<Jump>) {
let x = param >> 4;
let y = param & 0x0F;
match effect {
0xC => {
ch.volume = param.min(64);
}
0xB => {
*pending_jump = Some(Jump {
order: Some(param),
row: 0,
});
}
0xD => {
let row = (x * 10 + y).min(63);
*pending_jump = Some(Jump { order: None, row });
}
_ => {
}
}
}
fn apply_tickn_effect(ch: &mut Channel, tick: u8) {
let effect = ch.effect;
let param = ch.effect_param;
let x = param >> 4;
let y = param & 0x0F;
match effect {
0x0 if param != 0 => {
let semis = match tick % 3 {
0 => 0,
1 => x as i32,
2 => y as i32,
_ => 0,
};
if semis == 0 {
ch.period = ch.arp_base_period;
} else {
let factor = 2.0f32.powf(semis as f32 / 12.0);
let p = (ch.arp_base_period as f32 / factor) as u16;
ch.period = p.max(1);
}
}
0x1 => {
ch.period = ch.period.saturating_sub(param as u16).max(113);
}
0x2 => {
ch.period = (ch.period + param as u16).min(856);
}
0xA => {
if x != 0 {
ch.volume = (ch.volume as u16 + x as u16).min(64) as u8;
} else if y != 0 {
ch.volume = ch.volume.saturating_sub(y);
}
}
_ => {}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::header::parse_header;
use crate::samples::extract_samples;
pub fn synth_square_mod() -> Vec<u8> {
let mut out = vec![0u8; crate::header::HEADER_FIXED_SIZE];
out[0..4].copy_from_slice(b"test");
out[20 + 22..20 + 24].copy_from_slice(&16u16.to_be_bytes());
out[20 + 24] = 0;
out[20 + 25] = 64;
out[20 + 26..20 + 28].copy_from_slice(&0u16.to_be_bytes());
out[20 + 28..20 + 30].copy_from_slice(&16u16.to_be_bytes());
out[950] = 1;
out[951] = 0x7F;
out[952] = 0;
out[1080..1084].copy_from_slice(b"M.K.");
let mut pat = vec![0u8; 64 * 4 * 4];
let rows_and_periods = [(0, 428u16), (16, 381), (32, 339), (48, 320)];
for &(row, period) in &rows_and_periods {
let off = row * 4 * 4;
let p_hi = ((period >> 8) & 0x0F) as u8;
let p_lo = (period & 0xFF) as u8;
let sample_hi = 0u8; let sample_lo = 1u8;
pat[off] = (sample_hi << 4) | p_hi;
pat[off + 1] = p_lo;
pat[off + 2] = sample_lo << 4; pat[off + 3] = 0; }
out.extend(pat);
for i in 0..32 {
let v: i8 = if i < 16 { 100 } else { -100 };
out.push(v as u8);
}
out
}
#[test]
fn decodes_patterns() {
let bytes = synth_square_mod();
let header = parse_header(&bytes).unwrap();
let patterns = parse_patterns(&header, &bytes);
assert_eq!(patterns.len(), 1);
assert_eq!(patterns[0].rows.len(), 64);
assert_eq!(patterns[0].rows[0].len(), 4);
let n = patterns[0].rows[0][0];
assert_eq!(n.period, 428);
assert_eq!(n.sample, 1);
}
#[test]
fn player_renders_nonzero_audio() {
let bytes = synth_square_mod();
let header = parse_header(&bytes).unwrap();
let samples = extract_samples(&header, &bytes);
let patterns = parse_patterns(&header, &bytes);
let mut player = PlayerState::new(&header, samples, patterns, 44_100);
let mut buf = vec![0i16; 4410 * 2];
let produced = player.render(&mut buf);
assert_eq!(produced, 4410);
let nonzero = buf.iter().filter(|&&x| x != 0).count();
assert!(
nonzero > 100,
"expected non-silent PCM, got {nonzero} non-zero samples"
);
}
#[test]
fn samples_per_tick_default() {
let bytes = synth_square_mod();
let header = parse_header(&bytes).unwrap();
let samples = extract_samples(&header, &bytes);
let patterns = parse_patterns(&header, &bytes);
let player = PlayerState::new(&header, samples, patterns, 44_100);
assert_eq!(player.samples_per_tick(), 882);
}
#[test]
fn render_per_channel_isolates_channels() {
let bytes = synth_square_mod();
let header = parse_header(&bytes).unwrap();
let samples = extract_samples(&header, &bytes);
let patterns = parse_patterns(&header, &bytes);
let mut player = PlayerState::new(&header, samples, patterns, 44_100);
let n_frames = 4410;
let mut planes: Vec<Vec<i16>> = (0..player.channels.len())
.map(|_| vec![0i16; n_frames])
.collect();
let produced = {
let mut views: Vec<&mut [i16]> = planes.iter_mut().map(|v| v.as_mut_slice()).collect();
player.render_per_channel(&mut views, n_frames)
};
assert_eq!(produced, n_frames);
let ch0_nonzero = planes[0].iter().filter(|&&s| s != 0).count();
assert!(
ch0_nonzero > 100,
"channel 0 should carry audible signal, got {ch0_nonzero} non-zero samples"
);
for (i, plane) in planes.iter().enumerate().skip(1) {
let nonzero = plane.iter().filter(|&&s| s != 0).count();
assert_eq!(
nonzero, 0,
"channel {i} should be silent in synth_square_mod, got {nonzero} non-zero samples"
);
}
}
#[test]
fn render_per_channel_matches_mixed_song_length() {
let bytes = synth_square_mod();
let header = parse_header(&bytes).unwrap();
let samples_a = extract_samples(&header, &bytes);
let patterns_a = parse_patterns(&header, &bytes);
let mut player_mixed = PlayerState::new(&header, samples_a, patterns_a, 44_100);
let samples_b = extract_samples(&header, &bytes);
let patterns_b = parse_patterns(&header, &bytes);
let mut player_planar = PlayerState::new(&header, samples_b, patterns_b, 44_100);
let n_frames = 2205;
let mut mixed = vec![0i16; n_frames * 2];
let produced_mixed = player_mixed.render(&mut mixed);
let mut planes: Vec<Vec<i16>> = (0..player_planar.channels.len())
.map(|_| vec![0i16; n_frames])
.collect();
let produced_planar = {
let mut views: Vec<&mut [i16]> = planes.iter_mut().map(|v| v.as_mut_slice()).collect();
player_planar.render_per_channel(&mut views, n_frames)
};
assert_eq!(produced_mixed, n_frames);
assert_eq!(produced_planar, n_frames);
}
}