From 030de9896143e2343879dfe44c08d7806046ae6a Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Tue, 15 Apr 2025 18:09:38 +0200 Subject: [PATCH] IotaSound support (in TCA) --- nihav-acorn/Cargo.toml | 3 +- nihav-acorn/src/codecs/iotasound.rs | 92 +++++++++++++++++++++++++++++ nihav-acorn/src/codecs/mod.rs | 6 ++ nihav-acorn/src/demuxers/tca.rs | 89 +++++++++++++++++++++++----- nihav-registry/src/register.rs | 1 + 5 files changed, 176 insertions(+), 15 deletions(-) create mode 100644 nihav-acorn/src/codecs/iotasound.rs diff --git a/nihav-acorn/Cargo.toml b/nihav-acorn/Cargo.toml index 56ac79d..8ff3187 100644 --- a/nihav-acorn/Cargo.toml +++ b/nihav-acorn/Cargo.toml @@ -15,7 +15,7 @@ default = ["all_decoders", "all_demuxers", "all_packetisers"] all_decoders = ["all_video_decoders", "all_audio_decoders"] all_video_decoders = ["decoder_movinglines", "decoder_movingblocks", "decoder_movingblockshq", "decoder_supermovingblocks", "decoder_linepack", "decoder_escape", "decoder_rawvideo", "decoder_euclid"] -all_audio_decoders = ["decoder_rawaudio"] +all_audio_decoders = ["decoder_rawaudio", "decoder_iotasound"] decoders = [] decoder_movinglines = ["decoders"] @@ -26,6 +26,7 @@ decoder_linepack = ["decoders"] decoder_rawvideo = ["decoders"] decoder_escape = ["decoders"] decoder_euclid = ["decoders"] +decoder_iotasound = ["decoders"] decoder_rawaudio = ["decoders"] diff --git a/nihav-acorn/src/codecs/iotasound.rs b/nihav-acorn/src/codecs/iotasound.rs new file mode 100644 index 0000000..2efc141 --- /dev/null +++ b/nihav-acorn/src/codecs/iotasound.rs @@ -0,0 +1,92 @@ +use nihav_core::codecs::*; +use nihav_codec_support::codecs::imaadpcm::*; +use std::str::FromStr; + +struct IotaSoundDecoder { + ainfo: NAAudioInfo, + chmap: NAChannelMap, + ch_state: [IMAState; 2], +} + +impl IotaSoundDecoder { + fn new() -> Self { + Self { + ainfo: NAAudioInfo::new(8000, 1, SND_S16P_FORMAT, 0), + chmap: NAChannelMap::new(), + ch_state: [IMAState::new(), IMAState::new()], + } + } +} + +impl NADecoder for IotaSoundDecoder { + fn init(&mut self, _supp: &mut NADecoderSupport, info: NACodecInfoRef) -> DecoderResult<()> { + if let NACodecTypeInfo::Audio(ainfo) = info.get_properties() { + let channels = ainfo.get_channels(); + validate!(channels == 1 || channels == 2); + self.ainfo = NAAudioInfo::new(ainfo.get_sample_rate(), channels, SND_S16_FORMAT, 2); + self.chmap = NAChannelMap::from_str(if channels == 1 { "C" } else { "L,R" }).unwrap(); + Ok(()) + } else { + Err(DecoderError::InvalidData) + } + } + fn decode(&mut self, _supp: &mut NADecoderSupport, pkt: &NAPacket) -> DecoderResult { + let info = pkt.get_stream().get_info(); + if let NACodecTypeInfo::Audio(_) = info.get_properties() { + let pktbuf = pkt.get_buffer(); + let channels = self.ainfo.get_channels(); + let nsamples = pktbuf.len() * 2 / usize::from(channels); + let abuf = alloc_audio_buffer(self.ainfo, nsamples, self.chmap.clone())?; + let mut adata = abuf.get_abuf_i16().unwrap(); + let dst = adata.get_data_mut().unwrap(); + let idx2 = if channels == 1 { 0 } else { 1 }; + for (dpair, &src) in dst.chunks_exact_mut(2).zip(pktbuf.iter()) { + dpair[0] = self.ch_state[0].expand_sample(src >> 4); + dpair[1] = self.ch_state[idx2].expand_sample(src & 0xF); + } + + let mut frm = NAFrame::new_from_pkt(pkt, info, abuf); + frm.set_duration(Some(nsamples as u64)); + frm.set_keyframe(false); + Ok(frm.into_ref()) + } else { + Err(DecoderError::InvalidData) + } + } + fn flush(&mut self) { + self.ch_state[0].reset(0, 0); + self.ch_state[1].reset(0, 0); + } +} + +impl NAOptionHandler for IotaSoundDecoder { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} + +pub fn get_decoder() -> Box { + Box::new(IotaSoundDecoder::new()) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::{RegisteredDecoders, RegisteredPacketisers}; + use nihav_core::demuxers::RegisteredRawDemuxers; + use nihav_codec_support::test::dec_video::*; + use crate::*; + #[test] + fn test_iota_sound() { + let mut dmx_reg = RegisteredRawDemuxers::new(); + acorn_register_all_raw_demuxers(&mut dmx_reg); + let mut pkt_reg = RegisteredPacketisers::new(); + acorn_register_all_packetisers(&mut pkt_reg); + let mut dec_reg = RegisteredDecoders::new(); + acorn_register_all_decoders(&mut dec_reg); + + // sample from All About Planes + test_decoding_raw("armovie", "iota-sound", "assets/Acorn/wessex", None, + &dmx_reg, &pkt_reg, &dec_reg, + ExpectedTestResult::MD5([0x19509699, 0x0a2134c9, 0xef46040e, 0xa4ccd672])); + } +} diff --git a/nihav-acorn/src/codecs/mod.rs b/nihav-acorn/src/codecs/mod.rs index 4649531..1c77ecb 100644 --- a/nihav-acorn/src/codecs/mod.rs +++ b/nihav-acorn/src/codecs/mod.rs @@ -40,6 +40,8 @@ mod escape; #[cfg(feature="decoder_euclid")] mod euclid; +#[cfg(feature="decoder_iotasound")] +mod iotasound; #[cfg(feature="decoder_rawaudio")] mod rawaudio; @@ -70,6 +72,8 @@ const ACORN_CODECS: &[DecoderInfo] = &[ #[cfg(feature="decoder_euclid")] DecoderInfo { name: "euclid", get_decoder: euclid::get_decoder }, +#[cfg(feature="decoder_iotasound")] + DecoderInfo { name: "iota-sound", get_decoder: iotasound::get_decoder }, #[cfg(feature="decoder_rawaudio")] DecoderInfo { name: "arm_rawaudio", get_decoder: rawaudio::get_decoder }, @@ -114,6 +118,8 @@ const ACORN_PACKETISERS: &[PacketiserInfo] = &[ #[cfg(feature="decoder_euclid")] PacketiserInfo { name: "euclid", get_packetiser: euclid::get_packetiser }, +#[cfg(feature="decoder_iotasound")] + PacketiserInfo { name: "iota-sound", get_packetiser: rawaudio::get_packetiser }, #[cfg(feature="packetiser_cinepak")] PacketiserInfo { name: "cinepak", get_packetiser: wss_packetisers::get_packetiser_cinepak }, diff --git a/nihav-acorn/src/demuxers/tca.rs b/nihav-acorn/src/demuxers/tca.rs index 8f1890d..e35e718 100644 --- a/nihav-acorn/src/demuxers/tca.rs +++ b/nihav-acorn/src/demuxers/tca.rs @@ -48,6 +48,12 @@ impl DemuxerCreator for TCADemuxerCreator { pub(crate) struct TCACoreDemuxer { data_end: u64, frameno: u64, + video_pos: u64, + sound_pos: u64, + sound_end: u64, + asize: u64, + sblk_len: usize, + audio: bool, } impl TCACoreDemuxer { @@ -78,6 +84,9 @@ impl TCACoreDemuxer { validate!(width > 0 && height > 0); validate!((width | height) & 1 == 0); + let mut sound_start = 0; + let mut sound_end = 0; + if is_acef { let data_start = src.tell(); @@ -112,6 +121,21 @@ impl TCACoreDemuxer { src.read_skip(size - 8)?; } }, + b"SOUN" => { + sound_start = src.tell(); + let stag = src.read_tag()?; + validate!(&stag == b"WAV1" || &stag == b"WAV2"); + let ssize = src.read_u32le()?; + validate!(ssize as usize <= size - 8 && ssize > 0x1C); + sound_end = sound_start + u64::from(ssize); + src.read_u32le()?; // usually 1 + src.read_u32le()?; // usually 20 + src.read_u32le()?; // actual sound size + src.read_u32le()?; // some number, usually in 20000..30000 range + src.read_u32le()?; // usually -1 + sound_start = src.tell(); + src.read_skip(size - 0x1C)?; + }, _ => { src.read_skip(size - 8)?; }, @@ -133,24 +157,61 @@ impl TCACoreDemuxer { return Err(DemuxerError::MemoryError); } + if sound_end > 0 { + let aci = NACodecTypeInfo::Audio(NAAudioInfo::new(8000, 1, SND_S16_FORMAT, 1)); + let ainfo = NACodecInfo::new("iota-sound", aci, None); + let ret = strmgr.add_stream(NAStream::new(StreamType::Audio, 1, ainfo, 1, 8000, 0)); + if ret.is_none() { + return Err(DemuxerError::MemoryError); + } + self.sound_pos = sound_start; + self.sound_end = sound_end; + self.sblk_len = (8000 * u64::from(tb_num) / u64::from(tb_den)).max(1) as usize; + } + + self.audio = false; + self.video_pos = src.tell(); + Ok(()) } pub(crate) fn get_frame(&mut self, src: &mut ByteReader, strmgr: &mut StreamManager) -> DemuxerResult { - if src.tell() >= self.data_end { - return Err(DemuxerError::EOF); - } - let fsize = src.read_u32le()? as usize; - if fsize == 0 { - return Err(DemuxerError::EOF); - } - validate!((9..=1048576).contains(&fsize)); - if let Some(stream) = strmgr.get_stream(0) { - let ts = stream.make_ts(Some(self.frameno), None, None); - self.frameno += 1; - src.read_packet(stream, ts, false, fsize - 4) - } else { - Err(DemuxerError::InvalidData) + let has_video = self.video_pos + 4 < self.data_end; + let has_audio = self.audio && self.sound_pos < self.sound_end; + + match (has_video, has_audio) { + (true, false) => { + src.seek(SeekFrom::Start(self.video_pos))?; + let fsize = src.read_u32le()? as usize; + if fsize == 0 { + return Err(DemuxerError::EOF); + } + validate!((9..=1048576).contains(&fsize)); + if let Some(stream) = strmgr.get_stream(0) { + let ts = stream.make_ts(Some(self.frameno), None, None); + self.frameno += 1; + self.audio = true; + self.video_pos += fsize as u64; + src.read_packet(stream, ts, false, fsize - 4) + } else { + Err(DemuxerError::InvalidData) + } + }, + (_, true) => { + src.seek(SeekFrom::Start(self.sound_pos))?; + self.audio = false; + let cur_blk_len = self.sblk_len.min((self.sound_end - self.sound_pos) as usize); + if let Some(stream) = strmgr.get_stream(1) { + let ts = stream.make_ts(Some(self.asize * 2), None, None); + self.asize += cur_blk_len as u64; + self.audio = !has_video; + self.sound_pos += cur_blk_len as u64; + src.read_packet(stream, ts, false, cur_blk_len) + } else { + Err(DemuxerError::InvalidData) + } + }, + (false, false) => Err(DemuxerError::EOF), } } diff --git a/nihav-registry/src/register.rs b/nihav-registry/src/register.rs index eea2861..10b2266 100644 --- a/nihav-registry/src/register.rs +++ b/nihav-registry/src/register.rs @@ -223,6 +223,7 @@ static CODEC_REGISTER: &[CodecDescription] = &[ desc!(video; "escape130", "Eidos Escape 130"), desc!(audio; "escape-adpcm", "Eidos Escape ADPCM"), desc!(video-llp; "euclid", "Iota Euclid / The Complete Animation"), + desc!(audio; "iota-sound", "IotaSound"), desc!(video; "truemotion1", "TrueMotion 1"), desc!(video-im; "truemotionrt", "TrueMotion RT"), -- 2.39.5