shravan 1.0.0

shravan — Audio codecs: WAV, FLAC, AIFF, Ogg, MP3, Opus, PCM conversion, and resampling
Documentation
//! Reference implementation tests.
//!
//! These tests decode files generated by ffmpeg (a reference audio tool)
//! and validate that shravan produces correct output.

// Reference files: 440 Hz sine wave, 0.1 seconds duration.

#[cfg(feature = "wav")]
mod wav_reference {
    const REF_16BIT_MONO: &[u8] = include_bytes!("reference/ref_44100_16bit_mono.wav");
    const REF_24BIT_MONO: &[u8] = include_bytes!("reference/ref_48000_24bit_mono.wav");
    const REF_16BIT_STEREO: &[u8] = include_bytes!("reference/ref_44100_16bit_stereo.wav");
    const REF_F32_MONO: &[u8] = include_bytes!("reference/ref_44100_f32_mono.wav");

    #[test]
    fn wav_16bit_mono_format() {
        let (info, samples) = shravan::wav::decode(REF_16BIT_MONO).unwrap();
        assert_eq!(info.format, shravan::AudioFormat::Wav);
        assert_eq!(info.sample_rate, 44100);
        assert_eq!(info.channels, 1);
        assert_eq!(info.bit_depth, 16);
        assert_eq!(samples.len(), 4410); // 0.1s * 44100
        assert!(samples.iter().all(|s| s.is_finite()));
        assert!(samples.iter().all(|s| s.abs() <= 1.0));
        // Verify it's a sine wave: check RMS is reasonable for a full-scale sine
        let rms = rms(&samples);
        // ffmpeg sine filter default amplitude is low (~0.088 RMS)
        assert!(rms > 0.05, "RMS {rms} too low for a sine wave");
        assert!(rms < 0.9, "RMS {rms} too high");
    }

    #[test]
    fn wav_24bit_mono_format() {
        let (info, samples) = shravan::wav::decode(REF_24BIT_MONO).unwrap();
        assert_eq!(info.sample_rate, 48000);
        assert_eq!(info.bit_depth, 24);
        assert_eq!(samples.len(), 4800); // 0.1s * 48000
        let rms = rms(&samples);
        assert!(rms > 0.05 && rms < 0.9, "RMS {rms} out of range");
    }

    #[test]
    fn wav_16bit_stereo_format() {
        let (info, samples) = shravan::wav::decode(REF_16BIT_STEREO).unwrap();
        assert_eq!(info.channels, 2);
        assert_eq!(samples.len(), 8820); // 0.1s * 44100 * 2ch
    }

    #[test]
    fn wav_f32_mono_format() {
        let (info, samples) = shravan::wav::decode(REF_F32_MONO).unwrap();
        assert_eq!(info.bit_depth, 32);
        assert_eq!(samples.len(), 4410);
        let rms = rms(&samples);
        assert!(rms > 0.05 && rms < 0.9, "RMS {rms} out of range");
    }

    #[test]
    fn wav_cross_format_consistency() {
        // 16-bit and f32 mono should decode to the same signal (within quantization error)
        let (_, samples_16) = shravan::wav::decode(REF_16BIT_MONO).unwrap();
        let (_, samples_f32) = shravan::wav::decode(REF_F32_MONO).unwrap();
        assert_eq!(samples_16.len(), samples_f32.len());
        let max_diff: f32 = samples_16
            .iter()
            .zip(samples_f32.iter())
            .map(|(a, b)| (a - b).abs())
            .fold(0.0f32, f32::max);
        // 16-bit quantization error is ~1/32768 ≈ 0.00003
        assert!(
            max_diff < 0.001,
            "Max diff between 16-bit and f32: {max_diff}"
        );
    }

    #[cfg(all(feature = "wav", feature = "pcm"))]
    #[test]
    fn wav_encode_decode_matches_reference() {
        // Decode reference, re-encode with shravan, decode again, compare
        let (info, original) = shravan::wav::decode(REF_16BIT_MONO).unwrap();
        let re_encoded = shravan::wav::encode(
            &original,
            info.sample_rate,
            info.channels,
            shravan::pcm::PcmFormat::I16,
        )
        .unwrap();
        let (_, re_decoded) = shravan::wav::decode(&re_encoded).unwrap();
        assert_eq!(original.len(), re_decoded.len());
        for (a, b) in original.iter().zip(re_decoded.iter()) {
            // Re-encoding through i16 quantization introduces rounding
            assert!((a - b).abs() < 0.001, "Re-encode mismatch: {a} vs {b}");
        }
    }

    fn rms(samples: &[f32]) -> f32 {
        let sum: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum();
        libm::sqrt(sum / samples.len() as f64) as f32
    }
}

#[cfg(feature = "flac")]
mod flac_reference {
    const REF_16BIT_MONO: &[u8] = include_bytes!("reference/ref_44100_16bit_mono.flac");
    const REF_24BIT_MONO: &[u8] = include_bytes!("reference/ref_48000_24bit_mono.flac");
    const REF_16BIT_STEREO: &[u8] = include_bytes!("reference/ref_44100_16bit_stereo.flac");

    #[test]
    fn flac_16bit_mono_format() {
        let (info, samples) = shravan::flac::decode(REF_16BIT_MONO).unwrap();
        assert_eq!(info.format, shravan::AudioFormat::Flac);
        assert_eq!(info.sample_rate, 44100);
        assert_eq!(info.channels, 1);
        assert_eq!(info.bit_depth, 16);
        assert_eq!(samples.len(), 4410);
        assert!(samples.iter().all(|s| s.is_finite()));
        let rms = rms(&samples);
        assert!(rms > 0.05 && rms < 0.9, "RMS {rms} out of range");
    }

    #[test]
    fn flac_24bit_mono_format() {
        let (info, samples) = shravan::flac::decode(REF_24BIT_MONO).unwrap();
        assert_eq!(info.sample_rate, 48000);
        assert_eq!(info.bit_depth, 24);
        assert_eq!(samples.len(), 4800);
        let rms = rms(&samples);
        assert!(rms > 0.05 && rms < 0.9, "RMS {rms} out of range");
    }

    #[test]
    fn flac_16bit_stereo_format() {
        let (info, samples) = shravan::flac::decode(REF_16BIT_STEREO).unwrap();
        assert_eq!(info.channels, 2);
        assert_eq!(samples.len(), 8820);
    }

    #[test]
    fn flac_wav_cross_decode_consistency() {
        // Same source encoded as WAV and FLAC should produce identical samples
        // (FLAC is lossless, WAV is PCM — both from same ffmpeg source)
        let wav_data = include_bytes!("reference/ref_44100_16bit_mono.wav");
        let (_, wav_samples) = shravan::wav::decode(wav_data).unwrap();
        let (_, flac_samples) = shravan::flac::decode(REF_16BIT_MONO).unwrap();
        assert_eq!(wav_samples.len(), flac_samples.len());
        let max_diff: f32 = wav_samples
            .iter()
            .zip(flac_samples.iter())
            .map(|(a, b)| (a - b).abs())
            .fold(0.0f32, f32::max);
        // Both are 16-bit from the same source — should be identical
        assert!(max_diff < 0.0001, "WAV vs FLAC max diff: {max_diff}");
    }

    #[cfg(feature = "flac")]
    #[test]
    fn flac_encode_decode_roundtrip_vs_reference() {
        // Decode ffmpeg FLAC, re-encode with shravan, decode again, compare
        let (info, original) = shravan::flac::decode(REF_16BIT_MONO).unwrap();
        let re_encoded = shravan::flac::encode(
            &original,
            info.sample_rate,
            info.channels,
            info.bit_depth as u8,
        )
        .unwrap();
        let (_, re_decoded) = shravan::flac::decode(&re_encoded).unwrap();
        assert_eq!(original.len(), re_decoded.len());
        let max_diff: f32 = original
            .iter()
            .zip(re_decoded.iter())
            .map(|(a, b)| (a - b).abs())
            .fold(0.0f32, f32::max);
        assert!(
            max_diff < 0.0001,
            "FLAC re-encode roundtrip max diff: {max_diff}"
        );
    }

    fn rms(samples: &[f32]) -> f32 {
        let sum: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum();
        libm::sqrt(sum / samples.len() as f64) as f32
    }
}

#[cfg(feature = "aiff")]
mod aiff_reference {
    const REF_16BIT_MONO: &[u8] = include_bytes!("reference/ref_44100_16bit_mono.aiff");

    #[test]
    fn aiff_16bit_mono_format() {
        let (info, samples) = shravan::aiff::decode(REF_16BIT_MONO).unwrap();
        assert_eq!(info.format, shravan::AudioFormat::Aiff);
        assert_eq!(info.sample_rate, 44100);
        assert_eq!(info.channels, 1);
        assert_eq!(info.bit_depth, 16);
        assert_eq!(samples.len(), 4410);
        let rms = rms(&samples);
        assert!(rms > 0.05 && rms < 0.9, "RMS {rms} out of range");
    }

    #[test]
    fn aiff_wav_cross_decode_consistency() {
        let wav_data = include_bytes!("reference/ref_44100_16bit_mono.wav");
        let (_, wav_samples) = shravan::wav::decode(wav_data).unwrap();
        let (_, aiff_samples) = shravan::aiff::decode(REF_16BIT_MONO).unwrap();
        assert_eq!(wav_samples.len(), aiff_samples.len());
        let max_diff: f32 = wav_samples
            .iter()
            .zip(aiff_samples.iter())
            .map(|(a, b)| (a - b).abs())
            .fold(0.0f32, f32::max);
        assert!(max_diff < 0.0001, "WAV vs AIFF max diff: {max_diff}");
    }

    fn rms(samples: &[f32]) -> f32 {
        let sum: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum();
        libm::sqrt(sum / samples.len() as f64) as f32
    }
}

/// Auto-detect via codec::open should work with reference files.
#[cfg(feature = "wav")]
#[test]
fn codec_open_reference_wav() {
    let data = include_bytes!("reference/ref_44100_16bit_mono.wav");
    let (info, samples) = shravan::codec::open(data).unwrap();
    assert_eq!(info.format, shravan::AudioFormat::Wav);
    assert_eq!(samples.len(), 4410);
}

#[cfg(feature = "flac")]
#[test]
fn codec_open_reference_flac() {
    let data = include_bytes!("reference/ref_44100_16bit_mono.flac");
    let (info, samples) = shravan::codec::open(data).unwrap();
    assert_eq!(info.format, shravan::AudioFormat::Flac);
    assert_eq!(samples.len(), 4410);
}

#[cfg(feature = "aiff")]
#[test]
fn codec_open_reference_aiff() {
    let data = include_bytes!("reference/ref_44100_16bit_mono.aiff");
    let (info, samples) = shravan::codec::open(data).unwrap();
    assert_eq!(info.format, shravan::AudioFormat::Aiff);
    assert_eq!(samples.len(), 4410);
}