//! Integration tests: end-to-end render → WAV verification. use hound; use soundgen_core::{FrequencyAutomation, SweepCurve}; use soundgen_fmt::{ render_spec, ChannelSpec, CutoffAutomation, EnvelopeSpec, FilterKind, FilterSpec, PresetCategory, PresetRegistry, SoundSpec, }; use soundgen_io::write_wav; /// Get the workspace root path (presets/ directory). fn workspace_root() -> &'static str { concat!(env!("CARGO_MANIFEST_DIR"), "/../..") } fn rms(samples: &[f32]) -> f32 { if samples.is_empty() { return 0.0; } let sum: f32 = samples.iter().map(|s| s * s).sum(); (sum / samples.len() as f32).sqrt() } #[test] fn test_render_spec_to_wav_and_verify() { let spec = SoundSpec { name: "integration_test".to_string(), duration: 0.2, sample_rate: 44100, channels: vec![ChannelSpec::Pulse { duty: 50, frequency: FrequencyAutomation { start: 200.0, end: 800.0, curve: SweepCurve::Exponential, }, envelope: Some(EnvelopeSpec { attack: 0.005, decay: 0.05, sustain: 0.3, release: 0.1, }), filter: None, vibrato: None, volume: 0.6, pan: 0.0, }], }; let samples = render_spec(&spec); // 0.2s * 44100 = 8820 frames -> 17640 interleaved stereo samples assert_eq!(samples.len(), 17640); let mono: Vec = samples.iter().step_by(2).cloned().collect(); let r = rms(&mono); assert!(r > 0.01, "RMS too low: {}", r); let path = std::env::temp_dir().join("soundgen_integration.wav"); write_wav(&path, &samples, 44100).unwrap(); assert!(path.exists()); let reader = hound::WavReader::open(&path).unwrap(); let wav_spec = reader.spec(); assert_eq!(wav_spec.channels, 2); assert_eq!(wav_spec.sample_rate, 44100); let _ = std::fs::remove_file(&path); } #[test] fn test_all_presets_render_successfully() { let presets_dir = std::path::Path::new(workspace_root()).join("presets"); let registry = PresetRegistry::load_dir(&presets_dir).expect("failed to load presets"); assert!( registry.len() >= 10, "expected at least 10 presets, got {}", registry.len() ); for entry in registry.list(None) { let samples = render_spec(&entry.spec); let expected_len = (entry.spec.duration * entry.spec.sample_rate as f32).ceil() as usize * 2; assert_eq!( samples.len(), expected_len, "preset '{}' has wrong sample count", entry.name ); let mono: Vec = samples.iter().step_by(2).cloned().collect(); let r = rms(&mono); assert!( r > 0.001, "preset '{}' has near-zero RMS: {}", entry.name, r ); } } #[test] fn test_sfx_and_ui_categories_present() { let presets_dir = std::path::Path::new(workspace_root()).join("presets"); let registry = PresetRegistry::load_dir(&presets_dir).unwrap(); let sfx = registry.list(Some(PresetCategory::Sfx)); let ui = registry.list(Some(PresetCategory::Ui)); assert!(sfx.len() >= 6, "expected at least 6 SFX presets"); assert!(ui.len() >= 4, "expected at least 4 UI presets"); assert!(registry.get("jump").is_some()); assert!(registry.get("explosion").is_some()); assert!(registry.get("click").is_some()); assert!(registry.get("confirm").is_some()); } #[test] fn test_json_spec_roundtrip() { let spec = SoundSpec { name: "roundtrip".to_string(), duration: 0.3, sample_rate: 48000, channels: vec![ChannelSpec::Noise { mode: "white".to_string(), frequency: 8000.0, envelope: Some(EnvelopeSpec { attack: 0.0, decay: 0.2, sustain: 0.0, release: 0.1, }), filter: Some(FilterSpec { kind: FilterKind::Lowpass, cutoff: 2000.0, cutoff_sweep: Some(CutoffAutomation { start: 2000.0, end: 200.0, curve: SweepCurve::Exponential, }), q: 0.707, }), vibrato: None, volume: 0.7, pan: 0.0, }], }; let json = serde_json::to_string_pretty(&spec).unwrap(); let spec2: SoundSpec = serde_json::from_str(&json).unwrap(); assert_eq!(spec2.name, spec.name); assert!((spec2.duration - spec.duration).abs() < 0.001); assert_eq!(spec2.sample_rate, spec.sample_rate); assert_eq!(spec2.channels.len(), spec.channels.len()); } #[test] fn test_wav_file_format_correct() { let spec = SoundSpec { name: "wav_format_test".to_string(), duration: 0.05, sample_rate: 22050, channels: vec![ChannelSpec::Pulse { duty: 50, frequency: FrequencyAutomation::fixed(440.0), envelope: Some(EnvelopeSpec { attack: 0.0, decay: 0.0, sustain: 1.0, release: 0.0, }), filter: None, vibrato: None, volume: 0.5, pan: 0.0, }], }; let samples = render_spec(&spec); let path = std::env::temp_dir().join("soundgen_wav_format_test.wav"); write_wav(&path, &samples, 22050).unwrap(); let reader = hound::WavReader::open(&path).unwrap(); let wav_spec = reader.spec(); assert_eq!(wav_spec.channels, 2); assert_eq!(wav_spec.sample_rate, 22050); assert_eq!(wav_spec.bits_per_sample, 16); // 0.05 * 22050 = 1102.5, ceil = 1103 frames, * 2 channels = 2206 samples let sample_count = reader.into_samples::().count(); assert_eq!(sample_count, 2206); let _ = std::fs::remove_file(&path); } #[test] fn test_multi_channel_render() { let spec = SoundSpec { name: "multi".to_string(), duration: 0.1, sample_rate: 44100, channels: vec![ ChannelSpec::Pulse { duty: 50, frequency: FrequencyAutomation::fixed(440.0), envelope: Some(EnvelopeSpec::default()), filter: None, vibrato: None, volume: 0.5, pan: -0.5, }, ChannelSpec::Triangle { frequency: FrequencyAutomation::fixed(220.0), envelope: Some(EnvelopeSpec::default()), filter: None, vibrato: None, volume: 0.4, pan: 0.5, }, ChannelSpec::Noise { mode: "white".to_string(), frequency: 5000.0, envelope: Some(EnvelopeSpec::default()), filter: None, vibrato: None, volume: 0.3, pan: 0.0, }, ], }; let samples = render_spec(&spec); // 0.1 * 44100 = 4410 frames -> 8820 interleaved assert_eq!(samples.len(), 8820); let left: Vec = samples.iter().step_by(2).cloned().collect(); let right: Vec = samples.iter().skip(1).step_by(2).cloned().collect(); assert!(rms(&left) > 0.01); assert!(rms(&right) > 0.01); }