use std::io::Read;
use oxideav_container::{ContainerRegistry, Demuxer, ReadSeek};
use oxideav_core::{
CodecId, CodecParameters, Error, MediaType, Packet, Result, SampleFormat, StreamInfo, TimeBase,
};
use crate::header::parse_header;
pub const OUTPUT_SAMPLE_RATE: u32 = 44_100;
pub fn register(reg: &mut ContainerRegistry) {
reg.register_demuxer("mod", open);
reg.register_extension("mod", "mod");
reg.register_probe("mod", probe);
}
fn probe(p: &oxideav_container::ProbeData) -> u8 {
if p.buf.len() < 1084 {
if p.ext == Some("mod") {
return 25;
}
return 0;
}
let magic = &p.buf[1080..1084];
let known: &[&[u8; 4]] = &[
b"M.K.", b"M!K!", b"M&K!", b"FLT4", b"FLT8", b"4CHN", b"6CHN", b"8CHN", b"OCTA", b"CD81",
b"OKTA", b"16CN", b"32CN",
];
if known.iter().any(|m| (*m).as_slice() == magic) {
100
} else {
0
}
}
fn open(mut input: Box<dyn ReadSeek>) -> Result<Box<dyn Demuxer>> {
let mut blob = Vec::new();
input.read_to_end(&mut blob)?;
if blob.len() < crate::header::HEADER_FIXED_SIZE {
return Err(Error::invalid("MOD: file shorter than 1084-byte header"));
}
let header = parse_header(&blob)?;
let mut params = CodecParameters::audio(CodecId::new(crate::CODEC_ID_STR));
params.media_type = MediaType::Audio;
params.channels = Some(2); params.sample_rate = Some(OUTPUT_SAMPLE_RATE);
params.sample_format = Some(SampleFormat::S16);
params.extradata = blob.clone();
let stream = StreamInfo {
index: 0,
time_base: TimeBase::new(1, OUTPUT_SAMPLE_RATE as i64),
duration: None, start_time: Some(0),
params,
};
let metadata = build_metadata(&header);
let duration_micros: i64 = (header.song_length as i64).saturating_mul(64 * 6 * 1_000_000) / 50;
Ok(Box::new(ModDemuxer {
streams: vec![stream],
blob,
consumed: false,
metadata,
duration_micros,
_header: header,
}))
}
fn build_metadata(h: &crate::header::ModHeader) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = Vec::new();
if !h.title.is_empty() {
out.push(("title".into(), h.title.clone()));
}
for s in h.samples.iter() {
if !s.name.is_empty() {
out.push(("sample".into(), s.name.clone()));
}
}
let n_nonempty_samples = h.samples.iter().filter(|s| s.length > 0).count();
out.push((
"extra_info".into(),
format!(
"{} patterns, {} channels, {}/{} samples",
h.n_patterns,
h.channels,
n_nonempty_samples,
h.samples.len()
),
));
out
}
struct ModDemuxer {
streams: Vec<StreamInfo>,
blob: Vec<u8>,
consumed: bool,
metadata: Vec<(String, String)>,
duration_micros: i64,
_header: crate::header::ModHeader,
}
impl Demuxer for ModDemuxer {
fn format_name(&self) -> &str {
"mod"
}
fn streams(&self) -> &[StreamInfo] {
&self.streams
}
fn next_packet(&mut self) -> Result<Packet> {
if self.consumed {
return Err(Error::Eof);
}
self.consumed = true;
let data = std::mem::take(&mut self.blob);
let stream = &self.streams[0];
let mut pkt = Packet::new(0, stream.time_base, data);
pkt.pts = Some(0);
pkt.dts = Some(0);
pkt.flags.keyframe = true;
Ok(pkt)
}
fn metadata(&self) -> &[(String, String)] {
&self.metadata
}
fn duration_micros(&self) -> Option<i64> {
if self.duration_micros > 0 {
Some(self.duration_micros)
} else {
None
}
}
}