//! Render a [`SoundSpec`] to interleaved stereo `Vec`. use soundgen_core::{ ChannelRenderer, Envelope, Filter, FilterType, FrequencyAutomation, NoiseMode, Sweep, Vibrato, VoiceKind, }; use VoiceKind as VK; use crate::{ChannelSpec, EnvelopeSpec, FilterSpec, SoundSpec, VibratoSpec}; /// Render a [`SoundSpec`] to interleaved stereo samples (L, R, L, R, ...). pub fn render_spec(spec: &SoundSpec) -> Vec { let sr = spec.sample_rate as f32; let n_samples = (spec.duration * sr).ceil() as usize; let mut channels: Vec = spec .channels .iter() .map(|ch| build_channel(ch, sr, spec.duration)) .collect(); for ch in &mut channels { ch.trigger(); } soundgen_core::render_channels(&mut channels, n_samples) } fn build_channel(spec: &ChannelSpec, sr: f32, duration: f32) -> ChannelRenderer { match spec { ChannelSpec::Pulse { duty, frequency, envelope, filter, vibrato, volume, pan, } => { let duty_cycle = soundgen_core::voice::DutyCycle::from_percent(*duty); let voice = VK::Pulse(soundgen_core::voice::PulseChannel::new(sr, duty_cycle)); build_renderer( voice, sr, duration, frequency, envelope, filter, vibrato.as_ref(), *volume, *pan, ) } ChannelSpec::Triangle { frequency, envelope, filter, vibrato, volume, pan, } => { let voice = VK::Triangle(soundgen_core::voice::TriangleChannel::new(sr)); build_renderer( voice, sr, duration, frequency, envelope, filter, vibrato.as_ref(), *volume, *pan, ) } ChannelSpec::Noise { mode, frequency, envelope, filter, vibrato: _, volume, pan, } => { let noise_mode = match mode.as_str() { "periodic" => NoiseMode::Periodic, _ => NoiseMode::White, }; let noise = soundgen_core::voice::NoiseChannel::new(sr, noise_mode); let voice = VK::Noise(noise); let freq_auto = FrequencyAutomation::fixed(*frequency); build_renderer( voice, sr, duration, &freq_auto, envelope, filter, None, *volume, *pan, ) } } } #[allow(clippy::too_many_arguments)] fn build_renderer( voice: VoiceKind, sr: f32, duration: f32, frequency: &FrequencyAutomation, envelope: &Option, filter: &Option, vibrato: Option<&VibratoSpec>, volume: f32, pan: f32, ) -> ChannelRenderer { let mut cr = ChannelRenderer::new(voice, sr) .with_volume(volume) .with_pan(pan) .with_initial_frequency(frequency.start); // Frequency sweep (only if frequency changes over time) if !frequency.is_static() { cr = cr.with_freq_sweep(frequency.to_sweep(sr, duration)); } // Vibrato if let Some(vib) = vibrato { cr = cr.with_vibrato(Vibrato::new(sr, vib.rate, vib.depth)); } // Envelope if let Some(env) = envelope { cr = cr.with_envelope(Envelope::adsr( sr, env.attack, env.decay, env.sustain, env.release, )); } // Filter if let Some(filt) = filter { let ft = match filt.kind { crate::FilterKind::Lowpass => FilterType::Lowpass, crate::FilterKind::Highpass => FilterType::Highpass, }; let cutoff_start = filt .cutoff_sweep .as_ref() .map(|s| s.start) .unwrap_or(filt.cutoff); let filter = Filter::new(sr, ft, cutoff_start, filt.q); cr = cr.with_filter(filter); // Filter cutoff sweep if let Some(cs) = &filt.cutoff_sweep { let sweep = Sweep::new(sr, cs.start, cs.end, cs.curve, duration); cr = cr.with_filter_sweep(sweep); } } cr } #[cfg(test)] mod tests { use super::*; #[test] fn test_render_simple_spec() { let spec = SoundSpec { name: "test".to_string(), duration: 0.1, sample_rate: 44100, 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 out = render_spec(&spec); // Duration 0.1s * 44100 = 4410 samples → 8820 interleaved assert_eq!(out.len(), 8820); // Should have non-zero samples let non_zero = out.iter().filter(|&&s| s.abs() > 0.01).count(); assert!(non_zero > 100, "expected non-zero output"); } #[test] fn test_render_noise_spec() { let spec = SoundSpec { name: "noise_test".to_string(), duration: 0.05, sample_rate: 44100, channels: vec![ChannelSpec::Noise { mode: "white".to_string(), frequency: 8000.0, envelope: Some(EnvelopeSpec { attack: 0.0, decay: 0.05, sustain: 0.0, release: 0.0, }), filter: None, vibrato: None, volume: 0.5, pan: 0.0, }], }; let out = render_spec(&spec); assert_eq!(out.len(), 4410); // Noise should produce some non-zero samples let non_zero = out.iter().filter(|&&s| s.abs() > 0.01).count(); assert!(non_zero > 0); } #[test] fn test_render_empty_spec() { let spec = SoundSpec { name: "empty".to_string(), duration: 0.1, sample_rate: 44100, channels: vec![], }; let out = render_spec(&spec); assert_eq!(out.len(), 8820); // All zeros (no channels) assert!(out.iter().all(|&s| s == 0.0)); } #[test] fn test_render_with_frequency_sweep() { let spec = SoundSpec { name: "sweep".to_string(), duration: 0.1, sample_rate: 44100, channels: vec![ChannelSpec::Pulse { duty: 50, frequency: FrequencyAutomation { start: 100.0, end: 1000.0, curve: soundgen_core::SweepCurve::Exponential, }, 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 out = render_spec(&spec); assert_eq!(out.len(), 8820); // Should produce output let non_zero = out.iter().filter(|&&s| s.abs() > 0.01).count(); assert!(non_zero > 100); } #[test] fn test_render_with_filter() { let spec = SoundSpec { name: "filtered".to_string(), duration: 0.1, sample_rate: 44100, channels: vec![ChannelSpec::Noise { mode: "white".to_string(), frequency: 8000.0, envelope: Some(EnvelopeSpec { attack: 0.0, decay: 0.0, sustain: 1.0, release: 0.0, }), filter: Some(FilterSpec { kind: crate::FilterKind::Lowpass, cutoff: 500.0, cutoff_sweep: None, q: 0.707, }), vibrato: None, volume: 0.5, pan: 0.0, }], }; let out = render_spec(&spec); assert_eq!(out.len(), 8820); } }