From: Kostya Shishkov Date: Sat, 12 Apr 2025 07:38:58 +0000 (+0200) Subject: The Complete Animation format support X-Git-Url: https://git.nihav.org/?a=commitdiff_plain;h=9383afcb90e4d49061b85273c2c147245ae3ee37;p=nihav.git The Complete Animation format support --- diff --git a/nihav-acorn/Cargo.toml b/nihav-acorn/Cargo.toml index e430a58..56ac79d 100644 --- a/nihav-acorn/Cargo.toml +++ b/nihav-acorn/Cargo.toml @@ -14,7 +14,7 @@ path = "../nihav-codec-support" 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"] +all_video_decoders = ["decoder_movinglines", "decoder_movingblocks", "decoder_movingblockshq", "decoder_supermovingblocks", "decoder_linepack", "decoder_escape", "decoder_rawvideo", "decoder_euclid"] all_audio_decoders = ["decoder_rawaudio"] decoders = [] @@ -25,13 +25,15 @@ decoder_supermovingblocks = ["decoders"] decoder_linepack = ["decoders"] decoder_rawvideo = ["decoders"] decoder_escape = ["decoders"] +decoder_euclid = ["decoders"] decoder_rawaudio = ["decoders"] -all_demuxers = ["demuxer_armovie"] +all_demuxers = ["demuxer_armovie", "demuxer_tca"] demuxers = [] demuxer_armovie = ["demuxers"] +demuxer_tca = ["demuxers"] all_packetisers = ["packetiser_cinepak", "packetiser_msvideo1"] packetisers = [] diff --git a/nihav-acorn/src/codecs/euclid.rs b/nihav-acorn/src/codecs/euclid.rs new file mode 100644 index 0000000..3f7e13d --- /dev/null +++ b/nihav-acorn/src/codecs/euclid.rs @@ -0,0 +1,325 @@ +use nihav_core::codecs::*; +use nihav_core::io::bitreader::*; +use nihav_core::io::byteio::*; +use std::cmp::Ordering; + +const START_BITS: u8 = 9; +const MAX_BITS: u8 = 16; +const DICT_SIZE: usize = 1 << MAX_BITS; +const START_POS: usize = 257; +const INVALID_POS: usize = 65536; + +struct LZWState { + dict_sym: [u8; DICT_SIZE], + dict_prev: [u16; DICT_SIZE], + dict_pos: usize, + dict_lim: usize, + idx_bits: u8, +} + +impl LZWState { + fn new() -> Self { + Self { + dict_sym: [0; DICT_SIZE], + dict_prev: [0; DICT_SIZE], + dict_pos: START_POS, + dict_lim: 1 << START_BITS, + idx_bits: START_BITS, + } + } + fn reset(&mut self) { + self.dict_pos = START_POS; + self.dict_lim = 1 << START_BITS; + self.idx_bits = START_BITS; + } + fn add(&mut self, prev: usize, sym: u8) { + if self.dict_pos < self.dict_lim { + self.dict_sym [self.dict_pos] = sym; + self.dict_prev[self.dict_pos] = prev as u16; + self.dict_pos += 1; + } + } + fn decode_idx(&self, dst: &mut [u8], pos: usize, idx: usize) -> DecoderResult { + let mut tot_len = 1; + let mut tidx = idx; + while tidx > 256 { + tidx = self.dict_prev[tidx] as usize; + tot_len += 1; + } + validate!(pos + tot_len <= dst.len()); + + let mut end = pos + tot_len - 1; + let mut tidx = idx; + while tidx > 256 { + dst[end] = self.dict_sym[tidx]; + end -= 1; + tidx = self.dict_prev[tidx] as usize; + } + dst[end] = tidx as u8; + + Ok(tot_len) + } + fn decode(&mut self, br: &mut BitReader, dst: &mut [u8]) -> DecoderResult<()> { + self.reset(); + + let mut pos = 0; + let mut lastidx = INVALID_POS; + br.skip(9)?; + loop { + let idx = br.read(self.idx_bits)? as usize; + if idx == 256 { + break; + } + match idx.cmp(&self.dict_pos) { + Ordering::Less => { + let len = self.decode_idx(dst, pos, idx)?; + if lastidx != INVALID_POS { + self.add(lastidx, dst[pos]); + } + pos += len; + }, + Ordering::Equal => { + validate!(lastidx != INVALID_POS); + let len = self.decode_idx(dst, pos, lastidx)?; + let lastsym = dst[pos]; + pos += len; + validate!(pos < dst.len()); + dst[pos] = lastsym; + pos += 1; + self.add(lastidx, lastsym); + }, + Ordering::Greater => return Err(DecoderError::InvalidData), + } + lastidx = idx; + if self.dict_pos == self.dict_lim && self.idx_bits < MAX_BITS { + self.dict_lim <<= 1; + self.idx_bits += 1; + } + } + validate!(pos == dst.len()); + Ok(()) + } +} + +struct EuclidDecoder { + info: NACodecInfoRef, + width: usize, + height: usize, + lzw: LZWState, + pal: [u8; 768], + frame: Vec, + mode: u32, + update: bool, + buf: Vec, +} + +impl EuclidDecoder { + fn new() -> Self { + Self { + info: NACodecInfoRef::default(), + width: 0, + height: 0, + lzw: LZWState::new(), + pal: [0; 768], + frame: Vec::new(), + mode: 0, + update: false, + buf: Vec::new(), + } + } + fn output_frame(&self, dst: &mut [u8], stride: usize, paloff: usize) { + match self.mode { + 12 => { + for (dlines, sline) in dst.chunks_exact_mut(stride * 2) + .zip(self.frame.chunks_exact(self.width / 2)).take(self.height / 2) { + let (dline0, dline1) = dlines.split_at_mut(stride); + for (pair, &b) in dline0.chunks_exact_mut(2).zip(sline.iter()) { + pair[0] = b & 0xF; + pair[1] = b >> 4; + } + dline1.copy_from_slice(dline0); + } + }, + 15 | 40 => { + for (dlines, sline) in dst.chunks_exact_mut(stride * 2) + .zip(self.frame.chunks_exact(self.width)).take(self.height / 2) { + let (dline0, dline1) = dlines.split_at_mut(stride); + dline0[..sline.len()].copy_from_slice(sline); + dline1[..sline.len()].copy_from_slice(sline); + } + }, + 27 => { + for (dline, sline) in dst.chunks_exact_mut(stride) + .zip(self.frame.chunks_exact(self.width / 2)).take(self.height) { + for (pair, &b) in dline.chunks_exact_mut(2).zip(sline.iter()) { + pair[0] = b & 0xF; + pair[1] = b >> 4; + } + } + }, + _ => { + for (dline, sline) in dst.chunks_exact_mut(stride) + .zip(self.frame.chunks_exact(self.width)).take(self.height) { + dline[..sline.len()].copy_from_slice(sline); + } + }, + } + + dst[paloff..][..768].copy_from_slice(&self.pal); + } +} + +impl NADecoder for EuclidDecoder { + fn init(&mut self, _supp: &mut NADecoderSupport, info: NACodecInfoRef) -> DecoderResult<()> { + if let NACodecTypeInfo::Video(vinfo) = info.get_properties() { + self.width = vinfo.get_width(); + self.height = vinfo.get_height(); + if let Some(edata) = info.get_extradata() { + validate!(edata.len() >= 0x34); + let hdr_size = read_u32le(&edata)? as usize; + validate!(hdr_size >= 0x34 && hdr_size <= edata.len()); + self.mode = read_u32le(&edata[0x10..])?; + + self.update = (read_u32le(&edata[0x18..])? & 1) != 0; + + if edata.len() > hdr_size { + let src_pal = &edata[hdr_size..]; + let copy_size = src_pal.len().min(self.pal.len()); + self.pal[..copy_size].copy_from_slice(&src_pal[..copy_size]); + } else { + //self.pal[..DEFAULT_PAL.len()].copy_from_slice(&DEFAULT_PAL); + for (i, clr) in self.pal.chunks_exact_mut(3).enumerate() { + clr[0] = i as u8; + clr[1] = i as u8; + clr[2] = i as u8; + } + } + } else { + return Err(DecoderError::InvalidData); + } + let frm_size = match self.mode { + 12 => { + validate!((self.width & 1) == 0); + validate!((self.height & 1) == 0); + (self.width / 2) * (self.height / 2) + }, + 15 | 40 => { + validate!((self.height & 1) == 0); + self.width * (self.height / 2) + }, + 27 => { + validate!((self.width & 1) == 0); + (self.width / 2) * self.height + }, + 28 => { + self.width * self.height + }, + _ => return Err(DecoderError::NotImplemented), + }; + self.frame = vec![0; frm_size]; + self.buf = vec![0; frm_size]; + + let myinfo = NACodecTypeInfo::Video(NAVideoInfo::new(self.width, self.height, false, PAL8_FORMAT)); + self.info = NACodecInfo::new_ref(info.get_name(), myinfo, info.get_extradata()).into_ref(); + + Ok(()) + } else { + Err(DecoderError::InvalidData) + } + } + fn decode(&mut self, _supp: &mut NADecoderSupport, pkt: &NAPacket) -> DecoderResult { + let src = pkt.get_buffer(); + validate!(src.len() > 1); + + let mut br = BitReader::new(&src, BitReaderMode::LE); + + let bufinfo = alloc_video_buffer(self.info.get_properties().get_video_info().unwrap(), 0)?; + + let mut buf = bufinfo.get_vbuf().unwrap(); + let paloff = buf.get_offset(1); + let stride = buf.get_stride(0); + let data = buf.get_data_mut().unwrap(); + + self.lzw.decode(&mut br, if !self.update { &mut self.frame } else { &mut self.buf })?; + if self.update { + for (pix, &b) in self.frame.iter_mut().zip(self.buf.iter()) { + *pix ^= b; + } + } + + self.output_frame(data, stride, paloff); + + let mut frm = NAFrame::new_from_pkt(pkt, self.info.clone(), bufinfo); + frm.set_keyframe(self.update); + frm.set_frame_type(if !self.update { FrameType::I } else { FrameType::P }); + Ok(frm.into_ref()) + } + fn flush(&mut self) {} +} + +impl NAOptionHandler for EuclidDecoder { + 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(EuclidDecoder::new()) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::RegisteredDecoders; + use nihav_core::demuxers::RegisteredDemuxers; + use nihav_codec_support::test::dec_video::*; + use crate::*; + + #[test] + fn test_euclid() { + let mut dmx_reg = RegisteredDemuxers::new(); + acorn_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + acorn_register_all_decoders(&mut dec_reg); + + // a sample from RISC DISC 3 + test_decoding("tca", "euclid", "assets/Acorn/JUMPER", Some(1), + &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0x5ce22d08, 0x5f3cbd1b, 0x61a8b85e, 0x58197ba6], + [0x1fc3cc2e, 0xdc107b42, 0xada9f7b5, 0x0882f3d6]])); + } + #[test] + fn test_euclid_inter() { + let mut dmx_reg = RegisteredDemuxers::new(); + acorn_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + acorn_register_all_decoders(&mut dec_reg); + + // a sample from RISC DISC 3 + test_decoding("tca", "euclid", "assets/Acorn/WIGGLFACE", Some(1), + &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0xd1362b20, 0x986be0ec, 0x59aae46e, 0x11123e70], + [0xc07ab19c, 0xc518aca6, 0xc1f632b3, 0x9baa9cb5]])); + } +} + +#[allow(dead_code)] +const DEFAULT_PAL: [u8; 16 * 3] = [ + 0xFF, 0xFF, 0xFF, + 0xDD, 0xDD, 0xDD, + 0xBB, 0xBB, 0xBB, + 0x99, 0x99, 0x99, + 0x77, 0x77, 0x77, + 0x55, 0x55, 0x55, + 0x33, 0x33, 0x33, + 0x00, 0x00, 0x00, + 0x00, 0x44, 0x99, + 0xEE, 0xEE, 0x00, + 0x00, 0xCC, 0x00, + 0xDD, 0x00, 0x00, + 0xEE, 0xEE, 0xBB, + 0x55, 0x88, 0x00, + 0xFF, 0xBB, 0x00, + 0x00, 0xBB, 0xFF +]; diff --git a/nihav-acorn/src/codecs/mod.rs b/nihav-acorn/src/codecs/mod.rs index 48d8a3e..5503d1a 100644 --- a/nihav-acorn/src/codecs/mod.rs +++ b/nihav-acorn/src/codecs/mod.rs @@ -38,6 +38,9 @@ mod linepack; #[cfg(feature="decoder_escape")] mod escape; +#[cfg(feature="decoder_euclid")] +mod euclid; + #[cfg(feature="decoder_rawaudio")] mod rawaudio; @@ -65,6 +68,9 @@ const ACORN_CODECS: &[DecoderInfo] = &[ #[cfg(feature="decoder_escape")] DecoderInfo { name: "escape-adpcm", get_decoder: escape::get_decoder_audio }, +#[cfg(feature="decoder_euclid")] + DecoderInfo { name: "euclid", get_decoder: euclid::get_decoder }, + #[cfg(feature="decoder_rawaudio")] DecoderInfo { name: "arm_rawaudio", get_decoder: rawaudio::get_decoder }, ]; diff --git a/nihav-acorn/src/demuxers/mod.rs b/nihav-acorn/src/demuxers/mod.rs index 2acc820..69db369 100644 --- a/nihav-acorn/src/demuxers/mod.rs +++ b/nihav-acorn/src/demuxers/mod.rs @@ -11,6 +11,14 @@ macro_rules! validate { ($a:expr) => { if !$a { return Err(DemuxerError::InvalidData); } }; } +#[cfg(feature="demuxer_tca")] +mod tca; + +const DEMUXERS: &[&dyn DemuxerCreator] = &[ +#[cfg(feature="demuxer_tca")] + &tca::TCADemuxerCreator {}, +]; + #[cfg(feature="demuxer_armovie")] mod armovie; @@ -19,6 +27,13 @@ const RAW_DEMUXERS: &[&dyn RawDemuxerCreator] = &[ &armovie::ARMovieDemuxerCreator {}, ]; +/// Registers all available demuxers provided by this crate. +pub fn acorn_register_all_demuxers(rd: &mut RegisteredDemuxers) { + for demuxer in DEMUXERS.iter() { + rd.add_demuxer(*demuxer); + } +} + /// Registers all available demuxers provided by this crate. pub fn acorn_register_all_raw_demuxers(rd: &mut RegisteredRawDemuxers) { for demuxer in RAW_DEMUXERS.iter() { diff --git a/nihav-acorn/src/demuxers/tca.rs b/nihav-acorn/src/demuxers/tca.rs new file mode 100644 index 0000000..300b68a --- /dev/null +++ b/nihav-acorn/src/demuxers/tca.rs @@ -0,0 +1,147 @@ +use nihav_core::demuxers::*; + +struct TCADemuxer<'a> { + src: &'a mut ByteReader<'a>, + frameno: u64, + data_end: u64, +} + +impl<'a> TCADemuxer<'a> { + fn new(src: &'a mut ByteReader<'a>) -> Self { + Self { + src, + frameno: 0, + data_end: 0, + } + } +} + +impl<'a> DemuxCore<'a> for TCADemuxer<'a> { + fn open(&mut self, strmgr: &mut StreamManager, _seek_index: &mut SeekIndex) -> DemuxerResult<()> { + let tag = self.src.peek_tag()?; + let is_acef = &tag == b"ACEF"; + let acef_size = if is_acef { + self.src.read_skip(4)?; + self.src.read_u32le()? + } else { 0 }; + let size2 = self.src.read_u32le()?; + if is_acef { + validate!(acef_size > 0x44 && size2 + 8 <= acef_size); + } + self.data_end = u64::from(size2 + 8); + + self.src.read_skip(12)?; + + const HDR_SIZE: usize = 0x30; + + let mut hdr = vec![0; HDR_SIZE + 4]; + write_u32le(&mut hdr, HDR_SIZE as u32 + 4)?; + self.src.read_buf(&mut hdr[4..])?; + + let width = read_u32le(&hdr[8..])? as usize; + let height = read_u32le(&hdr[12..])? as usize; + validate!(width > 0 && height > 0); + validate!((width | height) & 1 == 0); + + if is_acef { + let data_start = self.src.tell(); + + // scan tail for palette and such + if self.src.seek(SeekFrom::Start(u64::from(acef_size))).is_ok() { + while let Ok(tag) = self.src.read_tag() { + let size = self.src.read_u32le()? as usize; + validate!(size >= 8); + if &tag == b"PALE" { + validate!((0x28..=0x428).contains(&size) && (size & 3) == 0); + self.src.read_skip(0x1C)?; + let nclrs = (size - 0x24) / 4; + hdr.resize(HDR_SIZE + 4 + 256 * 3, 0); + for _ in 0..nclrs { + let idx = usize::from(self.src.read_byte()?); + self.src.read_buf(&mut hdr[HDR_SIZE + 4 + idx * 3..][..3])?; + } + } else { + self.src.read_skip(size - 8)?; + } + } + } + self.src.seek(SeekFrom::Start(data_start))?; + } + + let vci = NACodecTypeInfo::Video(NAVideoInfo::new(width / 2, height / 2, false, PAL8_FORMAT)); + let vinfo = NACodecInfo::new("euclid", vci, Some(hdr)); + let ret = strmgr.add_stream(NAStream::new(StreamType::Video, 0, vinfo, 1, 10, 0)); + if ret.is_none() { + return Err(DemuxerError::MemoryError); + } + + Ok(()) + } + + fn get_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + if self.src.tell() >= self.data_end { + return Err(DemuxerError::EOF); + } + let fsize = self.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; + // last word is the current packet size for backwards seeking so it can be omitted + self.src.read_packet(stream, ts, false, fsize - 4) + } else { + Err(DemuxerError::InvalidData) + } + } + + fn seek(&mut self, _time: NATimePoint, _seek_index: &SeekIndex) -> DemuxerResult<()> { + Err(DemuxerError::NotPossible) + } + fn get_duration(&self) -> u64 { 0 } +} + +impl<'a> NAOptionHandler for TCADemuxer<'a> { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} + +pub struct TCADemuxerCreator { } + +impl DemuxerCreator for TCADemuxerCreator { + fn new_demuxer<'a>(&self, br: &'a mut ByteReader<'a>) -> Box + 'a> { + Box::new(TCADemuxer::new(br)) + } + fn get_name(&self) -> &'static str { "tca" } +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs::File; + + #[test] + fn test_tca_demux() { + // a sample from RISC DISC 3 + let mut file = File::open("assets/Acorn/JUMPER").unwrap(); + let mut fr = FileReader::new_read(&mut file); + let mut br = ByteReader::new(&mut fr); + let mut dmx = TCADemuxer::new(&mut br); + let mut sm = StreamManager::new(); + let mut si = SeekIndex::new(); + dmx.open(&mut sm, &mut si).unwrap(); + + loop { + match dmx.get_frame(&mut sm) { + Ok(pkt) => { + println!("Got {pkt}"); + }, + Err(DemuxerError::EOF) => return, + Err(_) => panic!("error"), + } + } + } +} diff --git a/nihav-acorn/src/lib.rs b/nihav-acorn/src/lib.rs index 0a99f67..9666f8f 100644 --- a/nihav-acorn/src/lib.rs +++ b/nihav-acorn/src/lib.rs @@ -8,4 +8,5 @@ mod demuxers; pub use crate::codecs::acorn_register_all_decoders; pub use crate::codecs::acorn_register_all_packetisers; +pub use crate::demuxers::acorn_register_all_demuxers; pub use crate::demuxers::acorn_register_all_raw_demuxers; \ No newline at end of file diff --git a/nihav-allstuff/src/lib.rs b/nihav-allstuff/src/lib.rs index 89a608f..08bca93 100644 --- a/nihav-allstuff/src/lib.rs +++ b/nihav-allstuff/src/lib.rs @@ -56,6 +56,7 @@ pub fn nihav_register_all_packetisers(rp: &mut RegisteredPacketisers) { /// Registers all known demuxers. pub fn nihav_register_all_demuxers(rd: &mut RegisteredDemuxers) { + acorn_register_all_demuxers(rd); duck_register_all_demuxers(rd); generic_register_all_demuxers(rd); flash_register_all_demuxers(rd); diff --git a/nihav-registry/src/detect.rs b/nihav-registry/src/detect.rs index fcfafef..be5b509 100644 --- a/nihav-registry/src/detect.rs +++ b/nihav-registry/src/detect.rs @@ -261,6 +261,12 @@ const DETECTORS: &[DetectConditions] = &[ extensions: ".rpl", conditions: &[CheckItem{offs: 0, cond: &CC::Str(b"ARMovie\n") }], }, + DetectConditions { + demux_name: "tca", + extensions: ".tca", + conditions: &[CheckItem{offs: 0x00, cond: &CC::Str(b"ACEF") }, + CheckItem{offs: 0x18, cond: &CC::Eq(Arg::U32LE(64))}], + }, DetectConditions { demux_name: "flv", extensions: ".flv", diff --git a/nihav-registry/src/register.rs b/nihav-registry/src/register.rs index 383ecd9..eea2861 100644 --- a/nihav-registry/src/register.rs +++ b/nihav-registry/src/register.rs @@ -222,6 +222,7 @@ static CODEC_REGISTER: &[CodecDescription] = &[ desc!(video; "escape124", "Eidos Escape 124"), desc!(video; "escape130", "Eidos Escape 130"), desc!(audio; "escape-adpcm", "Eidos Escape ADPCM"), + desc!(video-llp; "euclid", "Iota Euclid / The Complete Animation"), desc!(video; "truemotion1", "TrueMotion 1"), desc!(video-im; "truemotionrt", "TrueMotion RT"),