use core::f32::consts::PI;
use crate::mbe_params::MbeParams;
use crate::imbe_wire::dequantize::{
DecodeError, DecoderState, EncodeError, PITCH_INDEX_MAX, dequantize, encode_pitch as full_encode_pitch,
pitch_decode as full_pitch_decode, quantize,
};
use crate::imbe_wire::frame::{decode_frame as decode_full, encode_frame as encode_full};
use crate::imbe_wire::priority::{
IMBE_B_MAX, L_MIN as FULL_L_MIN, prioritize as prioritize_full,
};
use crate::ambe_plus2_wire::dequantize::{
Decoded, DecoderState as HalfDecoderState, decode_to_params, encode_pitch as half_encode_pitch,
quantize as quantize_half,
};
use crate::ambe_plus2_wire::frame::{
AMBE_PITCH_TABLE, DIBITS_PER_FRAME, decode_frame as decode_half, encode_frame as encode_half,
};
use crate::ambe_plus2_wire::priority::{AMBE_B_COUNT, prioritize as prioritize_half};
use crate::rate_conversion::predictor::{CrossRatePredictorState, blend as cross_rate_blend};
fn ambe_plus2_omega_min() -> f32 {
AMBE_PITCH_TABLE[AMBE_PITCH_TABLE.len() - 1].omega_0
}
fn ambe_plus2_omega_max() -> f32 {
AMBE_PITCH_TABLE[0].omega_0
}
fn imbe_omega_min() -> f32 {
4.0 * PI / (f32::from(PITCH_INDEX_MAX) + 39.5)
}
fn imbe_omega_max() -> f32 {
4.0 * PI / 39.5
}
fn clamp_omega_to(params: &MbeParams, min: f32, max: f32) -> MbeParams {
let w = params.omega_0();
if w >= min && w <= max {
return params.clone();
}
let clamped = w.clamp(min, max);
MbeParams::new(
clamped,
params.harmonic_count(),
params.voiced_slice(),
params.amplitudes_slice(),
)
.expect("clamped ω₀ stays inside (0, π)")
}
const HALFRATE_ERASURE_B0: u16 = 120;
const FULLRATE_ERASURE_B0: u16 = PITCH_INDEX_MAX as u16 + 1;
fn ambe_plus2_erasure_dibits() -> [u8; DIBITS_PER_FRAME] {
let mut b = [0u16; AMBE_B_COUNT];
b[0] = HALFRATE_ERASURE_B0;
let u = prioritize_half(&b);
encode_half(&u)
}
fn imbe_erasure_dibits() -> [u8; 72] {
let mut b = [0u16; IMBE_B_MAX];
b[0] = FULLRATE_ERASURE_B0;
let u = prioritize_full(&b, FULL_L_MIN);
encode_full(&u)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConvertError {
Decode(DecodeError),
Encode(EncodeError),
HalfPitchOutOfRange,
}
impl From<DecodeError> for ConvertError {
fn from(e: DecodeError) -> Self { Self::Decode(e) }
}
impl From<EncodeError> for ConvertError {
fn from(e: EncodeError) -> Self { Self::Encode(e) }
}
#[derive(Clone, Debug)]
pub struct FullToHalfConverter {
decoder: DecoderState,
encoder: HalfDecoderState,
predictor: CrossRatePredictorState,
predictor_enabled: bool,
}
impl Default for FullToHalfConverter {
fn default() -> Self {
Self {
decoder: DecoderState::default(),
encoder: HalfDecoderState::default(),
predictor: CrossRatePredictorState::default(),
predictor_enabled: true,
}
}
}
impl FullToHalfConverter {
pub fn new() -> Self { Self::default() }
pub fn convert(&mut self, dibits: &[u8; 72]) -> Result<[u8; DIBITS_PER_FRAME], ConvertError> {
let frame = decode_full(dibits);
let params = match dequantize(&frame.info, &mut self.decoder) {
Ok(p) => p,
Err(DecodeError::BadPitch) => return Ok(ambe_plus2_erasure_dibits()),
Err(other) => return Err(ConvertError::Decode(other)),
};
let clamped = clamp_omega_to(¶ms, ambe_plus2_omega_min(), ambe_plus2_omega_max());
let blended = if self.predictor_enabled {
match half_encode_pitch(clamped.omega_0()) {
Some(b0) => {
let target_entry = AMBE_PITCH_TABLE[b0 as usize];
cross_rate_blend(
&clamped,
target_entry.omega_0,
target_entry.l,
&mut self.predictor,
)
}
None => clamped,
}
} else {
clamped
};
let u = quantize_half(&blended, &mut self.encoder).map_err(|e| match e {
DecodeError::BadPitch => ConvertError::HalfPitchOutOfRange,
other => ConvertError::Decode(other),
})?;
Ok(encode_half(&u))
}
pub fn decoder_state(&self) -> &DecoderState { &self.decoder }
pub fn encoder_state(&self) -> &HalfDecoderState { &self.encoder }
pub fn predictor_state(&self) -> &CrossRatePredictorState { &self.predictor }
pub fn set_predictor_enabled(&mut self, enabled: bool) {
self.predictor_enabled = enabled;
}
}
#[derive(Clone, Debug)]
pub struct HalfToFullConverter {
decoder: HalfDecoderState,
encoder: DecoderState,
predictor: CrossRatePredictorState,
predictor_enabled: bool,
}
impl Default for HalfToFullConverter {
fn default() -> Self {
Self {
decoder: HalfDecoderState::default(),
encoder: DecoderState::default(),
predictor: CrossRatePredictorState::default(),
predictor_enabled: true,
}
}
}
impl HalfToFullConverter {
pub fn new() -> Self { Self::default() }
pub fn convert(&mut self, dibits: &[u8; DIBITS_PER_FRAME]) -> Result<[u8; 72], ConvertError> {
let frame = decode_half(dibits);
let params = match decode_to_params(&frame.info, &mut self.decoder) {
Ok(Decoded::Voice(p)) => p,
Ok(Decoded::Tone { params, .. }) => params,
Ok(Decoded::Erasure) => return Ok(imbe_erasure_dibits()),
Err(_) => return Ok(imbe_erasure_dibits()),
};
let clamped = clamp_omega_to(¶ms, imbe_omega_min(), imbe_omega_max());
let blended = if self.predictor_enabled {
match full_encode_pitch(clamped.omega_0()) {
Some(b0) => match full_pitch_decode(b0) {
Some(target_entry) => cross_rate_blend(
&clamped,
target_entry.omega_0,
target_entry.l,
&mut self.predictor,
),
None => clamped,
},
None => clamped,
}
} else {
clamped
};
let l = blended.harmonic_count();
let b = quantize(&blended, &mut self.encoder)?;
let u = prioritize_full(&b, l);
Ok(encode_full(&u))
}
pub fn decoder_state(&self) -> &HalfDecoderState { &self.decoder }
pub fn encoder_state(&self) -> &DecoderState { &self.encoder }
pub fn predictor_state(&self) -> &CrossRatePredictorState { &self.predictor }
pub fn set_predictor_enabled(&mut self, enabled: bool) {
self.predictor_enabled = enabled;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mbe_params::MbeParams;
fn sample_params(omega_0: f32) -> MbeParams {
let l = MbeParams::harmonic_count_for(omega_0);
let voiced: Vec<bool> = (1..=l).map(|h| h <= l / 2).collect();
let amps: Vec<f32> = (1..=l)
.map(|h| 100.0 * (-(h as f32) * 0.05).exp())
.collect();
MbeParams::new(omega_0, l, &voiced, &s).unwrap()
}
fn imbe_dibits_from(params: &MbeParams, state: &mut DecoderState) -> [u8; 72] {
let l = params.harmonic_count();
let b = quantize(params, state).expect("quantize imbe");
let u = prioritize_full(&b, l);
encode_full(&u)
}
fn ambe_plus2_dibits_from(
params: &MbeParams,
state: &mut HalfDecoderState,
) -> [u8; DIBITS_PER_FRAME] {
let u = quantize_half(params, state).expect("quantize ambe_plus2");
encode_half(&u)
}
fn assert_valid_dibits<const N: usize>(dibits: &[u8; N]) {
for (i, d) in dibits.iter().enumerate() {
assert!(*d < 4, "dibit {i} = {d} is out of range");
}
}
#[test]
fn converters_construct() {
let _ = FullToHalfConverter::new();
let _ = HalfToFullConverter::new();
}
#[test]
fn full_to_half_emits_valid_dibits() {
let mut src_state = DecoderState::new();
let mut conv = FullToHalfConverter::new();
let params = sample_params(0.20);
let input = imbe_dibits_from(¶ms, &mut src_state);
let out = conv.convert(&input).expect("convert");
assert_valid_dibits(&out);
}
#[test]
fn half_to_full_emits_valid_dibits() {
let mut src_state = HalfDecoderState::new();
let mut conv = HalfToFullConverter::new();
let params = sample_params(0.18);
let input = ambe_plus2_dibits_from(¶ms, &mut src_state);
let out = conv.convert(&input).expect("convert");
assert_valid_dibits(&out);
}
#[test]
fn cross_rate_round_trip_preserves_pitch_within_quantizer_grid() {
let mut src_state = DecoderState::new();
let mut a_to_b = FullToHalfConverter::new();
let mut b_to_a = HalfToFullConverter::new();
let mut sink_state = DecoderState::new();
let params = sample_params(0.20);
let mut last_omega = 0f32;
for _ in 0..6 {
let a = imbe_dibits_from(¶ms, &mut src_state);
let b = a_to_b.convert(&a).expect("A→B");
let a2 = b_to_a.convert(&b).expect("B→A");
let frame = decode_full(&a2);
let back = dequantize(&frame.info, &mut sink_state).expect("decode");
last_omega = back.omega_0();
}
let rel = (last_omega - params.omega_0()).abs() / params.omega_0();
assert!(rel < 0.05, "ω₀ drift {rel:.4} exceeds 5%");
}
#[test]
fn half_grid_minimum_round_trips_without_rejection() {
let mut src_state = HalfDecoderState::new();
let l = 56u8;
let voiced: Vec<bool> = (1..=l).map(|h| h <= l / 2).collect();
let amps: Vec<f32> = (1..=l)
.map(|h| 100.0 * (-(h as f32) * 0.05).exp())
.collect();
let params =
MbeParams::new(ambe_plus2_omega_min(), l, &voiced, &s).unwrap();
let mut a_to_b = HalfToFullConverter::new();
let mut b_to_a = FullToHalfConverter::new();
for _ in 0..4 {
let a = ambe_plus2_dibits_from(¶ms, &mut src_state);
let b = a_to_b.convert(&a).expect("half → full");
let _ = b_to_a.convert(&b).expect("full → half (was failing)");
}
}
#[test]
fn full_grid_maximum_round_trips_without_rejection() {
let mut src_state = DecoderState::new();
let params = MbeParams::silence(); let mut conv = FullToHalfConverter::new();
for _ in 0..4 {
let a = imbe_dibits_from(¶ms, &mut src_state);
let _ = conv.convert(&a).expect("full → half at grid top");
}
}
#[test]
fn clamp_is_no_op_for_in_range_omega() {
let params = sample_params(0.20);
let clamped = clamp_omega_to(¶ms, 0.1, 0.3);
assert_eq!(clamped.omega_0(), params.omega_0());
assert_eq!(clamped.harmonic_count(), params.harmonic_count());
}
#[test]
fn clamp_snaps_to_the_nearest_boundary() {
let below = sample_params(0.10);
let snapped = clamp_omega_to(&below, 0.15, 0.25);
assert!((snapped.omega_0() - 0.15).abs() < 1e-6);
assert_eq!(snapped.harmonic_count(), below.harmonic_count());
let above = sample_params(0.30);
let snapped = clamp_omega_to(&above, 0.15, 0.25);
assert!((snapped.omega_0() - 0.25).abs() < 1e-6);
}
use crate::ambe_plus2_wire::dequantize::{FrameKind, classify_ambe_plus2_frame};
fn ambe_plus2_erasure_input() -> [u8; DIBITS_PER_FRAME] {
ambe_plus2_erasure_dibits()
}
fn ambe_plus2_tone_input() -> [u8; DIBITS_PER_FRAME] {
let mut u = [0u16; 4];
u[0] = 0x3F << 6; u[3] = 5u16 << 5; encode_half(&u)
}
fn imbe_erasure_input() -> [u8; 72] {
imbe_erasure_dibits()
}
#[test]
fn ambe_plus2_erasure_dibits_round_trip_as_erasure() {
let dibits = ambe_plus2_erasure_dibits();
let frame = decode_half(&dibits);
assert_eq!(classify_ambe_plus2_frame(&frame.info), FrameKind::Erasure);
}
#[test]
fn imbe_erasure_dibits_round_trip_as_bad_pitch() {
let dibits = imbe_erasure_dibits();
let frame = decode_full(&dibits);
let mut state = DecoderState::new();
match dequantize(&frame.info, &mut state) {
Err(DecodeError::BadPitch) => {}
other => panic!("expected BadPitch, got {other:?}"),
}
}
#[test]
fn full_to_half_emits_erasure_on_reserved_pitch() {
let mut conv = FullToHalfConverter::new();
let input = imbe_erasure_input();
let out = conv.convert(&input).expect("convert");
let frame = decode_half(&out);
assert_eq!(classify_ambe_plus2_frame(&frame.info), FrameKind::Erasure);
}
#[test]
fn half_to_full_emits_erasure_on_erasure_input() {
let mut conv = HalfToFullConverter::new();
let input = ambe_plus2_erasure_input();
let out = conv.convert(&input).expect("convert");
let frame = decode_full(&out);
let mut sink = DecoderState::new();
match dequantize(&frame.info, &mut sink) {
Err(DecodeError::BadPitch) => {}
other => panic!("expected BadPitch on re-decode, got {other:?}"),
}
}
#[test]
fn half_to_full_encodes_tone_as_voice() {
let mut conv = HalfToFullConverter::new();
let input = ambe_plus2_tone_input();
let out = conv.convert(&input).expect("convert tone");
let frame = decode_full(&out);
let mut sink = DecoderState::new();
let params = dequantize(&frame.info, &mut sink).expect("tone → voice");
assert!(params.harmonic_count() >= 9, "tone yielded L={}", params.harmonic_count());
}
#[test]
fn half_to_full_erasure_is_idempotent() {
let mut conv = HalfToFullConverter::new();
let a = conv.convert(&ambe_plus2_erasure_input()).unwrap();
let b = conv.convert(&ambe_plus2_erasure_input()).unwrap();
assert_eq!(a, b, "erasure output should be deterministic");
}
#[test]
fn full_to_half_erasure_is_idempotent() {
let mut conv = FullToHalfConverter::new();
let a = conv.convert(&imbe_erasure_input()).unwrap();
let b = conv.convert(&imbe_erasure_input()).unwrap();
assert_eq!(a, b, "erasure output should be deterministic");
}
#[test]
fn cross_rate_predictor_state_tracks_target_across_frames() {
let mut src_state = DecoderState::new();
let mut conv = FullToHalfConverter::new();
let initial_omega_prev = conv.predictor_state().omega_0_prev();
let initial_l_prev = conv.predictor_state().l_prev();
assert_eq!(initial_l_prev, 30);
let p1 = sample_params(0.20);
let a1 = imbe_dibits_from(&p1, &mut src_state);
let _ = conv.convert(&a1).unwrap();
assert_ne!(
conv.predictor_state().omega_0_prev(),
initial_omega_prev,
"predictor ω_prev should have advanced from cold-start init"
);
let omega_after_f1 = conv.predictor_state().omega_0_prev();
let _l_after_f1 = conv.predictor_state().l_prev();
let p2 = sample_params(0.25);
let a2 = imbe_dibits_from(&p2, &mut src_state);
let _ = conv.convert(&a2).unwrap();
let omega_after_f2 = conv.predictor_state().omega_0_prev();
assert!(
(omega_after_f2 - omega_after_f1).abs() > 1e-4,
"frame 2 should shift predictor ω_prev (observed Δ = {})",
(omega_after_f2 - omega_after_f1).abs()
);
let found = AMBE_PITCH_TABLE.iter().any(|entry| {
(entry.omega_0 - omega_after_f2).abs() < 1e-6
&& entry.l == conv.predictor_state().l_prev()
});
assert!(
found,
"predictor ω_prev/l_prev = ({omega_after_f2}, {}) must be an AMBE_PITCH_TABLE entry",
conv.predictor_state().l_prev(),
);
let e = imbe_erasure_input();
let before_omega = conv.predictor_state().omega_0_prev();
let before_l = conv.predictor_state().l_prev();
let _ = conv.convert(&e).unwrap();
assert_eq!(
conv.predictor_state().omega_0_prev(),
before_omega,
"erasure must not advance predictor ω_prev"
);
assert_eq!(
conv.predictor_state().l_prev(),
before_l,
"erasure must not advance predictor l_prev"
);
}
#[test]
fn set_predictor_enabled_false_skips_blend() {
let mut src_state = DecoderState::new();
let mut conv = FullToHalfConverter::new();
conv.set_predictor_enabled(false);
let initial_omega = conv.predictor_state().omega_0_prev();
let initial_l = conv.predictor_state().l_prev();
let p = sample_params(0.18);
let a = imbe_dibits_from(&p, &mut src_state);
let _ = conv.convert(&a).unwrap();
assert_eq!(
conv.predictor_state().omega_0_prev(),
initial_omega,
"predictor should not advance when disabled"
);
assert_eq!(
conv.predictor_state().l_prev(),
initial_l,
"predictor l_prev should not advance when disabled"
);
}
#[test]
fn cross_rate_predictor_is_state_separate_from_decoder_and_encoder() {
let conv = FullToHalfConverter::new();
let _decoder = conv.decoder_state();
let _encoder = conv.encoder_state();
let _predictor = conv.predictor_state();
}
}