melpe-rs 0.1.2

MELPe vocoder (STANAG 4591) in pure Rust — 600 bps voice codec, no_std compatible
Documentation
/// MELPe 600 bps end-to-end loopback integration tests
///
/// These tests compose the Encoder and Decoder to verify
/// the full encode→decode pipeline works correctly.

use melpe::core_types::{
    SAMPLE_RATE, SUPERFRAME_SAMPLES, SUPERFRAME_BYTES_600, FRAME_SAMPLES,
};
use melpe::encoder::Encoder;
use melpe::decoder::Decoder;

// ── Helpers ──

fn sine_wave(freq: f32, amplitude: f32, n: usize) -> Vec<f32> {
    (0..n)
        .map(|i| {
            amplitude * (2.0 * std::f32::consts::PI * freq * i as f32 / SAMPLE_RATE as f32).sin()
        })
        .collect()
}

fn to_superframe_buf(samples: &[f32]) -> [f32; SUPERFRAME_SAMPLES] {
    let mut buf = [0.0f32; SUPERFRAME_SAMPLES];
    let n = samples.len().min(SUPERFRAME_SAMPLES);
    buf[..n].copy_from_slice(&samples[..n]);
    buf
}

fn rms(samples: &[f32]) -> f32 {
    if samples.is_empty() { return 0.0; }
    (samples.iter().map(|x| x * x).sum::<f32>() / samples.len() as f32).sqrt()
}

fn peak(samples: &[f32]) -> f32 {
    samples.iter().fold(0.0f32, |m, &s| m.max(s.abs()))
}

/// Encode then decode a superframe buffer, returning the reconstructed audio.
fn loopback(
    enc: &mut Encoder,
    dec: &mut Decoder,
    input: &[f32; SUPERFRAME_SAMPLES],
) -> [f32; SUPERFRAME_SAMPLES] {
    let mut bitstream = [0u8; SUPERFRAME_BYTES_600];
    let mut output = [0.0f32; SUPERFRAME_SAMPLES];
    enc.encode(input, &mut bitstream);
    dec.decode(&bitstream, &mut output);
    output
}

// ── Basic loopback tests ──

#[test]
fn test_loopback_sine_produces_output() {
    let mut enc = Encoder::new();
    let mut dec = Decoder::new();
    let input = to_superframe_buf(&sine_wave(200.0, 0.5, SUPERFRAME_SAMPLES));

    let output = loopback(&mut enc, &mut dec, &input);

    // Output must be finite
    assert!(output.iter().all(|x| x.is_finite()), "Output must be finite");

    // Output should have non-trivial energy
    let out_rms = rms(&output);
    assert!(
        out_rms > 1e-6,
        "Loopback of sine should produce output, rms={}",
        out_rms
    );
}

#[test]
fn test_loopback_silence() {
    let mut enc = Encoder::new();
    let mut dec = Decoder::new();
    let input = [0.0f32; SUPERFRAME_SAMPLES];

    let output = loopback(&mut enc, &mut dec, &input);

    assert!(output.iter().all(|x| x.is_finite()), "Output must be finite");

    // Silence in → should be very quiet out
    let out_rms = rms(&output);
    assert!(
        out_rms < 0.1,
        "Silence loopback should be quiet, rms={}",
        out_rms
    );
}

#[test]
fn test_loopback_deterministic() {
    let input = to_superframe_buf(&sine_wave(150.0, 0.4, SUPERFRAME_SAMPLES));

    let mut enc1 = Encoder::new();
    let mut dec1 = Decoder::new();
    let out1 = loopback(&mut enc1, &mut dec1, &input);

    let mut enc2 = Encoder::new();
    let mut dec2 = Decoder::new();
    let out2 = loopback(&mut enc2, &mut dec2, &input);

    for i in 0..SUPERFRAME_SAMPLES {
        assert!(
            (out1[i] - out2[i]).abs() < 1e-6,
            "Determinism failed at [{}]: {} vs {}",
            i, out1[i], out2[i]
        );
    }
}

// ── Signal quality tests ──

#[test]
fn test_loopback_loud_vs_quiet() {
    let loud = to_superframe_buf(&sine_wave(200.0, 0.8, SUPERFRAME_SAMPLES));
    let quiet = to_superframe_buf(&sine_wave(200.0, 0.02, SUPERFRAME_SAMPLES));

    let mut enc = Encoder::new();
    let mut dec = Decoder::new();
    let out_loud = loopback(&mut enc, &mut dec, &loud);

    enc.reset();
    dec.reset();
    let out_quiet = loopback(&mut enc, &mut dec, &quiet);

    let rms_loud = rms(&out_loud);
    let rms_quiet = rms(&out_quiet);

    assert!(
        rms_loud > rms_quiet,
        "Louder input should produce louder output: loud_rms={}, quiet_rms={}",
        rms_loud, rms_quiet
    );
}

#[test]
fn test_loopback_different_freqs_differ() {
    let low = to_superframe_buf(&sine_wave(100.0, 0.5, SUPERFRAME_SAMPLES));
    let high = to_superframe_buf(&sine_wave(300.0, 0.5, SUPERFRAME_SAMPLES));

    let mut enc = Encoder::new();
    let mut dec = Decoder::new();
    let out_low = loopback(&mut enc, &mut dec, &low);

    enc.reset();
    dec.reset();
    let out_high = loopback(&mut enc, &mut dec, &high);

    // Outputs should differ meaningfully
    let diff: f32 = out_low.iter().zip(out_high.iter())
        .map(|(a, b)| (a - b).abs())
        .sum::<f32>() / SUPERFRAME_SAMPLES as f32;

    assert!(
        diff > 1e-6,
        "Different frequencies should produce different output, avg_diff={}",
        diff
    );
}

// ── Multi-superframe continuity ──

#[test]
fn test_loopback_multi_superframe_stable() {
    let mut enc = Encoder::new();
    let mut dec = Decoder::new();
    let samples = sine_wave(180.0, 0.5, SUPERFRAME_SAMPLES * 10);

    let mut prev_peak = 0.0f32;
    for i in 0..10 {
        let start = i * SUPERFRAME_SAMPLES;
        let input = to_superframe_buf(&samples[start..start + SUPERFRAME_SAMPLES]);
        let output = loopback(&mut enc, &mut dec, &input);

        assert!(
            output.iter().all(|x| x.is_finite()),
            "Output not finite at superframe {}",
            i
        );

        let p = peak(&output);
        assert!(
            p < 50.0,
            "Output blowing up at superframe {}: peak={}",
            i, p
        );
    }
}

#[test]
fn test_loopback_multi_superframe_no_clicks() {
    // Encode/decode a continuous signal and check for discontinuities
    // at superframe boundaries
    let mut enc = Encoder::new();
    let mut dec = Decoder::new();
    let samples = sine_wave(150.0, 0.5, SUPERFRAME_SAMPLES * 4);

    let mut all_output = Vec::with_capacity(SUPERFRAME_SAMPLES * 4);

    for i in 0..4 {
        let start = i * SUPERFRAME_SAMPLES;
        let input = to_superframe_buf(&samples[start..start + SUPERFRAME_SAMPLES]);
        let output = loopback(&mut enc, &mut dec, &input);
        all_output.extend_from_slice(&output);
    }

    // Check for large jumps at superframe boundaries (skip the first boundary
    // since there's a cold-start transient)
    for boundary in 1..3 {
        let idx = boundary * SUPERFRAME_SAMPLES;
        let jump = (all_output[idx] - all_output[idx - 1]).abs();
        let local_rms = rms(&all_output[idx - 10..idx + 10]);

        // A "click" would be a jump much larger than the local signal level
        if local_rms > 1e-4 {
            let jump_ratio = jump / local_rms;
            assert!(
                jump_ratio < 20.0,
                "Possible click at boundary {}: jump={}, local_rms={}, ratio={}",
                boundary, jump, local_rms, jump_ratio
            );
        }
    }
}

// ── Bitstream-level tests ──

#[test]
fn test_loopback_bitstream_not_constant() {
    // Encoding a time-varying signal should produce different bitstreams
    // for consecutive superframes
    let mut enc = Encoder::new();
    let samples = sine_wave(200.0, 0.5, SUPERFRAME_SAMPLES * 2);

    // Add a frequency shift in the second superframe
    let mut samples2 = samples.clone();
    for i in SUPERFRAME_SAMPLES..SUPERFRAME_SAMPLES * 2 {
        samples2[i] = 0.5 * (2.0 * std::f32::consts::PI * 350.0 * i as f32 / SAMPLE_RATE as f32).sin();
    }

    let mut bs1 = [0u8; SUPERFRAME_BYTES_600];
    let mut bs2 = [0u8; SUPERFRAME_BYTES_600];

    let input1 = to_superframe_buf(&samples2[..SUPERFRAME_SAMPLES]);
    let input2 = to_superframe_buf(&samples2[SUPERFRAME_SAMPLES..]);

    enc.encode(&input1, &mut bs1);
    enc.encode(&input2, &mut bs2);

    assert_ne!(
        bs1, bs2,
        "Different signal content should produce different bitstreams"
    );
}

#[test]
fn test_loopback_6_bytes_sufficient() {
    // The entire codec state for one superframe fits in 6 bytes
    let mut enc = Encoder::new();
    let input = to_superframe_buf(&sine_wave(200.0, 0.5, SUPERFRAME_SAMPLES));
    let mut bitstream = [0u8; SUPERFRAME_BYTES_600];

    enc.encode(&input, &mut bitstream);

    // Two independent decoders with same bitstream should produce same output
    let mut dec1 = Decoder::new();
    let mut dec2 = Decoder::new();
    let mut out1 = [0.0f32; SUPERFRAME_SAMPLES];
    let mut out2 = [0.0f32; SUPERFRAME_SAMPLES];

    dec1.decode(&bitstream, &mut out1);
    dec2.decode(&bitstream, &mut out2);

    for i in 0..SUPERFRAME_SAMPLES {
        assert!(
            (out1[i] - out2[i]).abs() < 1e-6,
            "Two decoders should match at [{}]",
            i
        );
    }
}

// ── Edge cases ──

#[test]
fn test_loopback_dc_signal() {
    let mut enc = Encoder::new();
    let mut dec = Decoder::new();
    let input = [0.3f32; SUPERFRAME_SAMPLES];

    let output = loopback(&mut enc, &mut dec, &input);

    assert!(output.iter().all(|x| x.is_finite()), "DC output must be finite");
}

#[test]
fn test_loopback_full_scale() {
    let mut enc = Encoder::new();
    let mut dec = Decoder::new();
    let input = to_superframe_buf(&sine_wave(200.0, 1.0, SUPERFRAME_SAMPLES));

    let output = loopback(&mut enc, &mut dec, &input);

    assert!(output.iter().all(|x| x.is_finite()), "Full-scale output must be finite");
    assert!(peak(&output) < 100.0, "Full-scale output should not explode");
}

#[test]
fn test_loopback_impulse() {
    let mut enc = Encoder::new();
    let mut dec = Decoder::new();
    let mut input = [0.0f32; SUPERFRAME_SAMPLES];
    input[0] = 1.0;

    let output = loopback(&mut enc, &mut dec, &input);

    assert!(output.iter().all(|x| x.is_finite()), "Impulse output must be finite");
}

#[test]
fn test_encoder_decoder_independent_reset() {
    // Verify that resetting encoder doesn't affect decoder and vice versa
    let input = to_superframe_buf(&sine_wave(200.0, 0.5, SUPERFRAME_SAMPLES));

    let mut enc = Encoder::new();
    let mut dec = Decoder::new();

    // First pass
    let out1 = loopback(&mut enc, &mut dec, &input);

    // Reset both, do again
    enc.reset();
    dec.reset();
    let out2 = loopback(&mut enc, &mut dec, &input);

    // Should match (both fully reset to initial state)
    for i in 0..SUPERFRAME_SAMPLES {
        assert!(
            (out1[i] - out2[i]).abs() < 1e-6,
            "Post-reset loopback should match at [{}]: {} vs {}",
            i, out1[i], out2[i]
        );
    }
}