use oxideav_core::{
AudioFrame, CodecCapabilities, CodecId, CodecParameters, Error, Frame, Packet, Result,
SampleFormat, TimeBase,
};
use oxideav_core::{CodecInfo, CodecRegistry, Decoder};
use crate::container::OUTPUT_SAMPLE_RATE;
use crate::header::parse_header;
use crate::player::{parse_patterns, PlayerState};
use crate::samples::extract_samples;
pub fn register(reg: &mut CodecRegistry) {
let mixed_caps = CodecCapabilities::audio("mod_sw")
.with_lossy(false)
.with_lossless(true)
.with_intra_only(false)
.with_max_channels(32)
.with_max_sample_rate(OUTPUT_SAMPLE_RATE);
reg.register(
CodecInfo::new(CodecId::new(crate::CODEC_ID_STR))
.capabilities(mixed_caps)
.decoder(make_mixed_decoder),
);
let planar_caps = CodecCapabilities::audio("mod_sw_planar")
.with_lossy(false)
.with_lossless(true)
.with_intra_only(false)
.with_max_channels(32)
.with_max_sample_rate(OUTPUT_SAMPLE_RATE);
reg.register(
CodecInfo::new(CodecId::new(crate::CODEC_ID_PLANAR_STR))
.capabilities(planar_caps)
.decoder(make_planar_decoder),
);
let stm_caps = CodecCapabilities::audio("stm_sw")
.with_lossy(false)
.with_lossless(true)
.with_intra_only(false)
.with_max_channels(4)
.with_max_sample_rate(OUTPUT_SAMPLE_RATE);
reg.register(
CodecInfo::new(CodecId::new(crate::CODEC_ID_STM_STR))
.capabilities(stm_caps)
.decoder(make_stm_stub_decoder),
);
let xm_caps = CodecCapabilities::audio("xm_sw")
.with_lossy(false)
.with_lossless(true)
.with_intra_only(false)
.with_max_channels(32)
.with_max_sample_rate(OUTPUT_SAMPLE_RATE);
reg.register(
CodecInfo::new(CodecId::new(crate::CODEC_ID_XM_STR))
.capabilities(xm_caps)
.decoder(make_xm_stub_decoder),
);
}
fn make_mixed_decoder(_params: &CodecParameters) -> Result<Box<dyn Decoder>> {
Ok(Box::new(ModDecoder {
codec_id: CodecId::new(crate::CODEC_ID_STR),
state: DecoderState::AwaitingPacket,
}))
}
fn make_planar_decoder(_params: &CodecParameters) -> Result<Box<dyn Decoder>> {
Ok(Box::new(ModPlanarDecoder {
codec_id: CodecId::new(crate::CODEC_ID_PLANAR_STR),
state: DecoderState::AwaitingPacket,
}))
}
fn make_stm_stub_decoder(_params: &CodecParameters) -> Result<Box<dyn Decoder>> {
Ok(Box::new(StmStubDecoder {
codec_id: CodecId::new(crate::CODEC_ID_STM_STR),
}))
}
fn make_xm_stub_decoder(_params: &CodecParameters) -> Result<Box<dyn Decoder>> {
Ok(Box::new(XmStubDecoder {
codec_id: CodecId::new(crate::CODEC_ID_XM_STR),
}))
}
struct XmStubDecoder {
codec_id: CodecId,
}
impl Decoder for XmStubDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
if !crate::xm::is_xm(&packet.data) {
return Err(Error::invalid(
"XM: packet does not start with the 'Extended Module: ' banner",
));
}
crate::xm::parse_header(&packet.data)?;
Err(Error::unsupported(
"XM playback is not yet wired through the MOD mixer; use \
oxideav_mod::xm::parse_header() / parse_patterns() / \
parse_instruments() / extract_sample_bodies() directly for \
structural access",
))
}
fn receive_frame(&mut self) -> Result<Frame> {
Err(Error::Eof)
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
fn reset(&mut self) -> Result<()> {
Ok(())
}
}
struct StmStubDecoder {
codec_id: CodecId,
}
impl Decoder for StmStubDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
if !crate::stm::is_stm(&packet.data) {
return Err(Error::invalid(
"STM: packet does not carry a valid Scream Tracker v1 header",
));
}
Err(Error::unsupported(
"STM playback is not yet wired through the MOD mixer; use \
oxideav_mod::stm::parse_header() / parse_patterns() / extract_samples() \
directly for structural access",
))
}
fn receive_frame(&mut self) -> Result<Frame> {
Err(Error::Eof)
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
fn reset(&mut self) -> Result<()> {
Ok(())
}
}
struct ModDecoder {
codec_id: CodecId,
state: DecoderState,
}
enum DecoderState {
AwaitingPacket,
Playing {
player: Box<PlayerState>,
emit_pts: i64,
},
Done,
}
const CHUNK_FRAMES: u32 = 1024;
impl Decoder for ModDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
if !matches!(self.state, DecoderState::AwaitingPacket) {
return Err(Error::other(
"MOD decoder received a second packet; only one is expected per song",
));
}
let header = parse_header(&packet.data)?;
let samples = extract_samples(&header, &packet.data);
let patterns = parse_patterns(&header, &packet.data);
let player = PlayerState::new(&header, samples, patterns, OUTPUT_SAMPLE_RATE);
self.state = DecoderState::Playing {
player: Box::new(player),
emit_pts: 0,
};
Ok(())
}
fn receive_frame(&mut self) -> Result<Frame> {
match &mut self.state {
DecoderState::AwaitingPacket => Err(Error::NeedMore),
DecoderState::Done => Err(Error::Eof),
DecoderState::Playing { player, emit_pts } => {
let mut pcm = vec![0i16; CHUNK_FRAMES as usize * 2];
let produced = player.render(&mut pcm);
if produced == 0 {
self.state = DecoderState::Done;
return Err(Error::Eof);
}
pcm.truncate(produced * 2);
let mut bytes = Vec::with_capacity(pcm.len() * 2);
for s in &pcm {
bytes.extend_from_slice(&s.to_le_bytes());
}
let pts = *emit_pts;
*emit_pts += produced as i64;
Ok(Frame::Audio(AudioFrame {
format: SampleFormat::S16,
channels: 2,
sample_rate: OUTPUT_SAMPLE_RATE,
samples: produced as u32,
pts: Some(pts),
time_base: TimeBase::new(1, OUTPUT_SAMPLE_RATE as i64),
data: vec![bytes],
}))
}
}
}
fn flush(&mut self) -> Result<()> {
if let DecoderState::Playing { .. } = self.state {
}
Ok(())
}
fn reset(&mut self) -> Result<()> {
self.state = DecoderState::AwaitingPacket;
Ok(())
}
}
struct ModPlanarDecoder {
codec_id: CodecId,
state: DecoderState,
}
impl Decoder for ModPlanarDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
if !matches!(self.state, DecoderState::AwaitingPacket) {
return Err(Error::other(
"MOD decoder received a second packet; only one is expected per song",
));
}
let header = parse_header(&packet.data)?;
let samples = extract_samples(&header, &packet.data);
let patterns = parse_patterns(&header, &packet.data);
let player = PlayerState::new(&header, samples, patterns, OUTPUT_SAMPLE_RATE);
self.state = DecoderState::Playing {
player: Box::new(player),
emit_pts: 0,
};
Ok(())
}
fn receive_frame(&mut self) -> Result<Frame> {
match &mut self.state {
DecoderState::AwaitingPacket => Err(Error::NeedMore),
DecoderState::Done => Err(Error::Eof),
DecoderState::Playing { player, emit_pts } => {
let n_ch = player.channels.len();
let n_frames = CHUNK_FRAMES as usize;
let mut bufs: Vec<Vec<i16>> = (0..n_ch).map(|_| vec![0i16; n_frames]).collect();
let produced = {
let mut views: Vec<&mut [i16]> =
bufs.iter_mut().map(|b| b.as_mut_slice()).collect();
player.render_per_channel(&mut views, n_frames)
};
if produced == 0 {
self.state = DecoderState::Done;
return Err(Error::Eof);
}
let mut planes: Vec<Vec<u8>> = Vec::with_capacity(n_ch);
for buf in &bufs {
let mut bytes = Vec::with_capacity(produced * 2);
for &s in &buf[..produced] {
bytes.extend_from_slice(&s.to_le_bytes());
}
planes.push(bytes);
}
let pts = *emit_pts;
*emit_pts += produced as i64;
Ok(Frame::Audio(AudioFrame {
format: SampleFormat::S16P,
channels: n_ch as u16,
sample_rate: OUTPUT_SAMPLE_RATE,
samples: produced as u32,
pts: Some(pts),
time_base: TimeBase::new(1, OUTPUT_SAMPLE_RATE as i64),
data: planes,
}))
}
}
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
fn reset(&mut self) -> Result<()> {
self.state = DecoderState::AwaitingPacket;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::player::tests::synth_square_mod;
use oxideav_core::TimeBase;
#[test]
fn decoder_emits_nonsilent_pcm() {
let bytes = synth_square_mod();
let params = CodecParameters::audio(CodecId::new(crate::CODEC_ID_STR));
let mut dec = make_mixed_decoder(¶ms).unwrap();
let pkt = Packet::new(0, TimeBase::new(1, OUTPUT_SAMPLE_RATE as i64), bytes);
dec.send_packet(&pkt).unwrap();
let mut total_samples = 0u64;
let mut total_nonzero = 0u64;
loop {
match dec.receive_frame() {
Ok(Frame::Audio(a)) => {
assert_eq!(a.channels, 2);
assert_eq!(a.sample_rate, OUTPUT_SAMPLE_RATE);
assert_eq!(a.format, SampleFormat::S16);
total_samples += a.samples as u64;
let plane = &a.data[0];
for chunk in plane.chunks_exact(2) {
let s = i16::from_le_bytes([chunk[0], chunk[1]]);
if s != 0 {
total_nonzero += 1;
}
}
}
Ok(_) => unreachable!("MOD emits audio only"),
Err(Error::Eof) => break,
Err(e) => panic!("unexpected decode error: {e:?}"),
}
}
assert!(
total_samples > 1000,
"expected substantial sample output, got {total_samples}"
);
assert!(
total_nonzero > 100,
"expected non-silent PCM, got {total_nonzero} non-zero samples"
);
}
#[test]
fn planar_decoder_emits_one_plane_per_channel() {
let bytes = synth_square_mod();
let params = CodecParameters::audio(CodecId::new(crate::CODEC_ID_PLANAR_STR));
let mut dec = make_planar_decoder(¶ms).unwrap();
let pkt = Packet::new(0, TimeBase::new(1, OUTPUT_SAMPLE_RATE as i64), bytes);
dec.send_packet(&pkt).unwrap();
let mut got_frame = false;
let mut ch0_nonzero = 0u64;
let mut other_nonzero = 0u64;
loop {
match dec.receive_frame() {
Ok(Frame::Audio(a)) => {
got_frame = true;
assert_eq!(a.channels, 4);
assert_eq!(a.format, SampleFormat::S16P);
assert_eq!(a.sample_rate, OUTPUT_SAMPLE_RATE);
assert_eq!(a.data.len(), 4, "one plane per MOD channel");
let expected_plane_len = a.samples as usize * 2;
for plane in &a.data {
assert_eq!(plane.len(), expected_plane_len);
}
for (idx, plane) in a.data.iter().enumerate() {
for chunk in plane.chunks_exact(2) {
let s = i16::from_le_bytes([chunk[0], chunk[1]]);
if s != 0 {
if idx == 0 {
ch0_nonzero += 1;
} else {
other_nonzero += 1;
}
}
}
}
}
Ok(_) => unreachable!("MOD emits audio only"),
Err(Error::Eof) => break,
Err(e) => panic!("unexpected decode error: {e:?}"),
}
}
assert!(got_frame, "planar decoder produced no frames");
assert!(
ch0_nonzero > 100,
"expected channel-0 signal, got {ch0_nonzero} non-zero samples"
);
assert_eq!(
other_nonzero, 0,
"expected silence on channels 1..=3 (got {other_nonzero} non-zero samples)"
);
}
}