117 lines
4.4 KiB
Rust
117 lines
4.4 KiB
Rust
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
use molotov_core::audio::c_api::*;
|
|
use molotov_core::audio::types::{AAudioDirection, AAudioFormat, AAudioResult};
|
|
use std::ffi::c_void;
|
|
|
|
#[test]
|
|
fn test_440hz_loopback_frequency_and_amplitude() {
|
|
let sample_rate = 48000;
|
|
let target_freq = 440.0f32;
|
|
let duration_frames = 24000; // 0.5s at 48kHz
|
|
|
|
// 1. Setup Stream via AAudio C-ABI
|
|
unsafe {
|
|
let mut builder_ptr = std::ptr::null_mut();
|
|
assert_eq!(AAudio_createStreamBuilder(&mut builder_ptr), AAudioResult::Ok as i32);
|
|
|
|
AAudioStreamBuilder_setDirection(builder_ptr, AAudioDirection::Input as i32);
|
|
AAudioStreamBuilder_setSampleRate(builder_ptr, sample_rate);
|
|
AAudioStreamBuilder_setChannelCount(builder_ptr, 1);
|
|
AAudioStreamBuilder_setFormat(builder_ptr, AAudioFormat::PcmFloat as i32);
|
|
AAudioStreamBuilder_setBufferCapacityInFrames(builder_ptr, duration_frames);
|
|
|
|
let mut stream_ptr = std::ptr::null_mut();
|
|
assert_eq!(AAudioStreamBuilder_openStream(builder_ptr, &mut stream_ptr), AAudioResult::Ok as i32);
|
|
assert!(!stream_ptr.is_null());
|
|
|
|
assert_eq!(AAudioStream_requestStart(stream_ptr), AAudioResult::Ok as i32);
|
|
|
|
// 2. Synthesize 440 Hz pure tone into playback
|
|
let mut generated = Vec::with_capacity(duration_frames as usize);
|
|
for i in 0..duration_frames {
|
|
let t = i as f32 / sample_rate as f32;
|
|
let sample = (2.0 * std::f32::consts::PI * target_freq * t).sin();
|
|
generated.push(sample);
|
|
}
|
|
|
|
let written = AAudioStream_write(
|
|
stream_ptr,
|
|
generated.as_ptr() as *const c_void,
|
|
duration_frames,
|
|
0,
|
|
);
|
|
assert_eq!(written, duration_frames);
|
|
|
|
// 3. Capture audio from stream
|
|
let mut captured = vec![0.0f32; duration_frames as usize];
|
|
let read_frames = AAudioStream_read(
|
|
stream_ptr,
|
|
captured.as_mut_ptr() as *mut c_void,
|
|
duration_frames,
|
|
0,
|
|
);
|
|
assert_eq!(read_frames, duration_frames);
|
|
|
|
// 4. Teardown stream via C-ABI
|
|
assert_eq!(AAudioStream_requestPause(stream_ptr), AAudioResult::Ok as i32);
|
|
assert_eq!(AAudioStream_close(stream_ptr), AAudioResult::Ok as i32);
|
|
assert_eq!(AAudioStreamBuilder_delete(builder_ptr), AAudioResult::Ok as i32);
|
|
|
|
// 5. Rigorous Spectral Scan across 430 Hz to 450 Hz in 0.5 Hz steps
|
|
let mut max_magnitude = 0.0f32;
|
|
let mut peak_freq = 0.0f32;
|
|
|
|
let start_freq = 430.0f32;
|
|
let end_freq = 450.0f32;
|
|
let step = 0.5f32;
|
|
let steps = ((end_freq - start_freq) / step) as usize;
|
|
|
|
for s in 0..=steps {
|
|
let f = start_freq + (s as f32) * step;
|
|
let mut real = 0.0f32;
|
|
let mut imag = 0.0f32;
|
|
for (i, &sample) in captured.iter().enumerate() {
|
|
let angle = 2.0 * std::f32::consts::PI * f * (i as f32) / (sample_rate as f32);
|
|
real += sample * angle.cos();
|
|
imag -= sample * angle.sin();
|
|
}
|
|
let mag = (real * real + imag * imag).sqrt();
|
|
if mag > max_magnitude {
|
|
max_magnitude = mag;
|
|
peak_freq = f;
|
|
}
|
|
}
|
|
|
|
// Assert peak frequency is detected within ±1.0 Hz tolerance
|
|
let freq_delta = (peak_freq - target_freq).abs();
|
|
assert!(
|
|
freq_delta <= 1.0,
|
|
"Peak frequency {} Hz exceeds ±1.0 Hz tolerance around {} Hz",
|
|
peak_freq,
|
|
target_freq
|
|
);
|
|
|
|
// 6. Compute Normalized Cross-Correlation (rho) between generated and captured waveforms
|
|
let mean_gen = generated.iter().sum::<f32>() / (duration_frames as f32);
|
|
let mean_cap = captured.iter().sum::<f32>() / (duration_frames as f32);
|
|
|
|
let mut numerator = 0.0f32;
|
|
let mut denom_gen = 0.0f32;
|
|
let mut denom_cap = 0.0f32;
|
|
|
|
for i in 0..duration_frames as usize {
|
|
let dg = generated[i] - mean_gen;
|
|
let dc = captured[i] - mean_cap;
|
|
numerator += dg * dc;
|
|
denom_gen += dg * dg;
|
|
denom_cap += dc * dc;
|
|
}
|
|
|
|
let rho = numerator / (denom_gen.sqrt() * denom_cap.sqrt());
|
|
assert!(
|
|
rho >= 0.95,
|
|
"Waveform fidelity rho = {:.4} is below required 95% threshold",
|
|
rho
|
|
);
|
|
}
|
|
}
|