audio_lab/signal/test_signal/
sweep.rs1use 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}