Files
wifi-densepose/examples/vibecast-7sense/tests/integration/audio_test.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

730 lines
23 KiB
Rust

//! Integration tests for Audio Ingestion Context
//!
//! Tests for audio file loading, resampling, segmentation, and spectrogram generation.
use vibecast_tests::fixtures::*;
use vibecast_tests::mocks::*;
use std::io::Cursor;
// ============================================================================
// Audio File Loading Tests
// ============================================================================
mod audio_loading {
use super::*;
#[test]
fn test_load_wav_file_32khz() {
// Create test WAV data at 32kHz
let wav_bytes = create_test_wav_bytes(5000); // 5 seconds
// Verify WAV header
assert_eq!(&wav_bytes[0..4], b"RIFF");
assert_eq!(&wav_bytes[8..12], b"WAVE");
assert_eq!(&wav_bytes[12..16], b"fmt ");
// Parse sample rate from header
let sample_rate =
u32::from_le_bytes([wav_bytes[24], wav_bytes[25], wav_bytes[26], wav_bytes[27]]);
assert_eq!(sample_rate, 32000);
}
#[test]
fn test_load_audio_correct_duration() {
let duration_ms = 5000;
let samples = create_test_audio_samples(duration_ms, 32000);
let expected_samples = (duration_ms as f64 * 32000.0 / 1000.0) as usize;
assert_eq!(samples.len(), expected_samples);
}
#[test]
fn test_audio_samples_in_valid_range() {
let samples = create_test_audio_samples(1000, 32000);
for (i, sample) in samples.iter().enumerate() {
assert!(
*sample >= -1.0 && *sample <= 1.0,
"Sample {} out of range: {}",
i,
sample
);
assert!(
!sample.is_nan() && !sample.is_infinite(),
"Sample {} is NaN or Inf",
i
);
}
}
#[test]
fn test_audio_format_validation() {
let format = AudioFormat::default();
assert_eq!(format.sample_rate, 32000, "Must be 32kHz for Perch 2.0");
assert_eq!(format.channels, 1, "Must be mono");
assert!(format.bit_depth >= 16, "Minimum 16-bit");
}
#[test]
fn test_load_different_durations() {
let durations = vec![1000, 5000, 10000, 30000, 60000];
for duration in durations {
let samples = create_test_audio_samples(duration, 32000);
let expected = (duration as f64 * 32.0) as usize;
assert_eq!(
samples.len(),
expected,
"Wrong sample count for {}ms",
duration
);
}
}
#[test]
fn test_wav_bytes_parseable() {
let wav_bytes = create_test_wav_bytes(5000);
// Basic WAV structure validation
assert!(wav_bytes.len() > 44, "WAV too short for valid header");
// Verify data chunk
let data_marker = &wav_bytes[36..40];
assert_eq!(data_marker, b"data");
// Verify data size
let data_size =
u32::from_le_bytes([wav_bytes[40], wav_bytes[41], wav_bytes[42], wav_bytes[43]]);
assert!(data_size > 0);
}
}
// ============================================================================
// Resampling Tests
// ============================================================================
mod resampling {
use super::*;
/// Mock resampler that converts audio to target sample rate
struct MockResampler {
target_rate: u32,
}
impl MockResampler {
fn new(target_rate: u32) -> Self {
Self { target_rate }
}
fn resample(&self, samples: &[f32], source_rate: u32) -> Vec<f32> {
if source_rate == self.target_rate {
return samples.to_vec();
}
let ratio = self.target_rate as f64 / source_rate as f64;
let new_len = (samples.len() as f64 * ratio) as usize;
// Simple linear interpolation resampling
(0..new_len)
.map(|i| {
let src_idx = i as f64 / ratio;
let idx0 = src_idx.floor() as usize;
let idx1 = (idx0 + 1).min(samples.len() - 1);
let frac = src_idx - idx0 as f64;
samples[idx0] * (1.0 - frac as f32) + samples[idx1] * frac as f32
})
.collect()
}
}
#[test]
fn test_resample_44100_to_32000() {
let source_rate = 44100;
let target_rate = 32000;
let duration_ms = 1000;
// Create 44.1kHz audio
let samples: Vec<f32> = (0..(source_rate * duration_ms / 1000) as usize)
.map(|i| (i as f32 * 0.01).sin())
.collect();
let resampler = MockResampler::new(target_rate);
let resampled = resampler.resample(&samples, source_rate);
let expected_len = (target_rate * duration_ms / 1000) as usize;
// Allow 1 sample tolerance due to rounding
assert!(
(resampled.len() as i64 - expected_len as i64).abs() <= 1,
"Expected ~{} samples, got {}",
expected_len,
resampled.len()
);
}
#[test]
fn test_resample_48000_to_32000() {
let source_rate = 48000;
let target_rate = 32000;
let duration_ms = 1000;
let samples: Vec<f32> = (0..(source_rate * duration_ms / 1000) as usize)
.map(|i| (i as f32 * 0.01).sin())
.collect();
let resampler = MockResampler::new(target_rate);
let resampled = resampler.resample(&samples, source_rate);
let expected_len = (target_rate * duration_ms / 1000) as usize;
assert!(
(resampled.len() as i64 - expected_len as i64).abs() <= 1,
"Expected ~{} samples, got {}",
expected_len,
resampled.len()
);
}
#[test]
fn test_resample_preserves_energy() {
let source_rate = 44100;
let target_rate = 32000;
let samples: Vec<f32> = (0..44100).map(|i| (i as f32 * 0.01).sin()).collect();
let source_energy: f32 = samples.iter().map(|x| x * x).sum::<f32>() / samples.len() as f32;
let resampler = MockResampler::new(target_rate);
let resampled = resampler.resample(&samples, source_rate);
let target_energy: f32 =
resampled.iter().map(|x| x * x).sum::<f32>() / resampled.len() as f32;
// Energy should be approximately preserved
let energy_diff = (source_energy - target_energy).abs() / source_energy;
assert!(
energy_diff < 0.1,
"Energy changed by {:.1}%",
energy_diff * 100.0
);
}
#[test]
fn test_resample_identity_at_32000() {
let samples = create_test_audio_samples(1000, 32000);
let resampler = MockResampler::new(32000);
let resampled = resampler.resample(&samples, 32000);
assert_eq!(samples.len(), resampled.len());
for (a, b) in samples.iter().zip(resampled.iter()) {
assert!((a - b).abs() < 0.0001);
}
}
}
// ============================================================================
// Segmentation Tests
// ============================================================================
mod segmentation {
use super::*;
/// Mock energy-based segmenter
struct MockSegmenter {
window_ms: u64,
hop_ms: u64,
threshold: f32,
min_duration_ms: u64,
sample_rate: u32,
}
impl MockSegmenter {
fn new() -> Self {
Self {
window_ms: 100,
hop_ms: 50,
threshold: 0.1,
min_duration_ms: 500,
sample_rate: 32000,
}
}
fn segment(&self, samples: &[f32], recording_id: RecordingId) -> Vec<CallSegment> {
let window_size = (self.window_ms as usize * self.sample_rate as usize) / 1000;
let hop_size = (self.hop_ms as usize * self.sample_rate as usize) / 1000;
// Compute energy per window
let mut energies: Vec<f32> = Vec::new();
let mut i = 0;
while i + window_size <= samples.len() {
let energy: f32 =
samples[i..i + window_size].iter().map(|x| x * x).sum::<f32>() / window_size as f32;
energies.push(energy);
i += hop_size;
}
// Find segments above threshold
let mut segments = Vec::new();
let mut in_segment = false;
let mut segment_start = 0;
for (i, energy) in energies.iter().enumerate() {
let time_ms = (i * self.hop_ms as usize) as u64;
if *energy > self.threshold && !in_segment {
in_segment = true;
segment_start = time_ms;
} else if *energy <= self.threshold && in_segment {
in_segment = false;
let duration = time_ms - segment_start;
if duration >= self.min_duration_ms {
segments.push(CallSegment {
id: SegmentId::new(),
recording_id,
start_ms: segment_start,
end_ms: time_ms,
snr: 15.0,
energy: energies[segment_start as usize / self.hop_ms as usize],
clipping_score: 0.0,
overlap_score: 0.0,
quality_grade: QualityGrade::Good,
});
}
}
}
segments
}
}
#[test]
fn test_segmentation_detects_calls() {
let segmenter = MockSegmenter::new();
let recording_id = RecordingId::new();
// Create audio with clear signal/silence pattern
let mut samples = vec![0.0f32; 64000]; // 2 seconds
// Add a loud "call" at 200-1200ms (1 second of signal)
for i in 6400..38400 {
samples[i] = 0.8 * ((i as f32 * 0.05).sin()); // Louder signal
}
// Silence at start and end
let segments = segmenter.segment(&samples, recording_id);
assert!(
!segments.is_empty(),
"Should detect at least one segment"
);
}
#[test]
fn test_segmentation_non_overlapping() {
let segments = create_segment_sequence(5, 500);
for i in 0..segments.len() - 1 {
assert!(
segments[i].end_ms <= segments[i + 1].start_ms,
"Segments {} and {} overlap",
i,
i + 1
);
}
}
#[test]
fn test_segment_duration_constraint() {
let segments = create_segment_sequence(10, 0);
for segment in &segments {
let duration = segment.end_ms - segment.start_ms;
assert_eq!(duration, 5000, "Perch segments should be 5 seconds");
}
}
#[test]
fn test_segmentation_snr_computation() {
let segments = create_segment_sequence(5, 500);
for segment in &segments {
assert!(segment.snr > 0.0, "SNR should be positive");
assert!(segment.snr < 100.0, "SNR should be realistic");
}
}
#[test]
fn test_segment_within_recording_bounds() {
let recording = create_test_recording_with_duration(60000);
let segments = create_segment_sequence(10, 500);
for segment in &segments {
assert!(
segment.end_ms <= recording.duration_ms,
"Segment extends beyond recording"
);
}
}
#[test]
fn test_segmentation_preserves_recording_id() {
let recording_id = RecordingId::new();
let mut segment = create_test_segment();
segment.recording_id = recording_id;
assert_eq!(segment.recording_id, recording_id);
}
}
// ============================================================================
// Spectrogram Generation Tests
// ============================================================================
mod spectrogram {
use super::*;
const MEL_BINS: usize = 128;
const MEL_FRAMES: usize = 500;
#[test]
fn test_spectrogram_dimensions() {
let spectrogram = create_test_spectrogram();
assert_eq!(spectrogram.len(), MEL_FRAMES, "Should have 500 frames");
assert_eq!(
spectrogram[0].len(),
MEL_BINS,
"Should have 128 mel bins"
);
}
#[test]
fn test_spectrogram_values_non_negative() {
let spectrogram = create_test_spectrogram();
for (frame_idx, frame) in spectrogram.iter().enumerate() {
for (bin_idx, value) in frame.iter().enumerate() {
assert!(
*value >= 0.0,
"Frame {} bin {} has negative value: {}",
frame_idx,
bin_idx,
value
);
}
}
}
#[test]
fn test_spectrogram_no_nan_or_inf() {
let spectrogram = create_test_spectrogram();
for (frame_idx, frame) in spectrogram.iter().enumerate() {
for (bin_idx, value) in frame.iter().enumerate() {
assert!(
!value.is_nan() && !value.is_infinite(),
"Frame {} bin {} is NaN/Inf",
frame_idx,
bin_idx
);
}
}
}
#[test]
fn test_spectrogram_energy_distribution() {
let spectrogram = create_test_spectrogram();
// Compute total energy per frame
let frame_energies: Vec<f32> = spectrogram
.iter()
.map(|frame| frame.iter().sum())
.collect();
// Energy should vary (not all zeros or all same)
let min_energy = frame_energies
.iter()
.cloned()
.fold(f32::INFINITY, f32::min);
let max_energy = frame_energies
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max);
assert!(
max_energy > min_energy * 1.1,
"Energy should vary across frames"
);
}
#[test]
fn test_spectrogram_from_audio_samples() {
let samples = create_test_audio_samples(5000, 32000);
// Simple mock spectrogram computation
let hop_size = samples.len() / MEL_FRAMES;
let spectrogram: Vec<Vec<f32>> = (0..MEL_FRAMES)
.map(|frame| {
let start = frame * hop_size;
let end = (start + hop_size).min(samples.len());
let chunk = &samples[start..end];
// Mock mel filterbank (simplified)
(0..MEL_BINS)
.map(|bin| {
let freq_start = bin * chunk.len() / MEL_BINS;
let freq_end = ((bin + 1) * chunk.len() / MEL_BINS).min(chunk.len());
if freq_start < freq_end {
chunk[freq_start..freq_end]
.iter()
.map(|x| x.abs())
.sum::<f32>()
/ (freq_end - freq_start) as f32
} else {
0.0
}
})
.collect()
})
.collect();
assert_eq!(spectrogram.len(), MEL_FRAMES);
assert_eq!(spectrogram[0].len(), MEL_BINS);
}
#[test]
fn test_spectrogram_temporal_resolution() {
// 5 seconds at 32kHz = 160000 samples
// 500 frames means ~10ms per frame
let samples_per_frame = 160000 / MEL_FRAMES;
let ms_per_frame = (samples_per_frame as f64 / 32.0) as u64;
assert!(
ms_per_frame >= 9 && ms_per_frame <= 11,
"Frame duration should be ~10ms, got {}ms",
ms_per_frame
);
}
#[test]
fn test_spectrogram_frequency_range() {
// Perch 2.0 uses 60Hz to 16000Hz
// With 128 mel bins, each bin covers approximately 125Hz
let min_freq = 60.0;
let max_freq = 16000.0;
let hz_per_bin = (max_freq - min_freq) / MEL_BINS as f32;
assert!(
hz_per_bin > 100.0 && hz_per_bin < 150.0,
"Each mel bin should cover ~125Hz, got {}Hz",
hz_per_bin
);
}
}
// ============================================================================
// Recording Repository Integration Tests
// ============================================================================
mod repository_integration {
use super::*;
use chrono::Duration as ChronoDuration;
#[test]
fn test_recording_crud_operations() {
let repo = MockRecordingRepository::new();
// Create
let recording = create_test_recording();
let id = recording.id;
repo.save(recording).unwrap();
// Read
let found = repo.find_by_id(&id).unwrap().unwrap();
assert_eq!(found.id, id);
// Count
assert_eq!(repo.count(), 1);
// Delete
repo.delete(&id).unwrap();
assert_eq!(repo.count(), 0);
assert!(repo.find_by_id(&id).unwrap().is_none());
}
#[test]
fn test_find_recordings_by_sensor() {
let repo = MockRecordingRepository::new();
// Add recordings from different sensors
for i in 0..5 {
let mut recording = create_test_recording();
recording.sensor_id = format!("SENSOR_{}", i % 2);
repo.save(recording).unwrap();
}
let sensor0_recordings = repo.find_by_sensor_id("SENSOR_0").unwrap();
let sensor1_recordings = repo.find_by_sensor_id("SENSOR_1").unwrap();
assert_eq!(sensor0_recordings.len(), 3);
assert_eq!(sensor1_recordings.len(), 2);
}
#[test]
fn test_find_recordings_by_date_range() {
let repo = MockRecordingRepository::new();
let now = chrono::Utc::now();
// Add recordings at different times
for i in 0..5 {
let mut recording = create_test_recording();
recording.start_timestamp = now - ChronoDuration::hours(i as i64);
repo.save(recording).unwrap();
}
// Find recordings from last 2 hours
let start = now - ChronoDuration::hours(2);
let end = now + ChronoDuration::hours(1);
let recent = repo.find_by_date_range(start, end).unwrap();
assert_eq!(recent.len(), 3); // 0, 1, 2 hours ago
}
#[test]
fn test_segment_repository_by_recording() {
let repo = MockSegmentRepository::new();
let recording_id = RecordingId::new();
// Add segments for this recording
for i in 0..5 {
let segment = CallSegment {
recording_id,
start_ms: i * 5500,
end_ms: i * 5500 + 5000,
..Default::default()
};
repo.save(segment).unwrap();
}
// Add segments for another recording
let other_id = RecordingId::new();
for i in 0..3 {
let segment = CallSegment {
recording_id: other_id,
start_ms: i * 5500,
end_ms: i * 5500 + 5000,
..Default::default()
};
repo.save(segment).unwrap();
}
let segments = repo.find_by_recording(&recording_id).unwrap();
assert_eq!(segments.len(), 5);
}
#[test]
fn test_segment_repository_by_time_range() {
let repo = MockSegmentRepository::new();
let recording_id = RecordingId::new();
// Add segments spanning 0-30 seconds
for i in 0..6 {
let segment = CallSegment {
recording_id,
start_ms: i * 5000,
end_ms: (i + 1) * 5000,
..Default::default()
};
repo.save(segment).unwrap();
}
// Find segments in 10-20 second range
let segments = repo
.find_by_time_range(&recording_id, 10000, 20000)
.unwrap();
assert_eq!(segments.len(), 2); // Segments at 10-15s and 15-20s
}
}
// ============================================================================
// Quality Assessment Tests
// ============================================================================
mod quality_assessment {
use super::*;
#[test]
fn test_quality_grade_from_snr() {
assert_eq!(QualityGrade::from_snr(25.0), QualityGrade::Excellent);
assert_eq!(QualityGrade::from_snr(20.1), QualityGrade::Excellent);
assert_eq!(QualityGrade::from_snr(15.0), QualityGrade::Good);
assert_eq!(QualityGrade::from_snr(10.1), QualityGrade::Good);
assert_eq!(QualityGrade::from_snr(7.0), QualityGrade::Fair);
assert_eq!(QualityGrade::from_snr(5.1), QualityGrade::Fair);
assert_eq!(QualityGrade::from_snr(3.0), QualityGrade::Poor);
assert_eq!(QualityGrade::from_snr(0.1), QualityGrade::Poor);
assert_eq!(QualityGrade::from_snr(-5.0), QualityGrade::Unusable);
}
#[test]
fn test_find_segments_by_quality() {
let repo = MockSegmentRepository::new();
// Add segments with varying quality
let snr_values = vec![25.0, 15.0, 7.0, 3.0, -5.0];
for snr in snr_values {
let segment = create_test_segment_with_snr(snr);
repo.save(segment).unwrap();
}
// Find good or better
let good_or_better = repo.find_by_quality(QualityGrade::Good).unwrap();
assert_eq!(good_or_better.len(), 2); // Excellent and Good
// Find fair or better
let fair_or_better = repo.find_by_quality(QualityGrade::Fair).unwrap();
assert_eq!(fair_or_better.len(), 3); // Excellent, Good, Fair
}
#[test]
fn test_segment_clipping_detection() {
let mut segment = create_test_segment();
// No clipping
segment.clipping_score = 0.0;
assert!(segment.clipping_score < 0.01);
// Minor clipping
segment.clipping_score = 0.05;
assert!(segment.clipping_score < 0.1);
// Severe clipping
segment.clipping_score = 0.3;
assert!(segment.clipping_score > 0.2);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_integration_smoke_test() {
// Create recording
let recording = create_test_recording();
// Create segments
let segments = create_segment_sequence(5, 500);
// Create spectrogram
let spectrogram = create_test_spectrogram();
// Verify relationships
assert!(recording.duration_ms >= segments.last().unwrap().end_ms);
assert_eq!(spectrogram.len(), 500);
assert_eq!(spectrogram[0].len(), 128);
}
}