use captchaforge::audio_dsp::{
bandpass_filter, decode_to_pcm, encode_wav_pcm16, peak_normalise, resample_linear,
spectral_subtraction_denoise, time_stretch, PcmAudio,
};
use proptest::prelude::*;
fn arb_audio() -> impl Strategy<Value = PcmAudio> {
(
proptest::collection::vec(-1.0f32..=1.0f32, 64..1024),
proptest::sample::select(vec![8000u32, 16000, 22050, 44100, 48000]),
)
.prop_map(|(samples, sample_rate)| PcmAudio::new(samples, sample_rate))
}
fn arb_long_audio() -> impl Strategy<Value = PcmAudio> {
(
proptest::collection::vec(-1.0f32..=1.0f32, 2048..8192),
proptest::sample::select(vec![8000u32, 16000]),
)
.prop_map(|(samples, sample_rate)| PcmAudio::new(samples, sample_rate))
}
fn no_nan_or_inf(samples: &[f32]) -> bool {
samples.iter().all(|s| s.is_finite())
}
proptest! {
#[test]
fn bandpass_filter_output_is_finite(
audio in arb_audio(),
low_hz in 0.0f32..2000.0,
high_hz in 2000.0f32..8000.0,
) {
let out = bandpass_filter(&audio, low_hz, high_hz);
prop_assert!(no_nan_or_inf(&out.samples),
"bandpass produced non-finite samples");
prop_assert_eq!(out.samples.len(), audio.samples.len());
prop_assert_eq!(out.sample_rate, audio.sample_rate);
}
#[test]
fn peak_normalise_bounds_output(
audio in arb_audio(),
target_dbfs in -60.0f32..=-1.0,
) {
prop_assume!(audio.peak() > 1e-6);
let out = peak_normalise(&audio, target_dbfs);
prop_assert!(no_nan_or_inf(&out.samples));
let new_peak = out.peak();
prop_assert!(new_peak.is_finite());
let target_linear = 10f32.powf(target_dbfs / 20.0);
prop_assert!(new_peak <= target_linear + 1e-3,
"peak {} exceeds target {}", new_peak, target_linear);
}
#[test]
fn time_stretch_factor_one_is_no_op_length(audio in arb_audio()) {
let out = time_stretch(&audio, 1.0);
prop_assert_eq!(out.samples.len(), audio.samples.len());
prop_assert!(no_nan_or_inf(&out.samples));
}
#[test]
fn time_stretch_is_monotonic_in_factor(
audio in arb_long_audio(),
factor_low in 0.5f32..0.9,
factor_high in 1.5f32..2.5,
) {
let out_low = time_stretch(&audio, factor_low);
let out_high = time_stretch(&audio, factor_high);
prop_assert!(no_nan_or_inf(&out_low.samples));
prop_assert!(no_nan_or_inf(&out_high.samples));
prop_assert!(out_high.samples.len() >= out_low.samples.len(),
"factor {} produced {} samples; factor {} produced {}",
factor_high, out_high.samples.len(),
factor_low, out_low.samples.len());
}
#[test]
fn denoise_preserves_finiteness_and_rate(
audio in arb_audio(),
head_ms in 1u32..50,
) {
let out = spectral_subtraction_denoise(&audio, head_ms);
prop_assert!(no_nan_or_inf(&out.samples));
prop_assert_eq!(out.sample_rate, audio.sample_rate);
}
#[test]
fn encode_then_decode_preserves_shape(audio in arb_audio()) {
let wav = encode_wav_pcm16(&audio);
prop_assert!(wav.len() >= 44, "encoded WAV must include header");
let decoded = decode_to_pcm(&wav);
prop_assert!(decoded.is_ok(),
"round-trip decode failed: {:?}", decoded.err());
let decoded = decoded.unwrap();
prop_assert!(no_nan_or_inf(&decoded.samples));
}
#[test]
fn decode_to_pcm_rejects_garbage_without_panic(bytes in proptest::collection::vec(any::<u8>(), 0..200)) {
let _ = decode_to_pcm(&bytes); }
#[test]
fn resample_same_rate_preserves_length(audio in arb_audio()) {
let rate = audio.sample_rate;
let len = audio.samples.len();
let out = resample_linear(audio, rate);
prop_assert_eq!(out.samples.len(), len);
prop_assert!(no_nan_or_inf(&out.samples));
}
}
#[test]
fn empty_audio_decode_returns_err() {
assert!(decode_to_pcm(&[]).is_err());
}
#[test]
fn under_44_byte_input_returns_err() {
let short = vec![0u8; 16];
assert!(decode_to_pcm(&short).is_err());
}
#[test]
fn non_riff_input_returns_err() {
let mut not_riff = vec![0u8; 64];
not_riff[0..4].copy_from_slice(b"NOPE");
assert!(decode_to_pcm(¬_riff).is_err());
}