]> git.nihav.org Git - nihav.git/commitdiff
QT IMA ADPCM encoder
authorKostya Shishkov <kostya.shishkov@gmail.com>
Fri, 15 May 2026 16:21:54 +0000 (18:21 +0200)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Fri, 15 May 2026 16:21:54 +0000 (18:21 +0200)
nihav-qt/Cargo.toml
nihav-qt/src/codecs/imaadpcmenc.rs [new file with mode: 0644]
nihav-qt/src/codecs/mod.rs

index 6beb078dad8ea278a18a9501f81d29fcf95ad78a..9564a2cfbaf8826ebed4737c421ddd391c6b48ab 100644 (file)
@@ -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 (file)
index 0000000..2735d57
--- /dev/null
@@ -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<NAStreamRef>,
+    samples:    Vec<Vec<i16>>,
+    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<NAPacket> {
+        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<EncodeParameters> {
+        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<NAStreamRef> {
+        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<Option<NAPacket>> {
+        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<NAValue> { None }
+}
+
+pub fn get_encoder() -> Box<dyn NAEncoder + Send> {
+    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]);
+    }
+}
index 13d975687d113af1315ef6e33f8ab4ca4681b4e4..a058a3bb0fe5cf23612a3300eab52f380b19f138 100644 (file)
@@ -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")]