Files
Soundgen/crates/soundgen-fmt/src/renderer.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

295 lines
8.5 KiB
Rust

//! Render a [`SoundSpec`] to interleaved stereo `Vec<f32>`.
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<f32> {
let sr = spec.sample_rate as f32;
let n_samples = (spec.duration * sr).ceil() as usize;
let mut channels: Vec<ChannelRenderer> = 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<EnvelopeSpec>,
filter: &Option<FilterSpec>,
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);
}
}