Skip to main content

pedalkernel/
wav.rs

1//! WAV file I/O for testing and offline rendering.
2//!
3//! Uses `hound` to write processed audio to WAV files so circuits
4//! can be auditioned without a running JACK server.
5
6use hound::{SampleFormat, WavReader, WavSpec, WavWriter};
7use std::path::Path;
8
9use crate::PedalProcessor;
10
11/// WAV output spec: 48 kHz, 32-bit float, mono.
12pub const DEFAULT_SAMPLE_RATE: u32 = 48_000;
13
14fn wav_spec(sample_rate: u32) -> WavSpec {
15    WavSpec {
16        channels: 1,
17        sample_rate,
18        bits_per_sample: 32,
19        sample_format: SampleFormat::Float,
20    }
21}
22
23/// Generate a sine wave test signal.
24pub fn sine_wave(freq_hz: f64, duration_secs: f64, sample_rate: u32) -> Vec<f64> {
25    let n = (duration_secs * sample_rate as f64) as usize;
26    let mut buf = Vec::with_capacity(n);
27    for i in 0..n {
28        let t = i as f64 / sample_rate as f64;
29        buf.push(0.5 * (2.0 * std::f64::consts::PI * freq_hz * t).sin());
30    }
31    buf
32}
33
34/// Generate a guitar-like test signal (sum of harmonics with decay).
35pub fn guitar_pluck(freq_hz: f64, duration_secs: f64, sample_rate: u32) -> Vec<f64> {
36    let n = (duration_secs * sample_rate as f64) as usize;
37    let mut buf = Vec::with_capacity(n);
38    for i in 0..n {
39        let t = i as f64 / sample_rate as f64;
40        let envelope = (-3.0 * t).exp(); // exponential decay
41        let fundamental = (2.0 * std::f64::consts::PI * freq_hz * t).sin();
42        let h2 = 0.5 * (2.0 * std::f64::consts::PI * 2.0 * freq_hz * t).sin();
43        let h3 = 0.25 * (2.0 * std::f64::consts::PI * 3.0 * freq_hz * t).sin();
44        let h4 = 0.125 * (2.0 * std::f64::consts::PI * 4.0 * freq_hz * t).sin();
45        buf.push(0.4 * envelope * (fundamental + h2 + h3 + h4));
46    }
47    buf
48}
49
50/// Read a WAV file and return mono f64 samples normalized to [-1, 1] plus the sample rate.
51///
52/// Handles float and integer sample formats and mixes stereo (or multi-channel) down to mono.
53pub fn read_wav_mono(path: &Path) -> Result<(Vec<f64>, u32), Box<dyn std::error::Error>> {
54    let mut reader = WavReader::open(path)?;
55    let spec = reader.spec();
56    let sample_rate = spec.sample_rate;
57    let channels = spec.channels as usize;
58
59    let samples = match spec.sample_format {
60        SampleFormat::Float => {
61            let all: Vec<f32> = reader.samples::<f32>().map(|s| s.unwrap()).collect();
62            mix_to_mono(&all.iter().map(|&s| s as f64).collect::<Vec<_>>(), channels)
63        }
64        SampleFormat::Int => {
65            let max_val = (1_i64 << (spec.bits_per_sample - 1)) as f64;
66            let all: Vec<i32> = reader.samples::<i32>().map(|s| s.unwrap()).collect();
67            let normalized: Vec<f64> = all.iter().map(|&s| s as f64 / max_val).collect();
68            mix_to_mono(&normalized, channels)
69        }
70    };
71
72    Ok((samples, sample_rate))
73}
74
75/// Mix interleaved multi-channel samples down to mono by averaging channels.
76fn mix_to_mono(interleaved: &[f64], channels: usize) -> Vec<f64> {
77    if channels == 1 {
78        return interleaved.to_vec();
79    }
80    interleaved
81        .chunks(channels)
82        .map(|frame| frame.iter().sum::<f64>() / channels as f64)
83        .collect()
84}
85
86/// Process a buffer of samples through a pedal and write the result to a WAV file.
87pub fn render_to_wav<P: PedalProcessor>(
88    pedal: &mut P,
89    input: &[f64],
90    path: &Path,
91    sample_rate: u32,
92) -> Result<(), Box<dyn std::error::Error>> {
93    pedal.set_sample_rate(sample_rate as crate::Wave);
94    pedal.reset();
95
96    let mut writer = WavWriter::create(path, wav_spec(sample_rate))?;
97    for &sample in input {
98        let out = pedal.process(sample as crate::Wave);
99        writer.write_sample(out as f32)?;
100    }
101    writer.finalize()?;
102    Ok(())
103}
104
105/// Write raw f64 samples to a WAV file.
106pub fn write_wav(
107    samples: &[f64],
108    path: &Path,
109    sample_rate: u32,
110) -> Result<(), Box<dyn std::error::Error>> {
111    let mut writer = WavWriter::create(path, wav_spec(sample_rate))?;
112    for &s in samples {
113        writer.write_sample(s as f32)?;
114    }
115    writer.finalize()?;
116    Ok(())
117}
118
119/// Write both input and output (stereo) to a WAV for A/B comparison.
120pub fn write_stereo_wav(
121    left: &[f64],
122    right: &[f64],
123    path: &Path,
124    sample_rate: u32,
125) -> Result<(), Box<dyn std::error::Error>> {
126    let spec = WavSpec {
127        channels: 2,
128        sample_rate,
129        bits_per_sample: 32,
130        sample_format: SampleFormat::Float,
131    };
132    let mut writer = WavWriter::create(path, spec)?;
133    let n = left.len().min(right.len());
134    for i in 0..n {
135        writer.write_sample(left[i] as f32)?;
136        writer.write_sample(right[i] as f32)?;
137    }
138    writer.finalize()?;
139    Ok(())
140}
141
142// ---------------------------------------------------------------------------
143// Tests
144// ---------------------------------------------------------------------------
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn sine_wave_length() {
152        let buf = sine_wave(440.0, 1.0, 48000);
153        assert_eq!(buf.len(), 48000);
154    }
155
156    #[test]
157    fn sine_wave_amplitude() {
158        let buf = sine_wave(440.0, 1.0, 48000);
159        let max = buf.iter().copied().fold(0.0_f64, |a, b| a.max(b.abs()));
160        assert!((max - 0.5).abs() < 0.01, "expected peak ≈ 0.5, got {max}");
161    }
162
163    #[test]
164    fn guitar_pluck_decays() {
165        let buf = guitar_pluck(82.41, 2.0, 48000);
166        // First 1000 samples should be louder than last 1000
167        let rms_start: f64 = buf[..1000].iter().map(|x| x * x).sum::<f64>() / 1000.0;
168        let rms_end: f64 = buf[buf.len() - 1000..].iter().map(|x| x * x).sum::<f64>() / 1000.0;
169        assert!(rms_start > rms_end * 10.0, "signal should decay");
170    }
171
172    #[test]
173    fn render_wav_roundtrip() {
174        let tmp = std::env::temp_dir().join("pedalkernel_test_render.wav");
175        let input = sine_wave(440.0, 0.1, 48000);
176        // Compile a real pedal from DSL
177        let pedal = crate::dsl::parse_pedal_file(include_str!(
178            "../examples/pedals/distortion/proco_rat.pedal"
179        ))
180        .expect("should parse RAT pedal");
181        let mut compiled =
182            crate::compiler::compile_pedal(&pedal, 48000.0).expect("should compile RAT pedal");
183        render_to_wav(&mut compiled, &input, &tmp, 48000).unwrap();
184
185        // Verify the file was created and has correct length
186        let reader = hound::WavReader::open(&tmp).unwrap();
187        assert_eq!(reader.spec().sample_rate, 48000);
188        assert_eq!(reader.len(), input.len() as u32);
189        let _ = std::fs::remove_file(&tmp);
190    }
191}