From 8ceec5dd82d11b2c7d77ee74c7022172ddba29c6 Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Fri, 15 May 2026 17:52:27 +0200 Subject: [PATCH] MACE-3 and MACE-6 encoder --- nihav-qt/Cargo.toml | 5 +- nihav-qt/src/codecs/maceenc.rs | 333 +++++++++++++++++++++++++++++++++ nihav-qt/src/codecs/mod.rs | 8 + 3 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 nihav-qt/src/codecs/maceenc.rs diff --git a/nihav-qt/Cargo.toml b/nihav-qt/Cargo.toml index 8e8f20a..6beb078 100644 --- a/nihav-qt/Cargo.toml +++ b/nihav-qt/Cargo.toml @@ -13,7 +13,7 @@ path = "../nihav-codec-support" features = ["blockdsp", "fft", "qmf", "qt_pal"] [dev-dependencies] -nihav_commonfmt = { path = "../nihav-commonfmt", default-features=false, features = ["all_demuxers", "muxer_mov"] } +nihav_commonfmt = { path = "../nihav-commonfmt", default-features=false, features = ["all_demuxers", "muxer_mov", "decoder_pcm"] } [features] default = ["all_decoders", "all_demuxers", "all_encoders"] @@ -51,4 +51,5 @@ encoder_rawvid = ["encoders"] encoder_rle = ["encoders"] encoder_rpza = ["encoders"] -all_audio_encoders = [] +all_audio_encoders = ["encoder_mace"] +encoder_mace = ["encoders"] diff --git a/nihav-qt/src/codecs/maceenc.rs b/nihav-qt/src/codecs/maceenc.rs new file mode 100644 index 0000000..5f7d45a --- /dev/null +++ b/nihav-qt/src/codecs/maceenc.rs @@ -0,0 +1,333 @@ +use nihav_core::codecs::*; +use super::macecommon::*; + +const MIN_BLOCK_LEN: usize = 16 * 6; +const DEFAULT_BLOCK_LEN: usize = 128 * 3; + +struct MaceEncoder { + stream: Option, + ch_pred: [ChannelPredictor; 2], + is_mace6: bool, + samples: Vec>, + rate: u32, + block_len: usize, + flush: bool, + ts: u32, +} + +fn samp_dist(a: u8, b: u8) -> u16 { + let diff = u16::from(a.abs_diff(b)); + diff * diff +} + +impl MaceEncoder { + fn new(is_mace6: bool) -> Self { + Self { + stream: None, + samples: Vec::new(), + ch_pred: [ChannelPredictor::default(); 2], + block_len: 0, + rate: 0, + ts: 0, + flush: false, + is_mace6, + } + } + fn encode_packet(&mut self) -> EncoderResult { + let align = 6; + let len = self.samples[0].len().min(self.block_len) / align * align; + if len == 0 || (len < self.block_len && !self.flush) { + return Err(EncoderError::TryAgain); + } + + let mut dbuf = Vec::with_capacity(len * self.samples.len() / align); + if !self.is_mace6 { + let mut iters = Vec::new(); + for smp in self.samples.iter() { + iters.push(smp.chunks_exact(3)); + } + for _ in (0..len).step_by(6) { + for (src, cpred) in iters.iter_mut().zip(self.ch_pred.iter_mut()) { + for _ in 0..2 { + let samples = src.next().unwrap(); + + let mut best_idx0 = 0; + let mut best_dist = u16::MAX; + for idx0 in 0..8 { + let mut cp = *cpred; + let samp = cp.pred_mace3(idx0, false); + let dist = samp_dist(samp, samples[0]); + if dist < best_dist { + best_idx0 = idx0; + best_dist = dist; + } + } + cpred.pred_mace3(best_idx0, false); + + let mut best_idx1 = 0; + let mut best_dist = u16::MAX; + for idx1 in 0..4 { + let mut cp = *cpred; + let samp = cp.pred_mace3(idx1, true); + let dist = samp_dist(samp, samples[1]); + if dist < best_dist { + best_idx1 = idx1; + best_dist = dist; + } + } + cpred.pred_mace3(best_idx1, true); + + let mut best_idx2 = 0; + let mut best_dist = u16::MAX; + for idx2 in 0..8 { + let mut cp = *cpred; + let samp = cp.pred_mace3(idx2, false); + let dist = samp_dist(samp, samples[2]); + if dist < best_dist { + best_idx2 = idx2; + best_dist = dist; + } + } + cpred.pred_mace3(best_idx2, false); + + dbuf.push(((best_idx2 << 5) | (best_idx1 << 3) | best_idx0) as u8); + } + } + } + } else { + let mut iters = Vec::new(); + for smp in self.samples.iter() { + iters.push(smp.chunks_exact(6)); + } + for _ in (0..len).step_by(6) { + for (src, cpred) in iters.iter_mut().zip(self.ch_pred.iter_mut()) { + let samples = src.next().unwrap(); + + let mut best_idx0 = 0; + let mut best_dist = u16::MAX; + for idx0 in 0..8 { + let mut cp = *cpred; + let (samp0, samp1) = cp.pred_mace6(idx0, false); + let dist = samp_dist(samp0, samples[0]) + samp_dist(samp1, samples[1]); + if dist < best_dist { + best_idx0 = idx0; + best_dist = dist; + } + } + cpred.pred_mace6(best_idx0, false); + + let mut best_idx1 = 0; + let mut best_dist = u16::MAX; + for idx1 in 0..4 { + let mut cp = *cpred; + let (samp0, samp1) = cp.pred_mace6(idx1, true); + let dist = samp_dist(samp0, samples[2]) + samp_dist(samp1, samples[3]); + if dist < best_dist { + best_idx1 = idx1; + best_dist = dist; + } + } + cpred.pred_mace6(best_idx1, true); + + let mut best_idx2 = 0; + let mut best_dist = u16::MAX; + for idx2 in 0..8 { + let mut cp = *cpred; + let (samp0, samp1) = cp.pred_mace6(idx2, false); + let dist = samp_dist(samp0, samples[4]) + samp_dist(samp1, samples[5]); + if dist < best_dist { + best_idx2 = idx2; + best_dist = dist; + } + } + cpred.pred_mace6(best_idx2, false); + + dbuf.push(((best_idx0 << 5) | (best_idx1 << 3) | best_idx2) as u8); + } + } + } + for samps in self.samples.iter_mut() { + samps.drain(..len); + } + + let ts = NATimeInfo::new(Some(u64::from(self.ts)), None, Some(len as u64), 1, self.rate); + self.ts += len as u32; + Ok(NAPacket::new(self.stream.clone().unwrap(), ts, true, dbuf)) + } +} + +impl NAEncoder for MaceEncoder { + fn negotiate_format(&self, encinfo: &EncodeParameters) -> EncoderResult { + match encinfo.format { + NACodecTypeInfo::None => { + Ok(EncodeParameters { + format: NACodecTypeInfo::Audio(NAAudioInfo::new(0, 1, SND_S16_FORMAT, DEFAULT_BLOCK_LEN)), + ..Default::default() }) + }, + NACodecTypeInfo::Video(_) => Err(EncoderError::FormatError), + NACodecTypeInfo::Audio(ainfo) => { + let mut outinfo = ainfo; + outinfo.channels = outinfo.channels.clamp(1, 2); + outinfo.format = if outinfo.channels == 2 || outinfo.format == SND_U8P_FORMAT { SND_U8P_FORMAT } else { SND_U8_FORMAT }; + if outinfo.block_len < MIN_BLOCK_LEN { + outinfo.block_len = DEFAULT_BLOCK_LEN; + } + if (outinfo.block_len % 6) != 0 { + outinfo.block_len = (outinfo.block_len / 6 + 1) * 6; + } + let mut ofmt = *encinfo; + ofmt.format = NACodecTypeInfo::Audio(outinfo); + Ok(ofmt) + } + } + } + fn get_capabilities(&self) -> u64 { ENC_CAPS_CBR } + fn init(&mut self, stream_id: u32, encinfo: EncodeParameters) -> EncoderResult { + match encinfo.format { + NACodecTypeInfo::None => Err(EncoderError::FormatError), + NACodecTypeInfo::Video(_) => Err(EncoderError::FormatError), + NACodecTypeInfo::Audio(ainfo) => { + if ainfo.format != SND_U8P_FORMAT && (ainfo.format != SND_U8_FORMAT || ainfo.channels != 1) { + return Err(EncoderError::FormatError); + } + let align = if self.is_mace6 { 6 } else { 3 }; + if ainfo.block_len < MIN_BLOCK_LEN || (ainfo.block_len % 6) != 0 { + return Err(EncoderError::FormatError); + } + if ainfo.channels != 1 && ainfo.channels != 2 { + return Err(EncoderError::FormatError); + } + for _ in 0..ainfo.channels { + self.samples.push(Vec::new()); + } + self.block_len = ainfo.block_len; + + let out_ainfo = NAAudioInfo::new(ainfo.sample_rate, ainfo.channels, SND_U8P_FORMAT, self.block_len * usize::from(ainfo.channels) / align); + let info = NACodecInfo::new(if self.is_mace6 { "mace-6" } else { "mace-3" }, NACodecTypeInfo::Audio(out_ainfo), None); + let mut stream = NAStream::new(StreamType::Audio, stream_id, info, 1, ainfo.sample_rate, 0); + stream.set_num(stream_id as usize); + let stream = stream.into_ref(); + + self.stream = Some(stream.clone()); + self.rate = ainfo.sample_rate; + self.flush = false; + + Ok(stream) + }, + } + } + fn encode(&mut self, frm: &NAFrame) -> EncoderResult<()> { + let buf = frm.get_buffer(); + if let Some(ref abuf) = buf.get_abuf_u8() { + let src = abuf.get_data(); + let len = abuf.get_length(); + let ch = abuf.get_chmap().num_channels(); + if ch != self.samples.len() { + return Err(EncoderError::InvalidParameters); + } + if abuf.get_step() == 1 || ch == 1 { + let astride = abuf.get_stride(); + for (dst, samp) in self.samples.iter_mut().zip(src.chunks_exact(astride.max(len))) { + dst.extend_from_slice(&samp[..len]); + } + } else { + for pair in src.chunks_exact(2).take(len) { + self.samples[0].push(pair[0]); + self.samples[1].push(pair[1]); + } + } + Ok(()) + } else { + Err(EncoderError::InvalidParameters) + } + } + fn get_packet(&mut self) -> EncoderResult> { + if let Ok(pkt) = self.encode_packet() { + Ok(Some(pkt)) + } else { + Ok(None) + } + } + fn flush(&mut self) -> EncoderResult<()> { + self.flush = true; + Ok(()) + } +} + +impl NAOptionHandler for MaceEncoder { + 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_encoder_3() -> Box { + Box::new(MaceEncoder::new(false)) +} + +pub fn get_encoder_6() -> Box { + Box::new(MaceEncoder::new(true)) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::*; + use nihav_core::muxers::RegisteredMuxers; + use nihav_core::demuxers::RegisteredDemuxers; + use nihav_codec_support::test::enc_video::*; + use crate::*; + use nihav_commonfmt::*; + + fn test_encoder(name: &'static str, enc_name: &'static str, hash: &[u32; 4]) { + let mut dmx_reg = RegisteredDemuxers::new(); + generic_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + generic_register_all_decoders(&mut dec_reg); + qt_register_all_decoders(&mut dec_reg); + let mut mux_reg = RegisteredMuxers::new(); + generic_register_all_muxers(&mut mux_reg); + let mut enc_reg = RegisteredEncoders::new(); + qt_register_all_encoders(&mut enc_reg); + + // sample from The Wonders of Electricity: An Adventure in Safety + let dec_config = DecoderTestParams { + demuxer: "mov-resfork", + in_name: "assets/QT/car.mov", + stream_type: StreamType::Audio, + limit: None, + dmx_reg, dec_reg, + }; + let enc_config = EncoderTestParams { + muxer: "mov", + enc_name, + out_name: name, + mux_reg, enc_reg, + }; + let dst_ainfo = NAAudioInfo { + sample_rate: 0, + channels: 0, + format: SND_U8_FORMAT, + block_len: 128 * 3, + }; + let enc_params = EncodeParameters { + format: NACodecTypeInfo::Audio(dst_ainfo), + quality: 0, + bitrate: 0, + tb_num: 0, + tb_den: 0, + flags: 0, + }; + let enc_options = &[]; +// test_encoding_to_file(&dec_config, &enc_config, enc_params, enc_options); + + test_encoding_md5(&dec_config, &enc_config, enc_params, enc_options, + hash); + } + #[test] + fn test_mace3() { + test_encoder("mace3.mov", "mace-3", &[0xa9d4302f, 0xc78d80f4, 0xc12c5485, 0x43943976]); + } + #[test] + fn test_mace6() { + test_encoder("mace6.mov", "mace-6", &[0xca9efa62, 0xa475a762, 0x8d5c70b9, 0x9a7f46b6]); + } +} diff --git a/nihav-qt/src/codecs/mod.rs b/nihav-qt/src/codecs/mod.rs index fc043bd..13d9756 100644 --- a/nihav-qt/src/codecs/mod.rs +++ b/nihav-qt/src/codecs/mod.rs @@ -125,6 +125,9 @@ mod rleenc; #[cfg(feature="encoder_rpza")] mod rpzaenc; +#[cfg(feature="encoder_mace")] +mod maceenc; + #[cfg(feature="encoders")] const QT_ENCODERS: &[EncoderInfo] = &[ #[cfg(feature="encoder_rawvid")] @@ -135,6 +138,11 @@ const QT_ENCODERS: &[EncoderInfo] = &[ EncoderInfo { name: "qt-rle", get_encoder: rleenc::get_encoder }, #[cfg(feature="encoder_rpza")] EncoderInfo { name: "apple-video", get_encoder: rpzaenc::get_encoder }, + +#[cfg(feature="encoder_mace")] + EncoderInfo { name: "mace-3", get_encoder: maceenc::get_encoder_3 }, +#[cfg(feature="encoder_mace")] + EncoderInfo { name: "mace-6", get_encoder: maceenc::get_encoder_6 }, ]; /// Registers all available encoders provided by this crate. -- 2.39.5