From: Kostya Shishkov Date: Fri, 15 May 2026 16:21:54 +0000 (+0200) Subject: QT IMA ADPCM encoder X-Git-Url: https://git.nihav.org/?a=commitdiff_plain;h=d44360a83e6b2d7251aa09f4c3f37342c9ce3c69;p=nihav.git QT IMA ADPCM encoder --- diff --git a/nihav-qt/Cargo.toml b/nihav-qt/Cargo.toml index 6beb078..9564a2c 100644 --- a/nihav-qt/Cargo.toml +++ b/nihav-qt/Cargo.toml @@ -51,5 +51,6 @@ encoder_rawvid = ["encoders"] encoder_rle = ["encoders"] encoder_rpza = ["encoders"] -all_audio_encoders = ["encoder_mace"] +all_audio_encoders = ["encoder_ima_adpcm_qt", "encoder_mace"] +encoder_ima_adpcm_qt = ["encoders"] encoder_mace = ["encoders"] diff --git a/nihav-qt/src/codecs/imaadpcmenc.rs b/nihav-qt/src/codecs/imaadpcmenc.rs new file mode 100644 index 0000000..2735d57 --- /dev/null +++ b/nihav-qt/src/codecs/imaadpcmenc.rs @@ -0,0 +1,263 @@ +use std::convert::TryInto; +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; +use nihav_codec_support::codecs::imaadpcm::*; + +const PACKET_LEN: usize = 34; +const PACKET_SAMPLES: usize = 64; + +struct NibbleWriter<'a> { + buf: &'a mut [u8; PACKET_LEN], + pos: usize, + val: u8, + first: bool, +} + +impl<'a> NibbleWriter<'a> { + fn new(buf: &'a mut [u8; PACKET_LEN]) -> Self { + Self { buf, pos: 2, val: 0, first: true } + } + fn write(&mut self, nib: u8) { + if self.first { + self.val = nib; + self.first = false; + } else { + self.val |= nib << 4; + self.buf[self.pos] = self.val; + self.pos += 1; + self.first = true; + } + } +} + +fn encode_packet(samples: &[i16; PACKET_SAMPLES], dst: &mut [u8; PACKET_LEN]) { + if samples == &[0; PACKET_SAMPLES] { return; } + + let mut pred = IMAState::new(); + pred.reset(samples[0] & !0x7F, calc_step(samples[0], samples[1])); + + let hdr = (pred.predictor as u16) | (pred.step as u16); + let _ = write_u16be(dst, hdr); + let mut nw = NibbleWriter::new(dst); + for &samp in samples.iter() { + let nib = pred.compress_sample(samp); + pred.expand_sample(nib); + nw.write(nib); + } +} +fn calc_step(samp1: i16, samp2: i16) -> u8 { + let diff = (i32::from(samp1) - i32::from(samp2)).abs(); + for (i, &step) in IMA_STEP_TABLE.iter().enumerate() { + if step >= diff { + return i as u8; + } + } + IMA_MAX_STEP +} + +#[derive(Default)] +struct IMAADPCMEncoder { + stream: Option, + samples: Vec>, + time: u64, + block_len: usize, + block_size: usize, + flush: bool, + srate: u32, +} + +impl IMAADPCMEncoder { + fn new() -> Self { Self::default() } + fn encode_packet(&mut self) -> EncoderResult { + if self.flush && (1..PACKET_SAMPLES).contains(&self.samples[0].len()) { + for smp in self.samples.iter_mut() { + smp.resize(PACKET_SAMPLES, 0); + } + } + let nblocks = self.samples[0].len() / PACKET_SAMPLES; + if nblocks == 0 { + return Err(EncoderError::TryAgain); + } + + let mut dbuf = vec![0u8; nblocks * self.samples.len() * PACKET_LEN]; + + for (n, oblock) in dbuf.chunks_exact_mut(PACKET_LEN * self.samples.len()).enumerate() { + for (dst, samps) in oblock.chunks_exact_mut(PACKET_LEN).zip(self.samples.iter()) { + let blk = dst.try_into().unwrap(); + let src = samps[n * PACKET_SAMPLES..][..PACKET_SAMPLES].try_into().unwrap(); + encode_packet(src, blk); + } + } + + for samps in self.samples.iter_mut() { + samps.drain(..nblocks * PACKET_SAMPLES); + } + + let ts = NATimeInfo::new(Some(self.time), None, Some(nblocks as u64), PACKET_SAMPLES as u32, self.srate); + self.time += nblocks as u64; + Ok(NAPacket::new(self.stream.clone().unwrap(), ts, true, dbuf)) + } +} + +impl NAEncoder for IMAADPCMEncoder { + fn negotiate_format(&self, encinfo: &EncodeParameters) -> EncoderResult { + match encinfo.format { + NACodecTypeInfo::None => { + Ok(EncodeParameters { + format: NACodecTypeInfo::Audio(NAAudioInfo::new(0, 1, SND_S16_FORMAT, PACKET_SAMPLES)), + ..Default::default() }) + }, + NACodecTypeInfo::Video(_) => Err(EncoderError::FormatError), + NACodecTypeInfo::Audio(ainfo) => { + let mut outinfo = ainfo; + outinfo.channels = outinfo.channels.max(1); + if outinfo.format != SND_S16P_FORMAT && outinfo.format != SND_S16_FORMAT { + outinfo.format = SND_S16_FORMAT; + } + if outinfo.block_len <= PACKET_SAMPLES { + outinfo.block_len = PACKET_SAMPLES; + } + outinfo.block_len /= PACKET_SAMPLES; + outinfo.block_len *= PACKET_SAMPLES; + 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_S16P_FORMAT && ainfo.format != SND_S16_FORMAT { + return Err(EncoderError::FormatError); + } + if ainfo.block_len < PACKET_SAMPLES || (ainfo.block_len % PACKET_SAMPLES) != 0 { + return Err(EncoderError::FormatError); + } + let channels = usize::from(ainfo.channels); + self.samples.clear(); + for _ in 0..channels { + self.samples.push(Vec::new()); + } + self.block_len = ainfo.block_len; + self.block_size = (self.block_len / PACKET_SAMPLES) * PACKET_LEN * channels; + + let out_ainfo = NAAudioInfo::new(ainfo.sample_rate, ainfo.channels, SND_S16P_FORMAT, self.block_size); + let info = NACodecInfo::new("ima-adpcm-qt", NACodecTypeInfo::Audio(out_ainfo), None); + let mut stream = NAStream::new(StreamType::Audio, stream_id, info, PACKET_SAMPLES as u32, ainfo.sample_rate, 0); + stream.set_num(stream_id as usize); + let stream = stream.into_ref(); + + self.stream = Some(stream.clone()); + self.srate = 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_i16() { + let src = abuf.get_data(); + let len = abuf.get_length(); + let ch = abuf.get_chmap().num_channels(); + if abuf.get_step() == 1 || ch == 1 { + let astride = abuf.get_stride().max(len); + for (samp, src) in self.samples.iter_mut().zip(src.chunks_exact(astride)) { + samp.extend_from_slice(&src[..len]); + } + } else { + for samps in src.chunks_exact(ch) { + for (dst, &samp) in self.samples.iter_mut().zip(samps.iter()) { + dst.push(samp); + } + } + } + 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 IMAADPCMEncoder { + 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() -> Box { + Box::new(IMAADPCMEncoder::new()) +} + +#[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::*; + + #[test] + fn test_encoder() { + 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: "ima-adpcm-qt", + out_name: "ima-adpcm-qt.mov", + mux_reg, enc_reg, + }; + let dst_ainfo = NAAudioInfo { + sample_rate: 0, + channels: 0, + format: SND_S16_FORMAT, + block_len: 128, + }; + 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, + &[0x656ee676, 0x807322f6, 0xbb99d846, 0x222e269b]); + } +} diff --git a/nihav-qt/src/codecs/mod.rs b/nihav-qt/src/codecs/mod.rs index 13d9756..a058a3b 100644 --- a/nihav-qt/src/codecs/mod.rs +++ b/nihav-qt/src/codecs/mod.rs @@ -125,6 +125,8 @@ mod rleenc; #[cfg(feature="encoder_rpza")] mod rpzaenc; +#[cfg(feature="encoder_ima_adpcm_qt")] +mod imaadpcmenc; #[cfg(feature="encoder_mace")] mod maceenc; @@ -139,6 +141,8 @@ const QT_ENCODERS: &[EncoderInfo] = &[ #[cfg(feature="encoder_rpza")] EncoderInfo { name: "apple-video", get_encoder: rpzaenc::get_encoder }, +#[cfg(feature="encoder_ima_adpcm_qt")] + EncoderInfo { name: "ima-adpcm-qt", get_encoder: imaadpcmenc::get_encoder }, #[cfg(feature="encoder_mace")] EncoderInfo { name: "mace-3", get_encoder: maceenc::get_encoder_3 }, #[cfg(feature="encoder_mace")]