use oxideav_core::{CodecId, CodecParameters, Error, Frame, Packet, TimeBase};
use oxideav_core::{CodecRegistry, Decoder};
use oxideav_mod::{container::OUTPUT_SAMPLE_RATE, register_codecs, CODEC_ID_STR};
const HEADER_FIXED_SIZE: usize = 1084;
const PATTERN_BYTES: usize = 64 * 4 * 4;
fn write_note(
pat: &mut [u8],
row: usize,
channel: usize,
period: u16,
sample: u8,
effect: u8,
effect_param: u8,
) {
let off = row * 4 * 4 + channel * 4;
let p_hi = ((period >> 8) & 0x0F) as u8;
let p_lo = (period & 0xFF) as u8;
let sample_hi = (sample & 0xF0) >> 4;
let sample_lo = sample & 0x0F;
pat[off] = (sample_hi << 4) | p_hi;
pat[off + 1] = p_lo;
pat[off + 2] = (sample_lo << 4) | effect;
pat[off + 3] = effect_param;
}
fn build_mod<F: Fn(&mut [u8])>(populate_pattern: F) -> Vec<u8> {
let mut out = vec![0u8; 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; PATTERN_BYTES];
populate_pattern(&mut pat);
out.extend(pat);
for i in 0..32 {
let v: i8 = if i < 16 { 100 } else { -100 };
out.push(v as u8);
}
out
}
fn decode_interleaved(bytes: Vec<u8>, max_frames: usize) -> Vec<i16> {
let mut reg = CodecRegistry::new();
register_codecs(&mut reg);
let codec_id = CodecId::new(CODEC_ID_STR);
let params = CodecParameters::audio(codec_id);
let mut dec: Box<dyn Decoder> = reg.first_decoder(¶ms).expect("decoder available");
let pkt = Packet::new(0, TimeBase::new(1, OUTPUT_SAMPLE_RATE as i64), bytes);
dec.send_packet(&pkt).expect("send_packet");
let mut pcm: Vec<i16> = Vec::new();
loop {
match dec.receive_frame() {
Ok(Frame::Audio(a)) => {
for chunk in a.data[0].chunks_exact(2) {
pcm.push(i16::from_le_bytes([chunk[0], chunk[1]]));
}
if pcm.len() / 2 >= max_frames {
break;
}
}
Ok(_) => unreachable!("MOD emits audio only"),
Err(Error::Eof) => break,
Err(e) => panic!("decode error: {e:?}"),
}
}
pcm
}
#[test]
fn vibrato_introduces_period_variation_in_output() {
let no_vib = build_mod(|p| {
write_note(p, 0, 0, 428, 1, 0, 0);
});
let with_vib = build_mod(|p| {
write_note(p, 0, 0, 428, 1, 0x4, 0x88);
write_note(p, 1, 0, 0, 0, 0x4, 0x00);
write_note(p, 2, 0, 0, 0, 0x4, 0x00);
write_note(p, 3, 0, 0, 0, 0x4, 0x00);
});
let pcm_plain = decode_interleaved(no_vib, 17640);
let pcm_vib = decode_interleaved(with_vib, 17640);
fn zcr_spread(pcm: &[i16]) -> u32 {
let mut last = None;
let mut intervals = Vec::new();
for (i, &s) in pcm.iter().step_by(2).enumerate() {
if last.is_none() && s != 0 {
last = Some((i, s));
continue;
}
if let Some((prev_i, prev_s)) = last {
if (prev_s < 0) != (s < 0) && s != 0 {
intervals.push(i - prev_i);
last = Some((i, s));
}
}
}
if intervals.is_empty() {
return 0;
}
let max = *intervals.iter().max().unwrap();
let min = *intervals.iter().min().unwrap();
(max - min) as u32
}
let plain_spread = zcr_spread(&pcm_plain);
let vib_spread = zcr_spread(&pcm_vib);
assert!(
vib_spread > plain_spread,
"vibrato run should produce more zero-crossing spread; \
plain_spread={plain_spread}, vib_spread={vib_spread}"
);
}
#[test]
fn pattern_break_advances_order_and_row() {
let mod_bytes = build_mod(|p| {
write_note(p, 0, 0, 428, 1, 0, 0);
write_note(p, 5, 0, 0, 0, 0xD, 0x10);
});
let pcm = decode_interleaved(mod_bytes, 44100);
assert!(
!pcm.is_empty() && pcm.len() < 44100 * 2,
"expected bounded output after pattern break; got {}",
pcm.len()
);
}
#[test]
fn speed_command_halves_row_duration() {
let slow = build_mod(|p| {
for i in 0..16 {
write_note(p, i, 0, 428, 1, 0, 0);
}
});
let fast = build_mod(|p| {
write_note(p, 0, 0, 428, 1, 0xF, 0x03);
for i in 1..16 {
write_note(p, i, 0, 428, 1, 0, 0);
}
});
let pcm_slow = decode_interleaved(slow, 8820);
let pcm_fast = decode_interleaved(fast, 8820);
let rms_slow = rms(&pcm_slow);
let rms_fast = rms(&pcm_fast);
assert!(rms_slow > 100, "slow RMS {rms_slow}");
assert!(rms_fast > 100, "fast RMS {rms_fast}");
}
fn rms(pcm: &[i16]) -> u32 {
if pcm.is_empty() {
return 0;
}
let sum_sq: u64 = pcm.iter().map(|&s| (s as i64 * s as i64) as u64).sum();
((sum_sq / pcm.len() as u64) as f64).sqrt() as u32
}
#[test]
fn volume_slide_decays_amplitude() {
let mod_bytes = build_mod(|p| {
write_note(p, 0, 0, 428, 1, 0xC, 0x40);
for row in 1..8 {
write_note(p, row, 0, 0, 0, 0xA, 0x04);
}
});
let pcm = decode_interleaved(mod_bytes, 44100);
let row_frames = 6 * 882; let early_end = row_frames * 2 * 2; let late_start = row_frames * 2 * 6;
let late_end = (row_frames * 2 * 8).min(pcm.len());
if late_end > late_start && early_end < pcm.len() {
let early_rms = rms(&pcm[..early_end]);
let late_rms = rms(&pcm[late_start..late_end]);
assert!(
early_rms > late_rms,
"volume slide should decay RMS; early={early_rms}, late={late_rms}"
);
}
}