Files
Soundgen/crates/soundgen-fmt/tests/integration.rs
T
Emil 58c47fe286 Add vibrato support + fix feedback loop + GUI improvements
Vibrato (LFO frequency modulation):
- New VibratoSpec in SoundSpec: rate (Hz), depth (cents), delay (s)
- ChannelRenderer applies vibrato to frequency each tick
- Supported on Pulse and Triangle channels
- GUI: vibrato controls (enable, rate, depth) in channel panel
- Enables bird chirps, sirens, wobbles

Feedback loop fixes:
- generate_batch now accepts full SoundSpec array (not just names)
  so each sound is unique
- render_sound returns reference examples from feedback DB
- Fixed GUI bug: clicking stars no longer erases feedback text
- Feedback text auto-saves on Enter / focus loss
- Replaced emoji buttons (✓/🗑) with text (Save/Del)
- Green saved indicator when feedback exists in DB

99 tests passing, 0 warnings
2026-06-21 23:54:25 +03:00

243 lines
7.3 KiB
Rust

//! 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<f32> = 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<f32> = 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::<i16>().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<f32> = samples.iter().step_by(2).cloned().collect();
let right: Vec<f32> = samples.iter().skip(1).step_by(2).cloned().collect();
assert!(rms(&left) > 0.01);
assert!(rms(&right) > 0.01);
}