Skip to main content

audio_lab/signal/test_signal/
sweep.rs

1use std::{f64::consts::PI, ops::Range};
2
3use ndarray::Array1;
4
5use crate::signal::{SignalError, TimeSignal};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum SweepType {
9    Linear,
10    Exponential,
11}
12
13#[derive(Debug, thiserror::Error)]
14pub enum SweepError {
15    #[error("Frequency range exceeds Nyquist frequency")]
16    Nyquist,
17    #[error("Start of frequency range must be smaller than end")]
18    FreqRange,
19    #[error("Fade in is longer than sweep")]
20    FadeIn,
21    #[error("Fade out is longer than sweep")]
22    FadeOut,
23    #[error(transparent)]
24    Signal(#[from] SignalError),
25}
26
27#[derive(Debug, Clone, Copy)]
28pub struct SweepConfig {
29    pub amplitude: f64,
30    pub sample_rate: f64,
31    pub num_channels: usize,
32    pub fade_in: usize,
33    pub fade_out: usize,
34    pub sweep_type: SweepType,
35}
36
37impl Default for SweepConfig {
38    fn default() -> Self {
39        Self {
40            amplitude: 1.0,
41            sample_rate: 48_000.0,
42            num_channels: 1,
43            fade_in: 0,
44            fade_out: 90,
45            sweep_type: SweepType::Exponential,
46        }
47    }
48}
49
50pub fn generate_sweep(
51    num_time_steps: usize,
52    freq_range: Range<f64>,
53    config: SweepConfig,
54) -> Result<TimeSignal, SweepError> {
55    let SweepConfig {
56        amplitude,
57        sample_rate,
58        num_channels,
59        fade_in,
60        fade_out,
61        sweep_type,
62    } = config;
63
64    if freq_range.end <= freq_range.start {
65        return Err(SweepError::FreqRange);
66    }
67    if freq_range.end >= sample_rate / 2.0 {
68        return Err(SweepError::Nyquist);
69    }
70    if fade_in > num_time_steps {
71        return Err(SweepError::FadeIn);
72    }
73    if fade_out > num_time_steps {
74        return Err(SweepError::FadeOut);
75    }
76
77    let mut signal = match sweep_type {
78        SweepType::Linear => linear_sweep(
79            num_time_steps,
80            freq_range.clone(),
81            amplitude,
82            sample_rate,
83            num_channels,
84        )?,
85        SweepType::Exponential => exponential_sweep(
86            num_time_steps,
87            freq_range.clone(),
88            amplitude,
89            sample_rate,
90            num_channels,
91        )?,
92    };
93
94    if fade_in > 0 {
95        let fade = Array1::linspace(0.0, PI / 2.0, fade_in).mapv(|v| v.sin().powi(2));
96        for mut channel in signal.channel_iter_mut() {
97            for (idx, gain) in fade.iter().enumerate() {
98                channel[idx] *= *gain;
99            }
100        }
101    }
102
103    if fade_out > 0 {
104        let fade = Array1::linspace(0.0, PI / 2.0, fade_out).mapv(|v| v.cos().powi(2));
105        for mut channel in signal.channel_iter_mut() {
106            for (idx, gain) in fade.iter().enumerate() {
107                let frame = num_time_steps - fade_out + idx;
108                channel[frame] *= *gain;
109            }
110        }
111    }
112
113    let comment = format!(
114        "{sweep_type:?} sweep ({:.1} Hz..{:.1} Hz, amplitude = {amplitude})",
115        freq_range.start, freq_range.end
116    );
117    signal.set_comment(Some(&comment));
118    Ok(signal)
119}
120
121fn linear_sweep(
122    num_time_steps: usize,
123    freq_range: Range<f64>,
124    amplitude: f64,
125    sample_rate: f64,
126    num_channels: usize,
127) -> Result<TimeSignal, SignalError> {
128    let duration = num_time_steps as f64 / sample_rate;
129    let mut signal = TimeSignal::zeros(num_channels, num_time_steps, sample_rate)?;
130
131    let waveform = signal.time_steps().mapv(|time| {
132        amplitude
133            * (2.0 * PI * freq_range.start * time
134                + PI * (freq_range.end - freq_range.start) * time.powi(2) / duration)
135                .sin()
136    });
137    for mut channel in signal.channel_iter_mut() {
138        channel.assign(&waveform);
139    }
140
141    Ok(signal)
142}
143
144fn exponential_sweep(
145    num_time_steps: usize,
146    freq_range: Range<f64>,
147    amplitude: f64,
148    sample_rate: f64,
149    num_channels: usize,
150) -> Result<TimeSignal, SignalError> {
151    let c = (freq_range.end / freq_range.start).ln();
152    let l = num_time_steps as f64 / sample_rate / c;
153    let mut signal = TimeSignal::zeros(num_channels, num_time_steps, sample_rate)?;
154
155    let waveform = signal.time_steps().mapv(|time| {
156        amplitude * (2.0 * PI * freq_range.start * l * ((time / l).exp() - 1.0)).sin()
157    });
158    for mut channel in signal.channel_iter_mut() {
159        channel.assign(&waveform);
160    }
161
162    Ok(signal)
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn validates_sweep_range() {
171        assert!(matches!(
172            generate_sweep(128, 1_000.0..500.0, SweepConfig::default()),
173            Err(SweepError::FreqRange)
174        ));
175    }
176
177    #[test]
178    fn validates_fade_in_length() {
179        assert!(matches!(
180            generate_sweep(
181                128,
182                20.0..20_000.0,
183                SweepConfig {
184                    fade_in: 129,
185                    ..Default::default()
186                }
187            ),
188            Err(SweepError::FadeIn)
189        ));
190    }
191
192    #[test]
193    fn applies_fade_in() {
194        use approx::assert_abs_diff_eq;
195
196        let config = SweepConfig {
197            sample_rate: 8_000.0,
198            fade_out: 0,
199            sweep_type: SweepType::Linear,
200            ..Default::default()
201        };
202        let dry = generate_sweep(128, 100.0..1_000.0, config).unwrap();
203        let faded = generate_sweep(
204            128,
205            100.0..1_000.0,
206            SweepConfig {
207                fade_in: 4,
208                ..config
209            },
210        )
211        .unwrap();
212
213        assert_abs_diff_eq!(faded.channel(0)[0], 0.0, epsilon = 1e-12);
214        assert_abs_diff_eq!(
215            faded.channel(0)[1],
216            dry.channel(0)[1] * 0.25,
217            epsilon = 1e-12
218        );
219        assert_abs_diff_eq!(
220            faded.channel(0)[2],
221            dry.channel(0)[2] * 0.75,
222            epsilon = 1e-12
223        );
224        assert_abs_diff_eq!(faded.channel(0)[3], dry.channel(0)[3], epsilon = 1e-12);
225        assert_abs_diff_eq!(faded.channel(0)[4], dry.channel(0)[4], epsilon = 1e-12);
226    }
227
228    #[test]
229    fn all_channels_match_sweep_formula() {
230        use approx::assert_abs_diff_eq;
231
232        let (n, sr, f0, f1, amplitude) = (64, 8_000.0, 100.0, 2_000.0, 0.7);
233        for sweep_type in [SweepType::Linear, SweepType::Exponential] {
234            let signal = generate_sweep(
235                n,
236                f0..f1,
237                SweepConfig {
238                    amplitude,
239                    sample_rate: sr,
240                    num_channels: 3,
241                    fade_out: 0,
242                    sweep_type,
243                    ..Default::default()
244                },
245            )
246            .unwrap();
247
248            let duration = n as f64 / sr;
249            let l = duration / (f1 / f0).ln();
250            for channel in signal.channel_iter() {
251                for (i, sample) in channel.iter().enumerate() {
252                    let t = i as f64 / sr;
253                    let expected = match sweep_type {
254                        SweepType::Linear => {
255                            amplitude
256                                * (2.0 * PI * f0 * t + PI * (f1 - f0) * t * t / duration).sin()
257                        }
258                        SweepType::Exponential => {
259                            amplitude * (2.0 * PI * f0 * l * ((t / l).exp() - 1.0)).sin()
260                        }
261                    };
262                    assert_abs_diff_eq!(*sample, expected, epsilon = 1e-9);
263                }
264            }
265        }
266    }
267
268    #[test]
269    fn generates_linear_sweep() {
270        let signal = generate_sweep(
271            128,
272            20.0..20_000.0,
273            SweepConfig {
274                amplitude: 0.5,
275                num_channels: 2,
276                fade_out: 8,
277                sweep_type: SweepType::Linear,
278                ..Default::default()
279            },
280        )
281        .unwrap();
282        assert_eq!(signal.num_channels(), 2);
283        assert_eq!(signal.num_time_steps(), 128);
284    }
285}