From 561d0f7901f186b779e6fdaf26640355319324ba Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Sat, 29 Oct 2022 11:17:29 +0200 Subject: [PATCH] Beam Software SIFF format support --- nihav-game/Cargo.toml | 7 +- nihav-game/src/codecs/beam.rs | 611 ++++++++++++++++++++++++++++++++ nihav-game/src/codecs/mod.rs | 6 + nihav-game/src/demuxers/mod.rs | 4 + nihav-game/src/demuxers/siff.rs | 366 +++++++++++++++++++ nihav-registry/src/detect.rs | 10 + nihav-registry/src/register.rs | 2 + 7 files changed, 1004 insertions(+), 2 deletions(-) create mode 100644 nihav-game/src/codecs/beam.rs create mode 100644 nihav-game/src/demuxers/siff.rs diff --git a/nihav-game/Cargo.toml b/nihav-game/Cargo.toml index eb0f1fd..4e6ca31 100644 --- a/nihav-game/Cargo.toml +++ b/nihav-game/Cargo.toml @@ -18,7 +18,7 @@ nihav_commonfmt = { path = "../nihav-commonfmt", default-features=false, feature [features] default = ["all_decoders", "all_demuxers"] demuxers = [] -all_demuxers = ["demuxer_bmv", "demuxer_bmv3", "demuxer_fcmp", "demuxer_fst", "demuxer_gdv", "demuxer_hl_fmv", "demuxer_imax", "demuxer_q", "demuxer_smush", "demuxer_vmd", "demuxer_vx"] +all_demuxers = ["demuxer_bmv", "demuxer_bmv3", "demuxer_fcmp", "demuxer_fst", "demuxer_gdv", "demuxer_hl_fmv", "demuxer_imax", "demuxer_q", "demuxer_siff", "demuxer_smush", "demuxer_vmd", "demuxer_vx"] demuxer_bmv = ["demuxers"] demuxer_bmv3 = ["demuxers"] demuxer_fcmp = ["demuxers"] @@ -27,6 +27,7 @@ demuxer_gdv = ["demuxers"] demuxer_hl_fmv = ["demuxers"] demuxer_imax = ["demuxers"] demuxer_q = ["demuxers"] +demuxer_siff = ["demuxers"] demuxer_smush = ["demuxers"] demuxer_vmd = ["demuxers"] demuxer_vx = ["demuxers"] @@ -34,7 +35,9 @@ demuxer_vx = ["demuxers"] all_decoders = ["all_video_decoders", "all_audio_decoders"] decoders = [] -all_video_decoders = ["decoder_bmv", "decoder_bmv3", "decoder_fstvid", "decoder_gdvvid", "decoder_hl_fmv", "decoder_imax", "decoder_ipma", "decoder_midivid", "decoder_midivid3", "decoder_q", "decoder_smush_video", "decoder_vmd", "decoder_vx"] +all_video_decoders = ["decoder_beam_fcp", "decoder_beam_vbv", "decoder_bmv", "decoder_bmv3", "decoder_fstvid", "decoder_gdvvid", "decoder_hl_fmv", "decoder_imax", "decoder_ipma", "decoder_midivid", "decoder_midivid3", "decoder_q", "decoder_smush_video", "decoder_vmd", "decoder_vx"] +decoder_beam_fcp = ["decoders"] +decoder_beam_vbv = ["decoders"] decoder_bmv = ["decoders"] decoder_bmv3 = ["decoders"] decoder_fstvid = ["decoders"] diff --git a/nihav-game/src/codecs/beam.rs b/nihav-game/src/codecs/beam.rs new file mode 100644 index 0000000..e7d778f --- /dev/null +++ b/nihav-game/src/codecs/beam.rs @@ -0,0 +1,611 @@ +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; + +const VB_FLAG_GMC: u16 = 0x0001; +const VB_FLAG_AUDIO: u16 = 0x0004; +const VB_FLAG_VIDEO: u16 = 0x0008; +const VB_FLAG_PALETTE: u16 = 0x0010; + +const RGB555_FORMAT: NAPixelFormaton = NAPixelFormaton { model: ColorModel::RGB(RGBSubmodel::RGB), components: 3, + comp_info: [ + Some(NAPixelChromaton{ h_ss: 0, v_ss: 0, packed: true, depth: 5, shift: 10, comp_offs: 0, next_elem: 2 }), + Some(NAPixelChromaton{ h_ss: 0, v_ss: 0, packed: true, depth: 5, shift: 5, comp_offs: 1, next_elem: 2 }), + Some(NAPixelChromaton{ h_ss: 0, v_ss: 0, packed: true, depth: 5, shift: 0, comp_offs: 2, next_elem: 2 }), + None, None], + elem_size: 2, be: false, alpha: false, palette: false }; + +fn check_size(x: usize, row_no: usize, dx: i16, dy: i16, stride: usize, len: usize) -> Option { + let start = (x as i32) + i32::from(dx) + ((row_no * 4) as i32 + i32::from(dy)) * (stride as i32); + if start >= 0 { + let start = start as usize; + let end = start + 4 + stride * 3; + + if end <= len { + Some(start) + } else { + None + } + } else { + None + } +} + +fn decode_video8(br: &mut ByteReader, dst: &mut [u8], prev_frame: &[u8], width: usize, gmv: [i16; 2]) -> DecoderResult { + let mut ftype = FrameType::I; + + let mut btc = 0; + let mut btypes = 0; + for (row_no, strip) in dst.chunks_mut(width * 4).enumerate() { + for x in (0..width).step_by(4) { + if btc == 0 { + btypes = br.read_byte()?; + btc = 4; + } + match btypes & 0xC0 { + 0xC0 => { + let t = br.read_byte()?; + let mut pattern = VB_PATTERNS[(t & 0x3F) as usize]; + let op = t >> 6; + validate!(op != 3); + if op == 0 { + let mut clr = [0; 2]; + br.read_buf(&mut clr)?; + for dline in strip[x..].chunks_mut(width).take(4) { + for el in dline[..4].iter_mut() { + *el = clr[(pattern & 1) as usize]; + pattern >>= 1; + } + } + } else { + if op == 2 { + pattern = !pattern; + } + let clr = br.read_byte()?; + + if let Some(start) = check_size(x, row_no, gmv[0], gmv[1], width, prev_frame.len()) { + for (dline, sline) in strip[x..].chunks_mut(width).zip(prev_frame[start..].chunks(width)).take(4) { + for (dst, &src) in dline[..4].iter_mut().zip(sline.iter()) { + *dst = if (pattern & 1) != 0 { clr } else { src }; + pattern >>= 1; + } + } + } else { + return Err(DecoderError::InvalidData); + } + + ftype = FrameType::P; + } + }, + 0x80 => { + let clr = br.read_byte()?; + for dline in strip[x..].chunks_mut(width).take(4) { + for el in dline[..4].iter_mut() { + *el = clr; + } + } + }, + 0x40 => { + let mv = br.read_byte()?; + if mv == 0 { + for dline in strip[x..].chunks_mut(width).take(4) { + br.read_buf(&mut dline[..4])?; + } + } else { + let mvx = (((mv & 0xF) ^ 8) as i16) - 8; + let mvy = (((mv >> 4) ^ 8) as i16) - 8; + if let Some(start) = check_size(x, row_no, gmv[0] + mvx, gmv[1] + mvy, width, prev_frame.len()) { + for (dline, sline) in strip[x..].chunks_mut(width).zip(prev_frame[start..].chunks(width)).take(4) { + dline[..4].copy_from_slice(&sline[..4]); + } + } else { + return Err(DecoderError::InvalidData); + } + ftype = FrameType::P; + } + }, + _ => { + if let Some(start) = check_size(x, row_no, gmv[0], gmv[1], width, prev_frame.len()) { + for (dline, sline) in strip[x..].chunks_mut(width).zip(prev_frame[start..].chunks(width)).take(4) { + dline[..4].copy_from_slice(&sline[..4]); + } + } else { + return Err(DecoderError::InvalidData); + } + ftype = FrameType::P; + }, + } + + btypes <<= 2; + btc -= 1; + } + } + + Ok(ftype) +} + +struct OldData { + pal: [u8; 768], + prev_frame: Vec, + cur_frame: Vec, + width: usize, + flags: u16, +} + +impl OldData { + fn new(w: usize, h: usize, flags: u16) -> Self { + Self { + pal: [0; 768], + prev_frame: vec![0; w * h], + cur_frame: vec![0; w * h], + width: w, + flags, + } + } + fn decode(&mut self, br: &mut ByteReader, vbuf: &mut NAVideoBufferRef) -> DecoderResult { + let asize = br.read_u16le()? as usize; + let _smth = br.read_u16le()? as usize; + let pal_size = br.read_u16le()? as usize; + if pal_size > 0 { + let pal_start = br.read_u16le()? as usize; + validate!(pal_start + pal_size <= 256); + br.read_buf(&mut self.pal[pal_start * 3..][..pal_size * 3])?; + } + br.read_skip(asize)?; + + let (gmv_x, gmv_y) = if (self.flags & 0x2000) != 0 { + let mvx = br.read_u16le()? as i16; + let mvy = br.read_u16le()? as i16; + (mvx, mvy) + } else { + (0, 0) + }; + + std::mem::swap(&mut self.cur_frame, &mut self.prev_frame); + + let ftype = decode_video8(br, &mut self.cur_frame, &self.prev_frame, self.width, [gmv_x, gmv_y])?; + + let stride = vbuf.get_stride(0); + let pal_off = vbuf.get_offset(1); + let data = vbuf.get_data_mut().unwrap(); + + for (dline, sline) in data.chunks_mut(stride).zip(self.cur_frame.chunks(self.width)) { + dline[..self.width].copy_from_slice(sline); + } + data[pal_off..][..768].copy_from_slice(&self.pal); + + Ok(ftype) + } +} + +struct Data8 { + pal: [u8; 768], + prev_frame: Vec, + cur_frame: Vec, + width: usize, +} + +impl Data8 { + fn new(w: usize, h: usize) -> Self { + Self { + pal: [0; 768], + prev_frame: vec![0; w * h], + cur_frame: vec![0; w * h], + width: w, + } + } + fn decode(&mut self, br: &mut ByteReader, vbuf: &mut NAVideoBufferRef) -> DecoderResult { + let flags = br.read_u16le()?; + let (gmv_x, gmv_y) = if (flags & VB_FLAG_GMC) != 0 { + let mvx = br.read_u16le()? as i16; + let mvy = br.read_u16le()? as i16; + (mvx, mvy) + } else { + (0, 0) + }; + if (flags & VB_FLAG_AUDIO) != 0 { + let asize = br.read_u32le()? as usize; + validate!(asize >= 4 && asize <= (br.left() as usize)); + br.read_skip(asize - 4)?; + } + let mut ftype = FrameType::Skip; + if (flags & VB_FLAG_VIDEO) != 0 { + std::mem::swap(&mut self.cur_frame, &mut self.prev_frame); + + let vsize = br.read_u32le()? as u64; + validate!(vsize > 4); + let end = br.tell() + vsize - 4; + + ftype = decode_video8(br, &mut self.cur_frame, &self.prev_frame, self.width, [gmv_x, gmv_y])?; + + validate!(br.tell() == end); + } + if (flags & VB_FLAG_PALETTE) != 0 { + let pal_size = br.read_u32le()? as usize; + validate!(pal_size > 6); + validate!(br.left() as usize >= pal_size - 4); + let start = br.read_byte()? as usize; + let mut size = br.read_byte()? as usize; + if size == 0 { + size = 256; + } + validate!(start + size <= 256); + validate!(size * 3 + 6 == pal_size); + br.read_buf(&mut self.pal[start * 3..][..size * 3])?; + } + + let stride = vbuf.get_stride(0); + let pal_off = vbuf.get_offset(1); + let data = vbuf.get_data_mut().unwrap(); + + for (dline, sline) in data.chunks_mut(stride).zip(self.cur_frame.chunks(self.width)) { + dline[..self.width].copy_from_slice(sline); + } + data[pal_off..][..768].copy_from_slice(&self.pal); + + Ok(ftype) + } +} + +struct Data16 { + prev_frame: Vec, + cur_frame: Vec, + pal: [u16; 256], + width: usize, +} + +impl Data16 { + fn new(w: usize, h: usize) -> Self { + Self { + prev_frame: vec![0; w * h], + cur_frame: vec![0; w * h], + pal: [0; 256], + width: w, + } + } + fn decode(&mut self, br: &mut ByteReader, vbuf: &mut NAVideoBufferRef) -> DecoderResult { + let flags = br.read_u16le()?; + let (gmv_x, gmv_y) = if (flags & VB_FLAG_GMC) != 0 { + let mvx = br.read_u16le()? as i16; + let mvy = br.read_u16le()? as i16; + (mvx, mvy) + } else { + (0, 0) + }; + if (flags & VB_FLAG_AUDIO) != 0 { + let asize = br.read_u32le()? as usize; + validate!(asize <= (br.left() as usize)); + br.read_skip(asize - 4)?; + } + let mut ftype = FrameType::Skip; + if (flags & VB_FLAG_VIDEO) != 0 { + let vsize = br.read_u32le()? as u64; + validate!(vsize > 4); + let end = br.tell() + vsize - 4; + let has_pal = (flags & VB_FLAG_PALETTE) != 0; + if has_pal { + let cur_off = br.tell(); + br.seek(SeekFrom::Current((vsize - 4) as i64))?; + let psize = br.read_u32le()? as usize; + validate!(psize > 4 && psize <= 0x204 && (psize & 1) == 0); + for el in self.pal[..(psize - 4)/ 2].iter_mut() { + *el = br.read_u16le()?; + } + br.seek(SeekFrom::Start(cur_off))?; + } + + std::mem::swap(&mut self.cur_frame, &mut self.prev_frame); + + let mut btc = 0; + let mut btypes = 0; + ftype = FrameType::I; + for (row_no, strip) in self.cur_frame.chunks_mut(self.width * 4).enumerate() { + for x in (0..self.width).step_by(4) { + if btc == 0 { + btypes = br.read_byte()?; + btc = 4; + } + match btypes & 0xC0 { + 0xC0 => { + let t = br.read_byte()?; + let mut pattern = VB_PATTERNS[(t & 0x3F) as usize]; + let op = t >> 6; + validate!(op != 3); + if op == 0 { + let mut clr = [0; 2]; + if has_pal { + clr[0] = self.pal[br.read_byte()? as usize]; + clr[1] = self.pal[br.read_byte()? as usize]; + } else { + clr[0] = br.read_u16le()?; + clr[1] = br.read_u16le()?; + } + for dline in strip[x..].chunks_mut(self.width).take(4) { + for el in dline[..4].iter_mut() { + *el = clr[(pattern & 1) as usize]; + pattern >>= 1; + } + } + } else { + if op == 2 { + pattern = !pattern; + } + let clr = if has_pal { + self.pal[br.read_byte()? as usize] + } else { + br.read_u16le()? + }; + + if let Some(start) = check_size(x, row_no, gmv_x, gmv_y, self.width, self.prev_frame.len()) { + for (dline, sline) in strip[x..].chunks_mut(self.width).zip(self.prev_frame[start..].chunks(self.width)).take(4) { + for (dst, &src) in dline[..4].iter_mut().zip(sline.iter()) { + *dst = if (pattern & 1) != 0 { clr } else { src }; + pattern >>= 1; + } + } + } else { + return Err(DecoderError::InvalidData); + } + ftype = FrameType::P; + } + }, + 0x80 => { + let clr = if has_pal { + self.pal[br.read_byte()? as usize] + } else { + br.read_u16le()? + }; + for dline in strip[x..].chunks_mut(self.width).take(4) { + for el in dline[..4].iter_mut() { + *el = clr; + } + } + }, + 0x40 => { + let mv = br.read_byte()?; + if mv == 0 { + if has_pal { + for dline in strip[x..].chunks_mut(self.width).take(4) { + for el in dline[..4].iter_mut() { + *el = self.pal[br.read_byte()? as usize]; + } + } + } else { + for dline in strip[x..].chunks_mut(self.width).take(4) { + for el in dline[..4].iter_mut() { + *el = br.read_u16le()?; + } + } + } + } else { + let mvx = (((mv & 0xF) ^ 8) as i16) - 8; + let mvy = (((mv >> 4) ^ 8) as i16) - 8; + if let Some(start) = check_size(x, row_no, gmv_x + mvx, gmv_y + mvy, self.width, self.prev_frame.len()) { + for (dline, sline) in strip[x..].chunks_mut(self.width).zip(self.prev_frame[start..].chunks(self.width)).take(4) { + dline[..4].copy_from_slice(&sline[..4]); + } + } else { + return Err(DecoderError::InvalidData); + } + ftype = FrameType::P; + } + }, + _ => { + if let Some(start) = check_size(x, row_no, gmv_x, gmv_y, self.width, self.prev_frame.len()) { + for (dline, sline) in strip[x..].chunks_mut(self.width).zip(self.prev_frame[start..].chunks(self.width)).take(4) { + dline[..4].copy_from_slice(&sline[..4]); + } + } else { + return Err(DecoderError::InvalidData); + } + ftype = FrameType::P; + }, + } + + btypes <<= 2; + btc -= 1; + } + } + validate!(br.tell() == end); + } + if (flags & VB_FLAG_PALETTE) != 0 { + let psize = br.read_u32le()? as usize; + br.read_skip(psize - 4)?; + } + + let stride = vbuf.get_stride(0); + let data = vbuf.get_data_mut().unwrap(); + + for (dline, sline) in data.chunks_mut(stride).zip(self.cur_frame.chunks(self.width)) { + dline[..self.width].copy_from_slice(sline); + } + + Ok(ftype) + } +} + +enum FrameData { + Old(OldData), + Pal(Data8), + New(Data16), + None, +} + +struct BeamVideoDecoder { + info: NACodecInfoRef, + data: FrameData, + fcp: bool, +} + +impl BeamVideoDecoder { + fn new(fcp: bool) -> Self { + Self { + info: NACodecInfoRef::default(), + data: FrameData::None, + fcp, + } + } +} + +impl NADecoder for BeamVideoDecoder { + fn init(&mut self, _supp: &mut NADecoderSupport, info: NACodecInfoRef) -> DecoderResult<()> { + if let NACodecTypeInfo::Video(vinfo) = info.get_properties() { + let width = vinfo.get_width(); + let height = vinfo.get_height(); + validate!((width & 3) == 0 && (height & 3) == 0); + let fmt = match vinfo.bits { + _ if self.fcp => { + let edata = info.get_extradata(); + validate!(edata.is_some()); + let edata = edata.unwrap(); + validate!(edata.len() >= 2); + let flags = read_u16le(&edata)?; + self.data = FrameData::Old(OldData::new(width, height, flags)); + PAL8_FORMAT + }, + 8 => { + self.data = FrameData::Pal(Data8::new(width, height)); + PAL8_FORMAT + }, + 16 => { + self.data = FrameData::New(Data16::new(width, height)); + RGB555_FORMAT + }, + _ => return Err(DecoderError::InvalidData), + }; + + let myinfo = NACodecTypeInfo::Video(NAVideoInfo::new(vinfo.get_width(), vinfo.get_height(), false, fmt)); + 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 mr = MemoryReader::new_read(&src); + let mut br = ByteReader::new(&mut mr); + + let mut bufinfo = alloc_video_buffer(self.info.get_properties().get_video_info().unwrap(), 0)?; + + let ftype = match (&mut self.data, &mut bufinfo) { + (FrameData::Old(ref mut data), NABufferType::Video(ref mut vbuf)) => data.decode(&mut br, vbuf)?, + (FrameData::Pal(ref mut data), NABufferType::Video(ref mut vbuf)) => data.decode(&mut br, vbuf)?, + (FrameData::New(ref mut data), NABufferType::Video16(ref mut vbuf)) => data.decode(&mut br, vbuf)?, + _ => unreachable!(), + }; + + let mut frm = NAFrame::new_from_pkt(pkt, self.info.clone(), bufinfo); + frm.set_keyframe(ftype == FrameType::I); + frm.set_frame_type(ftype); + Ok(frm.into_ref()) + } + fn flush(&mut self) { + } +} + +impl NAOptionHandler for BeamVideoDecoder { + 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_vbv() -> Box { + Box::new(BeamVideoDecoder::new(false)) +} + +pub fn get_decoder_fcp() -> Box { + Box::new(BeamVideoDecoder::new(true)) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::RegisteredDecoders; + use nihav_core::demuxers::RegisteredDemuxers; + use nihav_codec_support::test::dec_video::*; + use crate::game_register_all_decoders; + use crate::game_register_all_demuxers; + #[test] + fn test_beam_fcp() { + let mut dmx_reg = RegisteredDemuxers::new(); + game_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + game_register_all_decoders(&mut dec_reg); + + // sample from The Dame was Loaded + test_decoding("siff", "beam-fcp", "assets/Game/siff/BEAM.FCP", Some(1000), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0xa31342c0, 0x7afe5a76, 0x4111f17a, 0x30ec3a18], + [0xa31342c0, 0x7afe5a76, 0x4111f17a, 0x30ec3a18], + [0xa31342c0, 0x7afe5a76, 0x4111f17a, 0x30ec3a18], + [0xa31342c0, 0x7afe5a76, 0x4111f17a, 0x30ec3a18], + [0xa31342c0, 0x7afe5a76, 0x4111f17a, 0x30ec3a18], + [0x447870bf, 0x9bb22dab, 0x1f557dae, 0xc41575ad], + [0xeb3bd749, 0xc72811d3, 0x23d430b2, 0xc31dbd85], + [0x34bb16b4, 0x8e6066fe, 0xf3f6170a, 0xae4ec54e], + [0xee12f3ff, 0x4000ed43, 0x446336d7, 0x6cc344bf], + [0xfe6ba9e1, 0xcc97f503, 0xf6fc8f14, 0x40c334c0], + [0xb4229527, 0x8591ac0b, 0x1ba40ea8, 0x15aa5710]])); + } + #[test] + fn test_beam_video_8bit() { + let mut dmx_reg = RegisteredDemuxers::new(); + game_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + game_register_all_decoders(&mut dec_reg); + + // sample from Lost Vikings 2 + test_decoding("siff", "beam-video", "assets/Game/siff/BEAM.VB", Some(1000), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0x23604e03, 0x5781ea8a, 0x6b28d338, 0xc32d52d6], + [0xba68d1af, 0xacf630b7, 0x3899073d, 0x07135315], + [0xb10cd159, 0xd787a566, 0x523123ca, 0x8a757f9e], + [0x7f4dfb53, 0x581884ab, 0x71de61c0, 0xfec72a11], + [0xc2a40282, 0x729548e1, 0xc63211bf, 0x586158fc], + [0xb8bf753c, 0x53686fb2, 0x7d307625, 0xdab70698], + [0xa2e90475, 0x076ef58a, 0xe2276941, 0x9c47ca14], + [0x80f785df, 0x19e015ab, 0x93beaf9b, 0xff1d0907], + [0x957a9a60, 0xc8a8ae09, 0xad5ecf6c, 0x06097f03], + [0xdfd331d4, 0x2f6ba92b, 0x5bfcb6f8, 0x85bab2b9], + [0xf37d42c1, 0x7f54d6bd, 0xc2dca688, 0x60d64e8c], + [0x97a0e18b, 0xce9ea101, 0x56eea57c, 0xeb48e8b3], + [0x7043048c, 0x373fdf9d, 0x1b27d5b3, 0x3e4e9c7a]])); + } + #[test] + fn test_beam_video_16bit() { + let mut dmx_reg = RegisteredDemuxers::new(); + game_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + game_register_all_decoders(&mut dec_reg); + + // sample from Alien Earth + test_decoding("siff", "beam-video", "assets/Game/siff/beamlogo.vbc", Some(500), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0x90aeded9, 0x922cf894, 0x6ea78586, 0x35493724], + [0x90aeded9, 0x922cf894, 0x6ea78586, 0x35493724], + [0xa19daf5e, 0xa4858d72, 0x6700fc4d, 0x95c5e5dd], + [0x57d556af, 0x95c5c5f4, 0xe943f1e3, 0xb86941e1], + [0xa63351ae, 0x12ed21ee, 0x7246d1f4, 0xf3a5a79a], + [0x5930e388, 0x4891d37b, 0x619ea7ff, 0xc8dac16d], + [0x36eeaf75, 0x767e5e5e, 0x397e0100, 0xfa306811], + [0xb4722a6d, 0x61cc8483, 0xe4966f11, 0xa67cd0f4], + [0x62be7367, 0xcbeb77b2, 0xcb438480, 0xda39b47a], + [0x568b020d, 0x4992bc7f, 0xcbe20b5c, 0x697a35bc], + [0x35e5a6a8, 0x903c0d07, 0xbacf7734, 0xd1c54b6d], + [0xd3450588, 0xfae0a9ec, 0x72a8fce6, 0x7c57e3c1], + [0x90f0cc47, 0xd7e7d3ef, 0x46d81b4d, 0xf4495aa2]])); + } +} + +const VB_PATTERNS: [u16; 64] = [ + 0x0660, 0xFF00, 0xCCCC, 0xF000, 0x8888, 0x000F, 0x1111, 0xFEC8, + 0x8CEF, 0x137F, 0xF731, 0xC800, 0x008C, 0x0013, 0x3100, 0xCC00, + 0x00CC, 0x0033, 0x3300, 0x0FF0, 0x6666, 0x00F0, 0x0F00, 0x2222, + 0x4444, 0xF600, 0x8CC8, 0x006F, 0x1331, 0x318C, 0xC813, 0x33CC, + 0x6600, 0x0CC0, 0x0066, 0x0330, 0xF900, 0xC88C, 0x009F, 0x3113, + 0x6000, 0x0880, 0x0006, 0x0110, 0xCC88, 0xFC00, 0x00CF, 0x88CC, + 0x003F, 0x1133, 0x3311, 0xF300, 0x6FF6, 0x0603, 0x08C6, 0x8C63, + 0xC631, 0x6310, 0xC060, 0x0136, 0x136C, 0x36C8, 0x6C80, 0x324C +]; diff --git a/nihav-game/src/codecs/mod.rs b/nihav-game/src/codecs/mod.rs index ff87adf..4d6710e 100644 --- a/nihav-game/src/codecs/mod.rs +++ b/nihav-game/src/codecs/mod.rs @@ -5,6 +5,8 @@ macro_rules! validate { ($a:expr) => { if !$a { println!("check failed at {}:{}", file!(), line!()); return Err(DecoderError::InvalidData); } }; } +#[cfg(any(feature="decoder_beam_vbv",feature="decoder_beam_fcp"))] +pub mod beam; #[cfg(feature="decoder_bmv")] pub mod bmv; #[cfg(feature="decoder_bmv3")] @@ -41,6 +43,10 @@ const GAME_CODECS: &[DecoderInfo] = &[ DecoderInfo { name: "gdv-audio", get_decoder: gremlinvideo::get_decoder_audio }, #[cfg(feature="decoder_gdvvid")] DecoderInfo { name: "gdv-video", get_decoder: gremlinvideo::get_decoder_video }, +#[cfg(feature="decoder_beam_fcp")] + DecoderInfo { name: "beam-fcp", get_decoder: beam::get_decoder_fcp }, +#[cfg(feature="decoder_beam_vbv")] + DecoderInfo { name: "beam-video", get_decoder: beam::get_decoder_vbv }, #[cfg(feature="decoder_bmv")] DecoderInfo { name: "bmv-audio", get_decoder: bmv::get_decoder_audio }, #[cfg(feature="decoder_bmv")] diff --git a/nihav-game/src/demuxers/mod.rs b/nihav-game/src/demuxers/mod.rs index ca85790..b332078 100644 --- a/nihav-game/src/demuxers/mod.rs +++ b/nihav-game/src/demuxers/mod.rs @@ -17,6 +17,8 @@ mod hl_fmv; mod imax; #[cfg(feature="demuxer_q")] mod q; +#[cfg(feature="demuxer_siff")] +mod siff; #[cfg(feature="demuxer_smush")] mod smush; #[cfg(feature="demuxer_vmd")] @@ -41,6 +43,8 @@ const GAME_DEMUXERS: &[&dyn DemuxerCreator] = &[ &imax::IMAXDemuxerCreator {}, #[cfg(feature="demuxer_q")] &q::QDemuxerCreator {}, +#[cfg(feature="demuxer_siff")] + &siff::SIFFDemuxerCreator {}, #[cfg(feature="demuxer_smush")] &smush::SmushDemuxerCreator {}, #[cfg(feature="demuxer_smush")] diff --git a/nihav-game/src/demuxers/siff.rs b/nihav-game/src/demuxers/siff.rs new file mode 100644 index 0000000..33c26f8 --- /dev/null +++ b/nihav-game/src/demuxers/siff.rs @@ -0,0 +1,366 @@ +use nihav_core::frame::*; +use nihav_core::demuxers::*; + +const DEFAULT_FCP_DELAY: u64 = 100; +const DEFAULT_VBV_DELAY: u64 = 80; + +#[derive(Clone,Copy,Debug,PartialEq)] +enum SIFFType { + None, + FCP, + VBV, + Sound, +} + +struct SIFFDemuxer<'a> { + src: &'a mut ByteReader<'a>, + subtype: SIFFType, + size: u32, + ablock: usize, + nframes: usize, + cframe: usize, + vpts: u64, + abuf: Vec, + apts: u64, + ver: u8, +} + +impl<'a> SIFFDemuxer<'a> { + fn new(io: &'a mut ByteReader<'a>) -> Self { + Self { + src: io, + subtype: SIFFType::None, + size: 0, + ablock: 0, + nframes: 0, + cframe: 0, + vpts: 0, + abuf: Vec::new(), + apts: 0, + ver: 0, + } + } + + fn parse_fcp_header(&mut self, strmgr: &mut StreamManager) -> DemuxerResult<()> { + let tag = self.src.read_tag()?; + validate!(&tag == b"FCHD"); + let hdr_size = self.src.read_u32be()? as usize; + validate!(hdr_size >= 16); + let mut flags = vec![0; 2]; + self.src.read_buf(&mut flags)?; + let width = self.src.read_u16le()? as usize; + let height = self.src.read_u16le()? as usize; + validate!(width > 0 && height > 0); + self.nframes = self.src.read_u16le()? as usize; + self.src.read_skip(8)?; + self.src.read_skip(hdr_size - 16)?; + + let vhdr = NAVideoInfo::new(width, height, false, PAL8_FORMAT); + let vci = NACodecTypeInfo::Video(vhdr); + let vinfo = NACodecInfo::new("beam-fcp", vci, Some(flags)); + if strmgr.add_stream(NAStream::new(StreamType::Video, 0, vinfo, 1, 1000, self.nframes as u64 * DEFAULT_FCP_DELAY)).is_none() { + return Err(DemuxerError::MemoryError); + } + + self.ablock = 1; + let srate = 22050; + let ahdr = NAAudioInfo::new(srate, 1, SND_U8_FORMAT, 1); + let ainfo = NACodecInfo::new("pcm", NACodecTypeInfo::Audio(ahdr), None); + if strmgr.add_stream(NAStream::new(StreamType::Audio, 1, ainfo, 1, srate, 0)).is_none() { + return Err(DemuxerError::MemoryError); + } + + self.vpts = 0; + self.apts = 0; + self.cframe = 0; + + Ok(()) + } + fn get_fcp_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + if self.cframe >= self.nframes { + return Err(DemuxerError::EOF); + } + let size = self.src.read_u16le()? as usize; + validate!(size > 8); + let stream = strmgr.get_stream(0).unwrap(); + let (tb_num, tb_den) = stream.get_timebase(); + let ts = NATimeInfo::new(Some(self.vpts), None, None, tb_num, tb_den); + let kframe = self.vpts == 0; + self.cframe += 1; + let pkt = self.src.read_packet(stream, ts, kframe, size - 2)?; + let buf = pkt.get_buffer(); + + let mut mr = MemoryReader::new_read(buf.as_slice()); + let mut br = ByteReader::new(&mut mr); + let asize = br.read_u16le()? as usize; + let duration = br.read_u16le()? as u64; + validate!(asize < buf.len()); + if asize > 0 { + let nclrs = br.read_u16le()? as usize; + if nclrs > 0 { + br.read_skip(nclrs * 3 + 2)?; + } + self.abuf.resize(asize, 0); + br.read_buf(&mut self.abuf)?; + } + + if duration > 0 { + self.vpts += duration; + } else { + self.vpts += DEFAULT_FCP_DELAY; + } + + Ok(pkt) + } + + fn parse_vbv_header(&mut self, strmgr: &mut StreamManager) -> DemuxerResult<()> { + let tag = self.src.read_tag()?; + validate!(&tag == b"VBHD"); + let hdr_size = self.src.read_u32be()? as usize; + validate!(hdr_size >= 32); + let version = self.src.read_u16le()?; + validate!(version == 1 || version == 2); + self.ver = version as u8; + let width = self.src.read_u16le()? as usize; + let height = self.src.read_u16le()? as usize; + validate!(width > 0 && height > 0); + self.src.read_skip(4)?; + self.nframes = self.src.read_u16le()? as usize; + let flags = self.src.read_u16le()?; + let bits = flags as u8; + let channels = if (flags & 0x100) != 0 { 2 } else { 1 }; + validate!(bits == 0 || bits >= 8); + let srate = self.src.read_u16le()? as u32; + self.ablock = (bits as usize) * (channels as usize) / 8; + self.src.read_skip(16)?; + self.src.read_skip(hdr_size - 32)?; + + let mut vhdr = NAVideoInfo::new(width, height, false, if version == 1 { PAL8_FORMAT } else { RGB565_FORMAT }); + vhdr.bits = version as u8 * 8; + let vci = NACodecTypeInfo::Video(vhdr); + let vinfo = NACodecInfo::new("beam-video", vci, None); + if strmgr.add_stream(NAStream::new(StreamType::Video, 0, vinfo, 1, 1000, self.nframes as u64 * DEFAULT_VBV_DELAY)).is_none() { + return Err(DemuxerError::MemoryError); + } + + if srate > 0 && bits > 0 { + let ahdr = NAAudioInfo::new(srate, channels, if bits == 8 { SND_U8_FORMAT } else { SND_S16_FORMAT }, self.ablock); + let ainfo = NACodecInfo::new("pcm", NACodecTypeInfo::Audio(ahdr), None); + if strmgr.add_stream(NAStream::new(StreamType::Audio, 1, ainfo, 1, srate, 0)).is_none() { + return Err(DemuxerError::MemoryError); + } + } + + self.vpts = 0; + self.apts = 0; + self.cframe = 0; + + Ok(()) + } + fn get_vbv_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + if self.cframe >= self.nframes { + return Err(DemuxerError::EOF); + } + let size = self.src.read_u32le()? as usize; + validate!(size > 6); + let stream = strmgr.get_stream(0).unwrap(); + let (tb_num, tb_den) = stream.get_timebase(); + let ts = NATimeInfo::new(Some(self.vpts), None, None, tb_num, tb_den); + let kframe = self.vpts == 0; + self.cframe += 1; + let pkt = self.src.read_packet(stream, ts, kframe, size - 4)?; + let buf = pkt.get_buffer(); + + let mut mr = MemoryReader::new_read(buf.as_slice()); + let mut br = ByteReader::new(&mut mr); + let flags = br.read_u16le()?; + if (flags & 0x01) != 0 { + br.read_skip(4)?; + } + if (flags & 0x04) != 0 { + let asize = br.read_u32le()? as usize; + validate!((asize > 4) && asize < (buf.len() - (br.tell() as usize))); + self.abuf.resize(asize - 4, 0); + br.read_buf(&mut self.abuf)?; + } + if (flags & 0x08) != 0 { + let vsize = br.read_u32le()? as usize; + validate!(vsize > 4); + br.read_skip(vsize - 4)?; + } + if (flags & 0x10) != 0 { + let psize = br.read_u32le()? as usize; + validate!(psize > 4); + br.read_skip(psize - 4)?; + } + let delay = if (flags & 0x20) != 0 { + br.read_u16le()? as u64 + } else { 0 }; + self.vpts += if delay > 0 { delay } else { DEFAULT_VBV_DELAY }; + + Ok(pkt) + } + + fn parse_snd_header(&mut self, strmgr: &mut StreamManager) -> DemuxerResult<()> { + let tag = self.src.read_tag()?; + validate!(&tag == b"SHDR"); + let hdr_size = self.src.read_u32be()? as usize; + validate!(hdr_size >= 8); + let snd_size = self.src.read_u32le()?; + let srate = self.src.read_u16le()? as u32; + let flags = self.src.read_u16le()?; + let bits = flags as u8; + validate!(bits >= 8); + let channels = if (flags & 0x100) != 0 { 2 } else { 1 }; + self.ablock = (bits as usize) * (channels as usize); + self.src.read_skip(hdr_size - 8)?; + + let duration = u64::from(snd_size) / u64::from(channels) * 8 / u64::from(bits); + + let fmt = match bits { + 8 => SND_U8_FORMAT, + 16 => SND_S16_FORMAT, + 12 => NASoniton::new(12, SONITON_FLAG_PACKED | SONITON_FLAG_SIGNED), + _ => return Err(DemuxerError::NotImplemented), + }; + let ahdr = NAAudioInfo::new(srate, channels, fmt, self.ablock); + let ainfo = NACodecInfo::new("pcm", NACodecTypeInfo::Audio(ahdr), None); + if strmgr.add_stream(NAStream::new(StreamType::Audio, 0, ainfo, 1, srate, duration)).is_none() { + return Err(DemuxerError::MemoryError); + } + + Ok(()) + } + fn get_snd_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + if self.size == 0 { + return Err(DemuxerError::EOF); + } + let cur_size = self.size.min(1024 * (self.ablock as u32)); + + let stream = strmgr.get_stream(0).unwrap(); + let (tb_num, tb_den) = stream.get_timebase(); + let ts = NATimeInfo::new(None, None, None, tb_num, tb_den); + let pkt = self.src.read_packet(stream, ts, true, cur_size as usize)?; + self.size -= cur_size; + + Ok(pkt) + } +} + +impl<'a> DemuxCore<'a> for SIFFDemuxer<'a> { + fn open(&mut self, strmgr: &mut StreamManager, _seek_index: &mut SeekIndex) -> DemuxerResult<()> { + let magic = self.src.read_tag()?; + validate!(&magic == b"SIFF"); + self.size = self.src.read_u32be()?; + let tag = self.src.read_tag()?; + self.subtype = match &tag { + b"FCPK" => SIFFType::FCP, + b"VBV1" => SIFFType::VBV, + b"SOUN" => SIFFType::Sound, + _ => return Err(DemuxerError::NotImplemented), + }; + + match self.subtype { + SIFFType::FCP => self.parse_fcp_header(strmgr)?, + SIFFType::VBV => self.parse_vbv_header(strmgr)?, + SIFFType::Sound => self.parse_snd_header(strmgr)?, + _ => unreachable!(), + }; + + let tag = self.src.read_tag()?; + validate!(&tag == b"BODY"); + let body_size = self.src.read_u32be()?; + validate!(self.src.tell() + u64::from(body_size) <= u64::from(self.size) + 8); + self.size = body_size; + + Ok(()) + } + + fn get_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + if !self.abuf.is_empty() { + let mut buf = Vec::new(); + std::mem::swap(&mut buf, &mut self.abuf); + + if let Some(stream) = strmgr.get_stream(1) { + let (tb_num, tb_den) = stream.get_timebase(); + let ts = NATimeInfo::new(Some(self.apts), None, None, tb_num, tb_den); + self.apts += (buf.len() / self.ablock) as u64; + return Ok(NAPacket::new(stream, ts, true, buf)); + } + } + match self.subtype { + SIFFType::FCP => self.get_fcp_frame(strmgr), + SIFFType::VBV => self.get_vbv_frame(strmgr), + SIFFType::Sound => self.get_snd_frame(strmgr), + _ => unreachable!(), + } + } + + fn seek(&mut self, _time: NATimePoint, _seek_index: &SeekIndex) -> DemuxerResult<()> { + Err(DemuxerError::NotPossible) + } + fn get_duration(&self) -> u64 { 0 } +} + +impl<'a> NAOptionHandler for SIFFDemuxer<'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 SIFFDemuxerCreator { } + +impl DemuxerCreator for SIFFDemuxerCreator { + fn new_demuxer<'a>(&self, br: &'a mut ByteReader<'a>) -> Box + 'a> { + Box::new(SIFFDemuxer::new(br)) + } + fn get_name(&self) -> &'static str { "siff" } +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs::File; + + fn test_siff_demux(name: &str) { + let mut file = File::open(name).unwrap(); + let mut fr = FileReader::new_read(&mut file); + let mut br = ByteReader::new(&mut fr); + let mut dmx = SIFFDemuxer::new(&mut br); + let mut sm = StreamManager::new(); + let mut si = SeekIndex::new(); + dmx.open(&mut sm, &mut si).unwrap(); + loop { + let pktres = dmx.get_frame(&mut sm); + if let Err(e) = pktres { + if (e as i32) == (DemuxerError::EOF as i32) { break; } + panic!("error"); + } + let pkt = pktres.unwrap(); + println!("Got {}", pkt); + } + } + + #[test] + fn test_siff_demux_fcp() { + // sample from The Dame was Loaded game + test_siff_demux("assets/Game/siff/BEAM.FCP"); + } + #[test] + fn test_siff_demux_anim_8bit() { + // sample from Lost Vikings 2 game + test_siff_demux("assets/Game/siff/BEAM.VB"); + } + #[test] + fn test_siff_demux_anim_16bit() { + // sample from Alien Earth game + test_siff_demux("assets/Game/siff/beamlogo.vbc"); + } + #[test] + fn test_siff_demux_snd() { + // sample from The Dame was Loaded game + test_siff_demux("assets/Game/siff/01AFIRST.SON"); + // sample from Lost Vikings 2 game + test_siff_demux("assets/Game/siff/01THEME.SON"); + } +} diff --git a/nihav-registry/src/detect.rs b/nihav-registry/src/detect.rs index e73ae4b..dd095d4 100644 --- a/nihav-registry/src/detect.rs +++ b/nihav-registry/src/detect.rs @@ -286,6 +286,16 @@ const DETECTORS: &[DetectConditions] = &[ conditions: &[CheckItem{offs: 0, cond: &CC::Eq(Arg::U16LE(0x6839))}, CheckItem{offs: 2, cond: &CC::In(Arg::Byte(3), Arg::Byte(7))}], }, + DetectConditions { + demux_name: "siff", + extensions: ".vb,.vbc,.fcp,.son", + conditions: &[CheckItem{offs: 0, cond: &CC::Str(b"SIFF")}, + CheckItem{offs: 4, cond: &CC::Or( + &CC::Or( + &CC::Str(b"VBV1VBHD"), + &CC::Str(b"SOUNSHDR")), + &CC::Str(b"FCPKFCHD"))}], + }, DetectConditions { demux_name: "smush", extensions: ".san", diff --git a/nihav-registry/src/register.rs b/nihav-registry/src/register.rs index 21e6165..9b0c392 100644 --- a/nihav-registry/src/register.rs +++ b/nihav-registry/src/register.rs @@ -243,6 +243,8 @@ static CODEC_REGISTER: &[CodecDescription] = &[ desc!(video; "gdv-video", "Gremlin Digital Video - video"), desc!(audio; "gdv-audio", "Gremlin Digital Video - audio"), + desc!(video; "beam-fcp", "Beam Software Animation"), + desc!(video; "beam-video", "Beam Software Video"), desc!(video; "bmv-video", "BMV video"), desc!(audio; "bmv-audio", "BMV audio"), desc!(video; "bmv3-video", "DW Noir BMV video"), -- 2.39.5