Skip to main content

pedalkernel_validate/
signals.rs

1//! Deterministic test signal generators for audio validation.
2//!
3//! This module provides functions to generate standard test signals used in
4//! audio circuit validation. All generators are deterministic (no randomness)
5//! to ensure reproducible test results.
6//!
7//! # Available Signals
8//!
9//! | Signal | Use Case |
10//! |--------|----------|
11//! | [`impulse`] | Impulse response measurement, FIR characterization |
12//! | [`sine`] | THD measurement, frequency response at single point |
13//! | [`two_tone`] | IMD (Intermodulation Distortion) testing |
14//! | [`exp_sweep`] | Frequency response measurement, system identification |
15//! | [`silence`] | DC drift testing, noise floor measurement |
16//! | [`tone_burst`] | Attack/release timing, transient response |
17//! | [`level_sweep`] | Gain curves, compression characteristics |
18//!
19//! # Example
20//!
21//! ```rust
22//! use pedalkernel_validate::signals;
23//!
24//! let sr = 48000.0;
25//!
26//! // Generate a 1kHz test tone
27//! let tone = signals::sine(sr, 1000.0, 0.1, 1.0);
28//!
29//! // Generate a frequency sweep for response measurement
30//! let sweep = signals::exp_sweep(sr, 20.0, 20000.0, 1.0, 0.8);
31//! ```
32
33use std::f64::consts::PI;
34
35/// Generate an impulse (unit sample at t=0).
36pub fn impulse(n_samples: usize, amplitude: f64) -> Vec<f64> {
37    let mut signal = vec![0.0; n_samples];
38    if !signal.is_empty() {
39        signal[0] = amplitude;
40    }
41    signal
42}
43
44/// Generate a sine wave.
45pub fn sine(sample_rate: f64, frequency: f64, duration: f64, amplitude: f64) -> Vec<f64> {
46    let n_samples = (duration * sample_rate) as usize;
47    (0..n_samples)
48        .map(|i| {
49            let t = i as f64 / sample_rate;
50            amplitude * (2.0 * PI * frequency * t).sin()
51        })
52        .collect()
53}
54
55/// Generate a two-tone signal for IMD (intermodulation distortion) testing.
56pub fn two_tone(sample_rate: f64, f1: f64, f2: f64, duration: f64, amplitude: f64) -> Vec<f64> {
57    let n_samples = (duration * sample_rate) as usize;
58    (0..n_samples)
59        .map(|i| {
60            let t = i as f64 / sample_rate;
61            amplitude * 0.5 * ((2.0 * PI * f1 * t).sin() + (2.0 * PI * f2 * t).sin())
62        })
63        .collect()
64}
65
66/// Generate an exponential (logarithmic) frequency sweep.
67///
68/// Sweeps from `f_start` to `f_end` over `duration` seconds.
69/// This is the standard sweep for frequency response measurement.
70pub fn exp_sweep(
71    sample_rate: f64,
72    f_start: f64,
73    f_end: f64,
74    duration: f64,
75    amplitude: f64,
76) -> Vec<f64> {
77    let n_samples = (duration * sample_rate) as usize;
78    let log_ratio = (f_end / f_start).ln();
79
80    (0..n_samples)
81        .map(|i| {
82            let t = i as f64 / sample_rate;
83            // Instantaneous frequency: f(t) = f_start * exp(t * ln(f_end/f_start) / duration)
84            // Phase integral: phi(t) = 2*pi * f_start * duration / ln(f_end/f_start) * (exp(...) - 1)
85            let phase = 2.0 * PI * f_start * duration / log_ratio
86                * ((t * log_ratio / duration).exp() - 1.0);
87            amplitude * phase.sin()
88        })
89        .collect()
90}
91
92/// Generate silence (zeros).
93pub fn silence(n_samples: usize) -> Vec<f64> {
94    vec![0.0; n_samples]
95}
96
97/// Generate a tone burst for attack/release timing measurement.
98///
99/// Creates a signal with `repetitions` bursts of a sine wave,
100/// each `on_ms` long followed by `off_ms` of silence.
101pub fn tone_burst(
102    sample_rate: f64,
103    frequency: f64,
104    amplitude: f64,
105    on_ms: f64,
106    off_ms: f64,
107    repetitions: usize,
108) -> Vec<f64> {
109    let on_samples = (on_ms * sample_rate / 1000.0) as usize;
110    let off_samples = (off_ms * sample_rate / 1000.0) as usize;
111    let period_samples = on_samples + off_samples;
112    let total_samples = period_samples * repetitions;
113
114    let mut signal = vec![0.0; total_samples];
115
116    for rep in 0..repetitions {
117        let start = rep * period_samples;
118        for i in 0..on_samples {
119            let t = i as f64 / sample_rate;
120            signal[start + i] = amplitude * (2.0 * PI * frequency * t).sin();
121        }
122        // off_samples remain zero
123    }
124
125    signal
126}
127
128/// Convert dBVU to peak voltage.
129///
130/// 0 dBVU ≈ +4 dBu ≈ 1.228 Vrms ≈ 1.736 Vpeak
131/// We use 0.7746 as the reference (simplification).
132pub fn dbvu_to_peak(dbvu: f64) -> f64 {
133    10.0_f64.powf(dbvu / 20.0) * 0.7746
134}
135
136/// Generate a level sweep for static gain curve measurement.
137///
138/// Generates a series of sine wave segments at different amplitudes,
139/// holding each level for `duration_per_level` seconds.
140pub fn level_sweep(
141    sample_rate: f64,
142    frequency: f64,
143    levels_dbvu: &[f64],
144    duration_per_level: f64,
145) -> Vec<f64> {
146    let samples_per_level = (duration_per_level * sample_rate) as usize;
147    let mut signal = Vec::with_capacity(levels_dbvu.len() * samples_per_level);
148
149    for &level in levels_dbvu {
150        let amplitude = dbvu_to_peak(level);
151        for i in 0..samples_per_level {
152            let t = i as f64 / sample_rate;
153            signal.push(amplitude * (2.0 * PI * frequency * t).sin());
154        }
155    }
156
157    signal
158}
159
160/// Signal specification from config.
161#[derive(Debug, Clone)]
162pub enum SignalSpec {
163    Impulse {
164        amplitude: f64,
165    },
166    Sine {
167        frequency: f64,
168        amplitude: f64,
169        duration: f64,
170    },
171    TwoTone {
172        f1: f64,
173        f2: f64,
174        amplitude: f64,
175        duration: f64,
176    },
177    ExpSweep {
178        f_start: f64,
179        f_end: f64,
180        amplitude: f64,
181        duration: f64,
182    },
183    Silence {
184        duration: f64,
185    },
186    ToneBurst {
187        frequency: f64,
188        amplitude: f64,
189        on_ms: f64,
190        off_ms: f64,
191        repetitions: usize,
192    },
193    LevelSweep {
194        frequency: f64,
195        levels_dbvu: Vec<f64>,
196        duration_per_level: f64,
197    },
198}
199
200impl SignalSpec {
201    /// Generate the signal at the given sample rate.
202    pub fn generate(&self, sample_rate: f64) -> Vec<f64> {
203        match self {
204            SignalSpec::Impulse { amplitude } => {
205                impulse((sample_rate * 0.1) as usize, *amplitude) // 100ms default
206            }
207            SignalSpec::Sine {
208                frequency,
209                amplitude,
210                duration,
211            } => sine(sample_rate, *frequency, *duration, *amplitude),
212            SignalSpec::TwoTone {
213                f1,
214                f2,
215                amplitude,
216                duration,
217            } => two_tone(sample_rate, *f1, *f2, *duration, *amplitude),
218            SignalSpec::ExpSweep {
219                f_start,
220                f_end,
221                amplitude,
222                duration,
223            } => exp_sweep(sample_rate, *f_start, *f_end, *duration, *amplitude),
224            SignalSpec::Silence { duration } => silence((duration * sample_rate) as usize),
225            SignalSpec::ToneBurst {
226                frequency,
227                amplitude,
228                on_ms,
229                off_ms,
230                repetitions,
231            } => tone_burst(
232                sample_rate,
233                *frequency,
234                *amplitude,
235                *on_ms,
236                *off_ms,
237                *repetitions,
238            ),
239            SignalSpec::LevelSweep {
240                frequency,
241                levels_dbvu,
242                duration_per_level,
243            } => level_sweep(sample_rate, *frequency, levels_dbvu, *duration_per_level),
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn impulse_has_correct_shape() {
254        let sig = impulse(100, 1.0);
255        assert_eq!(sig.len(), 100);
256        assert_eq!(sig[0], 1.0);
257        assert!(sig[1..].iter().all(|&x| x == 0.0));
258    }
259
260    #[test]
261    fn sine_has_correct_frequency() {
262        let sr = 48000.0;
263        let freq = 1000.0;
264        let sig = sine(sr, freq, 0.01, 1.0);
265
266        // Should have ~10 cycles in 10ms at 1kHz
267        let zero_crossings: usize = sig.windows(2).filter(|w| w[0] * w[1] < 0.0).count();
268        // Each cycle has 2 zero crossings, 10 cycles = ~20 crossings
269        assert!((18..=22).contains(&zero_crossings));
270    }
271
272    #[test]
273    fn exp_sweep_starts_and_ends_correctly() {
274        let sr = 48000.0;
275        let sig = exp_sweep(sr, 20.0, 20000.0, 1.0, 1.0);
276        assert_eq!(sig.len(), 48000);
277        // Just verify it doesn't explode
278        assert!(sig.iter().all(|&x| x.is_finite()));
279        assert!(sig.iter().map(|x| x.abs()).fold(0.0f64, |a, b| a.max(b)) <= 1.01);
280    }
281
282    #[test]
283    fn tone_burst_has_correct_structure() {
284        let sr = 48000.0;
285        let sig = tone_burst(sr, 1000.0, 1.0, 10.0, 90.0, 2);
286
287        // 10ms on + 90ms off = 100ms per rep, 2 reps = 200ms = 9600 samples
288        assert_eq!(sig.len(), 9600);
289
290        // First 480 samples (10ms) should have signal
291        let first_burst_energy: f64 = sig[0..480].iter().map(|x| x * x).sum();
292        assert!(first_burst_energy > 0.1);
293
294        // Next 4320 samples (90ms) should be silent
295        let first_silence: f64 = sig[480..4800].iter().map(|x| x * x).sum();
296        assert!(first_silence < 1e-10);
297    }
298}