use oxideav_core::bits::BitWriter;
use crate::encoder_asf::build_mono_simple_asf_body_from_pcm_spectrum;
use crate::encoder_mdct::EncoderMdctState;
#[derive(Debug, Clone)]
pub struct Ac4ImsEncoder {
pub bitstream_version: u8,
pub sequence_counter: u16,
pub fs_index: u8,
pub frame_rate_index: u8,
pub b_iframe_global: bool,
pub channel_mode_value: u8,
pub channel_mode_bits: u8,
pub mdct_state: Option<EncoderMdctState>,
}
impl Ac4ImsEncoder {
pub fn new() -> Self {
Self {
bitstream_version: 2,
sequence_counter: 0,
fs_index: 1,
frame_rate_index: 1,
b_iframe_global: true,
channel_mode_value: 0b0,
channel_mode_bits: 1,
mdct_state: None,
}
}
pub fn with_v0(mut self) -> Self {
self.bitstream_version = 0;
self
}
pub fn with_stereo(mut self) -> Self {
self.channel_mode_value = 0b10;
self.channel_mode_bits = 2;
self
}
pub fn with_5_1(mut self) -> Self {
self.channel_mode_value = 0b1110;
self.channel_mode_bits = 4;
self
}
pub fn encode_frame(&mut self, body_padding_bytes: usize) -> Vec<u8> {
let mut bw = BitWriter::new();
self.write_toc(&mut bw);
bw.align_to_byte();
let mut frame = bw.finish();
if body_padding_bytes > 0 {
frame.extend(vec![0u8; body_padding_bytes]);
}
self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
frame
}
pub fn encode_frame_v0(&mut self, body_padding_bytes: usize) -> Vec<u8> {
let saved = self.bitstream_version;
self.bitstream_version = 0;
let f = self.encode_frame(body_padding_bytes);
self.bitstream_version = saved;
f
}
fn write_toc(&self, bw: &mut BitWriter) {
bw.write_u32(self.bitstream_version as u32, 2);
bw.write_u32(self.sequence_counter as u32, 10);
bw.write_u32(0, 1);
bw.write_u32(self.fs_index as u32, 1);
bw.write_u32(self.frame_rate_index as u32, 4);
bw.write_u32(if self.b_iframe_global { 1 } else { 0 }, 1);
bw.write_u32(1, 1);
bw.write_u32(0, 1);
if self.bitstream_version <= 1 {
self.write_presentation_v0(bw);
} else {
bw.write_u32(0, 1); self.write_presentation_v1_info(bw);
self.write_substream_group_info(bw);
}
bw.write_u32(1, 2);
bw.write_u32(0, 1);
}
fn write_presentation_v0(&self, bw: &mut BitWriter) {
bw.write_u32(1, 1); bw.write_u32(0, 1); bw.write_u32(0, 3); bw.write_u32(0, 1); bw.write_u32(0, 1); bw.write_u32(0, 2); bw.write_u32(0, 3); bw.write_u32(0, 1); bw.write_u32(0, 1); bw.write_u32(
self.channel_mode_value as u32,
self.channel_mode_bits as u32,
);
bw.write_u32(0, 1); bw.write_u32(0, 1); bw.write_u32(0, 1); bw.write_u32(1, 1); bw.write_u32(0, 2); bw.write_u32(0, 1); bw.write_u32(0, 1); }
fn write_presentation_v1_info(&self, bw: &mut BitWriter) {
bw.write_u32(1, 1);
bw.write_u32(0, 1);
bw.write_u32(0, 3);
bw.write_u32(0, 1);
bw.write_u32(0, 1);
bw.write_u32(0, 2);
bw.write_u32(0, 3);
bw.write_u32(0, 1);
bw.write_u32(0, 1);
bw.write_u32(0, 1);
bw.write_u32(0, 3);
bw.write_u32(0, 1);
bw.write_u32(0, 1);
bw.write_u32(0, 1);
bw.write_u32(if self.b_iframe_global { 0 } else { 1 }, 1);
bw.write_u32(0, 2);
}
fn write_substream_group_info(&self, bw: &mut BitWriter) {
bw.write_u32(1, 1);
bw.write_u32(0, 1);
bw.write_u32(1, 1);
bw.write_u32(1, 1);
bw.write_u32(
self.channel_mode_value as u32,
self.channel_mode_bits as u32,
);
if self.fs_index == 1 {
bw.write_u32(0, 1); }
bw.write_u32(0, 1); bw.write_u32(if self.b_iframe_global { 0 } else { 1 }, 1);
bw.write_u32(0, 2); bw.write_u32(0, 1);
}
}
impl Default for Ac4ImsEncoder {
fn default() -> Self {
Self::new()
}
}
pub fn build_mono_simple_asf_tone_body(
transform_length: u32,
max_sfb: u32,
tone_cb_idx: usize,
tone_pair_idx: u32,
pad_target_bytes: usize,
) -> Vec<u8> {
let mut bw = BitWriter::new();
let audio_size = pad_target_bytes as u32;
let audio_size_lo = audio_size & 0x7FFF;
bw.write_u32(audio_size_lo, 15);
bw.write_bit(false);
bw.align_to_byte();
bw.write_u32(0, 1); bw.write_u32(0, 1); bw.write_bit(true); bw.write_u32(max_sfb, 6); bw.write_u32(5, 4); write_sect_len_incr(&mut bw, max_sfb, 3, 7);
let sfbo = crate::sfb_offset::sfb_offset_48(transform_length).expect("invalid tl");
let end_line = sfbo[max_sfb as usize] as u32;
let hcb = crate::huffman::asf_hcb(5u32).expect("HCB5 must exist");
let pairs = end_line / 2;
let zero_cw = hcb.cw[40];
let zero_len = hcb.len[40] as u32;
let tone_cw = hcb.cw[tone_cb_idx];
let tone_len = hcb.len[tone_cb_idx] as u32;
for p in 0..pairs {
if p == tone_pair_idx {
bw.write_u32(tone_cw, tone_len);
} else {
bw.write_u32(zero_cw, zero_len);
}
}
bw.write_u32(120, 8);
bw.write_u32(0, 1);
bw.align_to_byte();
while bw.byte_len() < pad_target_bytes {
bw.write_u32(0, 8);
}
bw.finish()
}
fn write_sect_len_incr(bw: &mut BitWriter, sect_len: u32, n_sect_bits: u32, esc: u32) {
let base = sect_len.saturating_sub(1);
let k = base / esc;
let incr = base % esc;
for _ in 0..k {
bw.write_u32(esc, n_sect_bits);
}
bw.write_u32(incr, n_sect_bits);
}
impl Ac4ImsEncoder {
pub fn encode_frame_mono_tone(&mut self, tone_cb_idx: usize, tone_pair_idx: u32) -> Vec<u8> {
let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
self.channel_mode_value = 0b0;
self.channel_mode_bits = 1;
let mut bw = BitWriter::new();
self.write_toc(&mut bw);
bw.align_to_byte();
let mut frame = bw.finish();
let body = build_mono_simple_asf_tone_body(1920, 10, tone_cb_idx, tone_pair_idx, 420);
frame.extend(body);
self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
self.channel_mode_value = saved_mode.0;
self.channel_mode_bits = saved_mode.1;
frame
}
pub fn encode_frame_pcm(&mut self, frame: &[f32]) -> Vec<u8> {
self.encode_frame_pcm_with_max_sfb(frame, 40)
}
pub fn encode_frame_pcm_with_max_sfb(&mut self, frame: &[f32], max_sfb: u32) -> Vec<u8> {
let (_fps_milli, frame_len) =
crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
let frame_len = if frame_len == 0 { 1920 } else { frame_len };
assert_eq!(
frame.len(),
frame_len as usize,
"encode_frame_pcm: input length must match frame_len = {frame_len}"
);
let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
self.channel_mode_value = 0b0;
self.channel_mode_bits = 1;
if self.mdct_state.is_none() || self.mdct_state.as_ref().unwrap().n != frame_len {
self.mdct_state = Some(EncoderMdctState::new(frame_len));
}
let coeffs = self.mdct_state.as_mut().unwrap().analyse_frame(frame);
let pad_target_bytes = match max_sfb {
0..=40 => 2048,
41..=50 => 4096,
_ => 8192,
};
let body = build_mono_simple_asf_body_from_pcm_spectrum(
frame_len,
max_sfb,
&coeffs,
pad_target_bytes,
);
let mut bw = BitWriter::new();
self.write_toc(&mut bw);
bw.align_to_byte();
let mut out = bw.finish();
out.extend(body);
self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
self.channel_mode_value = saved_mode.0;
self.channel_mode_bits = saved_mode.1;
out
}
pub fn encode_frame_mono_tone_at_hz(&mut self, target_hz: f32) -> (Vec<u8>, f32) {
let bin_spacing = 48_000.0 / (2.0 * 1_920.0); let target_bin = (target_hz / bin_spacing).round().max(0.0) as u32;
let pair_idx = target_bin / 2;
let actual_hz = (pair_idx * 2) as f32 * bin_spacing;
let frame = self.encode_frame_mono_tone(49, pair_idx);
(frame, actual_hz)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encoder_emits_nonempty_frame() {
let mut enc = Ac4ImsEncoder::new();
let frame = enc.encode_frame(0);
assert!(!frame.is_empty(), "encoder must produce at least the TOC");
assert_eq!(enc.sequence_counter, 1);
}
#[test]
fn encoder_sequence_counter_wraps_at_1024() {
let mut enc = Ac4ImsEncoder::new();
enc.sequence_counter = 1023;
let _ = enc.encode_frame(0);
assert_eq!(enc.sequence_counter, 0);
}
#[test]
fn v0_encoder_round_trips_through_parse_ac4_toc() {
let mut enc = Ac4ImsEncoder::new(); let frame = enc.encode_frame_v0(64);
let info = crate::toc::parse_ac4_toc(&frame).expect("v0 TOC must parse");
assert_eq!(info.fs_index, 1);
assert_eq!(info.frame_rate_index, 1);
assert_eq!(info.frame_length, 1_920);
assert!(info.b_iframe_global);
assert_eq!(info.n_presentations, 1);
assert_eq!(info.n_substreams, 1);
assert_eq!(info.channels, 1);
}
#[test]
fn v0_encoder_round_trips_stereo() {
let mut enc = Ac4ImsEncoder::new().with_v0().with_stereo();
let frame = enc.encode_frame(64);
let info = crate::toc::parse_ac4_toc(&frame).expect("v0 stereo TOC must parse");
assert_eq!(info.channels, 2);
assert!(info.b_iframe_global);
}
#[test]
fn v0_encoder_round_trips_5_1() {
let mut enc = Ac4ImsEncoder::new().with_v0().with_5_1();
let frame = enc.encode_frame(128);
let info = crate::toc::parse_ac4_toc(&frame).expect("v0 5.1 TOC must parse");
assert_eq!(info.channels, 6);
}
#[test]
fn v0_encoder_decoder_roundtrip_emits_silent_frame() {
use crate::decoder::Ac4Decoder;
use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
let mut enc = Ac4ImsEncoder::new(); let frame_bytes = enc.encode_frame_v0(64);
let params = CodecParameters::audio(CodecId::new("ac4"));
let mut dec = Ac4Decoder::new(¶ms);
let pkt = Packet::new(0, TimeBase::new(1, 48_000), frame_bytes);
dec.send_packet(&pkt).expect("send_packet");
let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
panic!("expected audio frame");
};
assert_eq!(af.samples, 1_920);
assert_eq!(af.data.len(), 1);
assert_eq!(af.data[0].len(), 1_920 * 2);
assert!(af.data[0].iter().all(|&b| b == 0));
}
#[test]
fn v2_encoder_emits_first_two_bits_as_bitstream_version_2() {
let mut enc = Ac4ImsEncoder::new(); let frame = enc.encode_frame(0);
assert!(!frame.is_empty());
let bv = (frame[0] >> 6) & 0b11;
assert_eq!(bv, 0b10, "IMS frame must start with bitstream_version = 2");
}
#[test]
fn v0_encoder_emits_first_two_bits_as_bitstream_version_0() {
let mut enc = Ac4ImsEncoder::new().with_v0();
let frame = enc.encode_frame(0);
assert!(!frame.is_empty());
let bv = (frame[0] >> 6) & 0b11;
assert_eq!(bv, 0b00, "v0 frame must start with bitstream_version = 0");
}
#[test]
fn v2_encoder_round_trips_through_parse_ac4_toc() {
let mut enc = Ac4ImsEncoder::new(); let frame = enc.encode_frame(64);
let info = crate::toc::parse_ac4_toc(&frame).expect("v2 TOC must parse");
assert_eq!(info.bitstream_version, 2);
assert_eq!(info.fs_index, 1);
assert_eq!(info.frame_rate_index, 1);
assert_eq!(info.frame_length, 1_920);
assert!(info.b_iframe_global);
assert_eq!(info.n_presentations, 1);
assert_eq!(info.n_substreams, 1);
}
#[test]
fn v2_encoder_round_trips_stereo() {
let mut enc = Ac4ImsEncoder::new().with_stereo(); let frame = enc.encode_frame(64);
let info = crate::toc::parse_ac4_toc(&frame).expect("v2 stereo TOC must parse");
assert_eq!(info.bitstream_version, 2);
assert!(info.b_iframe_global);
}
#[test]
fn v2_encoder_round_trips_5_1() {
let mut enc = Ac4ImsEncoder::new().with_5_1(); let frame = enc.encode_frame(128);
let info = crate::toc::parse_ac4_toc(&frame).expect("v2 5.1 TOC must parse");
assert_eq!(info.bitstream_version, 2);
}
#[test]
fn v2_encoder_mono_tone_roundtrip_emits_nonsilent_pcm() {
use crate::decoder::Ac4Decoder;
use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
let mut enc = Ac4ImsEncoder::new(); let frame_bytes = enc.encode_frame_mono_tone(49, 0);
let params = CodecParameters::audio(CodecId::new("ac4"));
let mut dec = Ac4Decoder::new(¶ms);
let pkt = Packet::new(0, TimeBase::new(1, 48_000), frame_bytes);
dec.send_packet(&pkt).expect("send_packet");
let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
panic!("expected audio frame");
};
assert_eq!(af.samples, 1_920);
assert_eq!(af.data.len(), 1);
assert_eq!(af.data[0].len(), 1_920 * 2);
let samples_i16: Vec<i16> = af.data[0]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect();
let nonzero_count = samples_i16.iter().filter(|&&s| s != 0).count();
assert!(
nonzero_count > 100,
"expected non-silent PCM from IMS tone encoder, got {nonzero_count} non-zero samples"
);
let energy: i64 = samples_i16.iter().map(|&s| (s as i64) * (s as i64)).sum();
assert!(energy > 0, "zero-energy tone output from IMS encoder");
let sub = dec.last_substream.as_ref().unwrap();
let scaled = sub.tools.scaled_spec_primary.as_ref().unwrap();
assert!(scaled[0].abs() > 0.0, "DC bin must carry the injected tone");
}
#[test]
fn v2_encoder_mono_tone_at_440hz_has_spectral_peak_near_target() {
use crate::decoder::Ac4Decoder;
use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
let mut enc = Ac4ImsEncoder::new();
let (frame_bytes, actual_hz) = enc.encode_frame_mono_tone_at_hz(440.0);
assert!(
(actual_hz - 425.0).abs() < 1.0,
"expected ~425 Hz target, got {actual_hz}"
);
let params = CodecParameters::audio(CodecId::new("ac4"));
let mut dec = Ac4Decoder::new(¶ms);
let pkt = Packet::new(0, TimeBase::new(1, 48_000), frame_bytes);
dec.send_packet(&pkt).expect("send_packet");
let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
panic!("expected audio frame");
};
assert_eq!(af.samples, 1_920);
let sub = dec.last_substream.as_ref().unwrap();
let scaled = sub.tools.scaled_spec_primary.as_ref().unwrap();
let target_bin = 34usize;
assert!(
scaled[target_bin].abs() > 0.0,
"expected non-zero spectral coefficient at bin {target_bin}, got {}",
scaled[target_bin]
);
let samples_i16: Vec<i16> = af.data[0]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect();
let nonzero_count = samples_i16.iter().filter(|&&s| s != 0).count();
assert!(
nonzero_count > 100,
"expected non-silent PCM at 440 Hz, got {nonzero_count} non-zero samples"
);
}
#[test]
fn v2_encoder_decoder_roundtrip_emits_silent_frame() {
use crate::decoder::Ac4Decoder;
use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
let mut enc = Ac4ImsEncoder::new(); let frame_bytes = enc.encode_frame(64);
let params = CodecParameters::audio(CodecId::new("ac4"));
let mut dec = Ac4Decoder::new(¶ms);
let pkt = Packet::new(0, TimeBase::new(1, 48_000), frame_bytes);
dec.send_packet(&pkt).expect("send_packet");
let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
panic!("expected audio frame");
};
assert_eq!(af.samples, 1_920);
assert_eq!(af.data.len(), 1);
assert_eq!(af.data[0].len(), 1_920 * 2);
assert!(af.data[0].iter().all(|&b| b == 0));
}
fn encode_decode_frames(frames: &[Vec<f32>]) -> Vec<Vec<i16>> {
use crate::decoder::Ac4Decoder;
use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
let params = CodecParameters::audio(CodecId::new("ac4"));
let mut dec = Ac4Decoder::new(¶ms);
let mut enc = Ac4ImsEncoder::new(); let mut out: Vec<Vec<i16>> = Vec::with_capacity(frames.len());
for (idx, f) in frames.iter().enumerate() {
let bytes = enc.encode_frame_pcm(f);
let _ = idx;
let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
dec.send_packet(&pkt).expect("send_packet");
let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
panic!("expected audio frame");
};
assert_eq!(af.samples, 1_920);
assert_eq!(af.data.len(), 1);
let pcm: Vec<i16> = af.data[0]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect();
out.push(pcm);
}
out
}
#[test]
fn encode_frame_pcm_1khz_tone_round_trips_with_spectral_peak() {
let n = 1920usize;
let fs = 48_000.0_f32;
let f = 1000.0_f32;
let make_frame = |start: usize| -> Vec<f32> {
(0..n)
.map(|i| {
let t = (start + i) as f32 / fs;
0.3 * (2.0 * std::f32::consts::PI * f * t).sin()
})
.collect()
};
let frames: Vec<Vec<f32>> = (0..4).map(|i| make_frame(i * n)).collect();
let decoded = encode_decode_frames(&frames);
let pcm = &decoded[2];
let nonzero = pcm.iter().filter(|&&s| s != 0).count();
assert!(
nonzero > 100,
"expected non-silent PCM from 1 kHz tone, got {nonzero} non-zero samples"
);
let peak = pcm.iter().map(|&s| s.abs()).max().unwrap_or(0);
assert!(peak > 1000, "expected peak amplitude > 1000, got {peak}");
}
#[test]
fn encode_frame_pcm_multitone_round_trips_with_positive_snr() {
let n = 1920usize;
let fs = 48_000.0_f32;
let make_frame = |start: usize| -> Vec<f32> {
(0..n)
.map(|i| {
let t = (start + i) as f32 / fs;
let pi2 = 2.0 * std::f32::consts::PI;
0.2 * (pi2 * 250.0 * t).sin()
+ 0.2 * (pi2 * 500.0 * t).sin()
+ 0.2 * (pi2 * 1000.0 * t).sin()
})
.collect()
};
let frames: Vec<Vec<f32>> = (0..5).map(|i| make_frame(i * n)).collect();
let decoded = encode_decode_frames(&frames);
let orig = &frames[2];
let recon_i16 = &decoded[2];
let recon: Vec<f32> = recon_i16.iter().map(|&s| s as f32 / 32767.0).collect();
let mut sig_e = 0.0_f64;
let mut err_e = 0.0_f64;
for (o, r) in orig.iter().zip(recon.iter()) {
sig_e += (*o as f64).powi(2);
err_e += (*o as f64 - *r as f64).powi(2);
}
let snr_db = 10.0 * (sig_e / err_e.max(1e-30)).log10();
assert!(
snr_db > 10.0,
"multi-tone round-trip SNR too low: {snr_db:.1} dB \
(expected > 10 dB; HCB5-only encoder caps q at ±4 — \
round 49 will widen the codebook selector)"
);
}
#[test]
fn encode_frame_pcm_silence_round_trips_to_silence() {
let n = 1920usize;
let frames: Vec<Vec<f32>> = (0..4).map(|_| vec![0.0_f32; n]).collect();
let decoded = encode_decode_frames(&frames);
let pcm = &decoded[2];
let peak = pcm.iter().map(|&s| s.abs()).max().unwrap_or(0);
assert!(
peak < 50,
"expected silent reconstruction, got peak amplitude {peak}"
);
}
#[test]
fn encode_frame_pcm_bumps_sequence_counter() {
let mut enc = Ac4ImsEncoder::new();
assert_eq!(enc.sequence_counter, 0);
let frame = vec![0.0_f32; 1920];
let _ = enc.encode_frame_pcm(&frame);
assert_eq!(enc.sequence_counter, 1);
let _ = enc.encode_frame_pcm(&frame);
assert_eq!(enc.sequence_counter, 2);
}
fn encode_decode_frames_with_max_sfb(frames: &[Vec<f32>], max_sfb: u32) -> Vec<Vec<i16>> {
use crate::decoder::Ac4Decoder;
use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
let params = CodecParameters::audio(CodecId::new("ac4"));
let mut dec = Ac4Decoder::new(¶ms);
let mut enc = Ac4ImsEncoder::new();
let mut out: Vec<Vec<i16>> = Vec::with_capacity(frames.len());
for f in frames {
let bytes = enc.encode_frame_pcm_with_max_sfb(f, max_sfb);
let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
dec.send_packet(&pkt).expect("send_packet");
let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
panic!("expected audio frame");
};
assert_eq!(af.samples, 1_920);
assert_eq!(af.data.len(), 1);
let pcm: Vec<i16> = af.data[0]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect();
out.push(pcm);
}
out
}
#[test]
fn encode_frame_pcm_white_noise_snr_exceeds_hcb5_only_ceiling() {
use crate::decoder::Ac4Decoder;
use crate::encoder_mdct::EncoderMdctState;
use oxideav_core::{CodecId, CodecParameters, Decoder, Packet, TimeBase};
let n = 1920usize;
let max_sfb = 50u32;
let sfbo = crate::sfb_offset::sfb_offset_48(n as u32).unwrap();
let end_bin = sfbo[max_sfb as usize] as usize;
let make_frame = |seed_off: u64| -> Vec<f32> {
let mut s: u64 = 0xACE4_u64.wrapping_add(seed_off);
(0..n)
.map(|_| {
s = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let u = (s >> 33) as u32;
(u as f32 / (1u32 << 31) as f32 - 1.0) * 0.3
})
.collect()
};
let frames: Vec<Vec<f32>> = (0..3).map(|i| make_frame(i as u64 * n as u64)).collect();
let params = CodecParameters::audio(CodecId::new("ac4"));
let mut dec = Ac4Decoder::new(¶ms);
let mut enc = Ac4ImsEncoder::new();
let mut last_recon_spec: Option<Vec<f32>> = None;
let mut mdct_in = EncoderMdctState::new(n as u32);
let mut last_orig_spec: Option<Vec<f32>> = None;
for f in &frames {
let orig_coeffs = mdct_in.analyse_frame(f);
last_orig_spec = Some(orig_coeffs.clone());
let bytes = enc.encode_frame_pcm_with_max_sfb(f, max_sfb);
let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
dec.send_packet(&pkt).expect("send_packet");
let _ = dec.receive_frame().expect("receive_frame");
let sub = dec.last_substream.as_ref().unwrap();
let scaled = sub.tools.scaled_spec_primary.as_ref().unwrap().clone();
last_recon_spec = Some(scaled);
}
let orig = last_orig_spec.unwrap();
let recon = last_recon_spec.unwrap();
let mut sig_e = 0.0_f64;
let mut err_e = 0.0_f64;
for k in 0..end_bin {
let o = orig[k] as f64;
let r = recon[k] as f64;
sig_e += o * o;
err_e += (o - r) * (o - r);
}
let snr_db = 10.0 * (sig_e / err_e.max(1e-30)).log10();
eprintln!("ROUND-49 white-noise spectral SNR (HCB1..11 optimiser, q_target=12, max_sfb=50): {snr_db:.1} dB");
assert!(
snr_db > 18.0,
"white-noise spectral SNR did not improve over HCB5-only ceiling: \
{snr_db:.1} dB (expected > 18 dB; round-48 HCB5-only baseline was ~12 dB)"
);
}
#[test]
fn encode_frame_pcm_max_sfb_55_preserves_tone_energy() {
let n = 1920usize;
let fs = 48_000.0_f32;
let f = 1000.0_f32;
let make_frame = |start: usize| -> Vec<f32> {
(0..n)
.map(|i| {
let t = (start + i) as f32 / fs;
0.3 * (2.0 * std::f32::consts::PI * f * t).sin()
})
.collect()
};
let frames: Vec<Vec<f32>> = (0..4).map(|i| make_frame(i * n)).collect();
let decoded = encode_decode_frames_with_max_sfb(&frames, 55);
let orig = &frames[2];
let recon_i16 = &decoded[2];
let recon: Vec<f32> = recon_i16.iter().map(|&s| s as f32 / 32767.0).collect();
let orig_e: f64 = orig.iter().map(|&v| (v as f64).powi(2)).sum();
let recon_e: f64 = recon.iter().map(|&v| (v as f64).powi(2)).sum();
let ratio = recon_e / orig_e.max(1e-30);
eprintln!(
"ROUND-49 max_sfb=55 1 kHz tone energy preservation: {:.1}%",
ratio * 100.0
);
assert!(
ratio >= 0.80,
"expected ≥80% energy preservation at max_sfb=55, got {:.1}%",
ratio * 100.0
);
}
#[test]
fn encode_frame_pcm_default_max_sfb_still_works() {
let n = 1920usize;
let fs = 48_000.0_f32;
let f = 1000.0_f32;
let make_frame = |start: usize| -> Vec<f32> {
(0..n)
.map(|i| {
let t = (start + i) as f32 / fs;
0.3 * (2.0 * std::f32::consts::PI * f * t).sin()
})
.collect()
};
let frames: Vec<Vec<f32>> = (0..4).map(|i| make_frame(i * n)).collect();
let decoded = encode_decode_frames(&frames); let pcm = &decoded[2];
let peak = pcm.iter().map(|&s| s.abs()).max().unwrap_or(0);
assert!(
peak > 1000,
"expected peak amplitude > 1000 at default max_sfb=40, got {peak}"
);
}
#[test]
fn baseline_hcb5_only_white_noise_snr_logs_for_comparison() {
use crate::asf_data::{
dequantise_and_scale, parse_asf_scalefac_data, parse_asf_section_data,
parse_asf_spectral_data,
};
use crate::encoder_asf::{
pick_scalefactor_for_band, single_section, write_scalefac_data, write_sect_len_incr,
write_spectral_data_single_section,
};
use crate::encoder_mdct::EncoderMdctState;
use oxideav_core::bits::{BitReader, BitWriter};
let n = 1920usize;
let max_sfb = 50u32;
let sfbo = crate::sfb_offset::sfb_offset_48(n as u32).unwrap();
let end_bin = sfbo[max_sfb as usize] as usize;
let mut s: u64 = 0xACE4u64;
let pcm: Vec<f32> = (0..n)
.map(|_| {
s = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let u = (s >> 33) as u32;
(u as f32 / (1u32 << 31) as f32 - 1.0) * 0.3
})
.collect();
let mut mdct = EncoderMdctState::new(n as u32);
let _ = mdct.analyse_frame(&pcm);
let coeffs = mdct.analyse_frame(&pcm);
let cb: u8 = 5;
let q_max = 4u32;
let mut qspec = vec![0i32; end_bin];
let mut sf_per_band = vec![100i32; max_sfb as usize];
let mut max_quant_idx = vec![0u32; max_sfb as usize];
for sfb in 0..max_sfb as usize {
let a = sfbo[sfb] as usize;
let b = sfbo[sfb + 1] as usize;
let band = &coeffs[a..b.min(coeffs.len())];
let (sf, q) = pick_scalefactor_for_band(band, q_max);
sf_per_band[sfb] = sf;
for (i, &qi) in q.iter().enumerate() {
qspec[a + i] = qi;
max_quant_idx[sfb] = max_quant_idx[sfb].max(qi.unsigned_abs());
}
}
let mut bw = BitWriter::new();
bw.write_u32(4096, 15);
bw.write_bit(false);
bw.align_to_byte();
bw.write_u32(0, 1);
bw.write_u32(0, 1);
bw.write_bit(true);
let (n_msfb_bits, _, _) = crate::tables::n_msfb_bits_48(n as u32).unwrap();
bw.write_u32(max_sfb, n_msfb_bits);
bw.write_u32(cb as u32, 4);
write_sect_len_incr(&mut bw, max_sfb, 3, 7);
write_spectral_data_single_section(&mut bw, &qspec, sfbo, max_sfb, cb as u32);
let sections = single_section(max_sfb, cb);
write_scalefac_data(
&mut bw,
&sf_per_band,
§ions.sfb_cb,
&max_quant_idx,
max_sfb,
);
bw.write_u32(0, 1);
bw.align_to_byte();
while bw.byte_len() < 4096 {
bw.write_u32(0, 8);
}
let body = bw.finish();
let mut br = BitReader::new(&body);
let _ = br.read_u32(15).unwrap();
let _ = br.read_bit().unwrap();
br.align_to_byte();
let _ = br.read_u32(1).unwrap();
let _ = br.read_u32(1).unwrap();
let _ = br.read_bit().unwrap();
let _ = br.read_u32(n_msfb_bits).unwrap();
let parsed = parse_asf_section_data(&mut br, 0, n as u32, max_sfb).unwrap();
let (qs, mqi) = parse_asf_spectral_data(&mut br, &parsed, sfbo, max_sfb).unwrap();
let sfg = parse_asf_scalefac_data(&mut br, &parsed, &mqi, max_sfb, n as u32).unwrap();
let scaled = dequantise_and_scale(&qs, &sfg, sfbo, max_sfb);
let mut sig_e = 0.0_f64;
let mut err_e = 0.0_f64;
for k in 0..end_bin {
let o = coeffs[k] as f64;
let r = scaled[k] as f64;
sig_e += o * o;
err_e += (o - r) * (o - r);
}
let snr_db = 10.0 * (sig_e / err_e.max(1e-30)).log10();
eprintln!("ROUND-48 baseline white-noise spectral SNR (HCB5-only, q_target=4, max_sfb=50): {snr_db:.1} dB");
assert!(
snr_db < 18.0,
"round-48 HCB5-only baseline unexpectedly high: {snr_db:.1} dB"
);
}
#[test]
fn encode_frame_pcm_white_noise_with_snf_fills_zero_quant_bands() {
use crate::decoder::Ac4Decoder;
use oxideav_core::{CodecId, CodecParameters, Decoder, Packet, TimeBase};
let n = 1920usize;
let max_sfb = 55u32;
let sfbo = crate::sfb_offset::sfb_offset_48(n as u32).unwrap();
let end_bin = sfbo[max_sfb as usize] as usize;
let make_frame = |seed_off: u64| -> Vec<f32> {
let mut s: u64 = 0xACE4_u64.wrapping_add(seed_off);
(0..n)
.map(|_| {
s = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let u = (s >> 33) as u32;
(u as f32 / (1u32 << 31) as f32 - 1.0) * 0.05 })
.collect()
};
let frames: Vec<Vec<f32>> = (0..3).map(|i| make_frame(i as u64 * n as u64)).collect();
let params = CodecParameters::audio(CodecId::new("ac4"));
let mut dec = Ac4Decoder::new(¶ms);
let mut enc = Ac4ImsEncoder::new();
let mut last_recon: Option<Vec<f32>> = None;
for f in &frames {
let bytes = enc.encode_frame_pcm_with_max_sfb(f, max_sfb);
let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
dec.send_packet(&pkt).expect("send_packet");
let _ = dec.receive_frame().expect("receive_frame");
let sub = dec.last_substream.as_ref().unwrap();
let scaled = sub.tools.scaled_spec_primary.as_ref().unwrap().clone();
last_recon = Some(scaled);
}
let recon = last_recon.unwrap();
let nonzero = recon[..end_bin].iter().filter(|&&v| v.abs() > 0.0).count();
assert!(
nonzero > 0,
"expected at least one non-zero bin in SNF reconstruction, got {nonzero}"
);
}
#[test]
fn encode_frame_pcm_silence_with_snf_off_round_trips() {
let n = 1920usize;
let frames: Vec<Vec<f32>> = (0..2).map(|_| vec![0.0_f32; n]).collect();
let decoded = encode_decode_frames_with_max_sfb(&frames, 50);
let pcm = &decoded[1];
let peak = pcm.iter().map(|&s| s.abs()).max().unwrap_or(0);
assert_eq!(
peak, 0,
"silence + SNF-off should decode to silence, peak={peak}"
);
}
#[test]
fn encode_frame_pcm_max_sfb_50_round_trips() {
let n = 1920usize;
let fs = 48_000.0_f32;
let make_frame = |start: usize| -> Vec<f32> {
(0..n)
.map(|i| {
let t = (start + i) as f32 / fs;
let pi2 = 2.0 * std::f32::consts::PI;
0.2 * (pi2 * 1000.0 * t).sin() + 0.2 * (pi2 * 8000.0 * t).sin()
})
.collect()
};
let frames: Vec<Vec<f32>> = (0..4).map(|i| make_frame(i * n)).collect();
let decoded = encode_decode_frames_with_max_sfb(&frames, 50);
let pcm = &decoded[2];
let nonzero = pcm.iter().filter(|&&s| s != 0).count();
assert!(nonzero > 100, "expected non-silent recon, got {nonzero}");
}
}