Skip to main content

audio_lab/dsp/
deconvolution.rs

1use std::{f64::consts::PI, ops::Range};
2
3use ndarray::Array1;
4use thiserror::Error;
5
6use crate::signal::{FreqSignal, SignalError, TimeSignal};
7
8use crate::dsp::{PadMode, pad_zeros};
9
10#[derive(Debug, Error)]
11pub enum DeconvolutionError {
12    #[error("sample rates do not match: output has {output} Hz, input has {input} Hz")]
13    SampleRateMismatch { output: f64, input: f64 },
14    #[error(
15        "channel counts do not match: output has {output} channels, input has {input} channels"
16    )]
17    ChannelMismatch { output: usize, input: usize },
18    #[error(
19        "fft length ({fft_len}) is shorter than signal length (output: {output_len}, input: {input_len})"
20    )]
21    FftLengthTooShort {
22        fft_len: usize,
23        output_len: usize,
24        input_len: usize,
25    },
26    #[error(
27        "invalid frequency range: {start} Hz to {end} Hz (must satisfy 0 <= start < end <= {nyquist} Hz)"
28    )]
29    InvalidFrequencyRange { start: f64, end: f64, nyquist: f64 },
30    #[error("signals must not be empty")]
31    EmptySignal,
32    #[error(transparent)]
33    Signal(#[from] SignalError),
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub struct DeconvolutionConfig {
38    fft_len: Option<usize>,
39    regularized_band: Option<Range<f64>>,
40    regularization_outside_band: f64,
41    regularization_inside_band: f64,
42}
43
44impl Default for DeconvolutionConfig {
45    fn default() -> Self {
46        Self {
47            fft_len: None,
48            regularized_band: None,
49            regularization_outside_band: 1.0,
50            regularization_inside_band: 10_f64.powf(-200.0 / 20.0),
51        }
52    }
53}
54
55impl DeconvolutionConfig {
56    pub fn with_fft_len(mut self, fft_len: usize) -> Self {
57        self.fft_len = Some(fft_len);
58        self
59    }
60
61    pub fn with_regularized_band(mut self, regularized_band: Range<f64>) -> Self {
62        self.regularized_band = Some(regularized_band);
63        self
64    }
65
66    pub fn with_frequency_range(self, frequency_range: Range<f64>) -> Self {
67        self.with_regularized_band(frequency_range)
68    }
69
70    pub fn with_regularization(mut self, outside_band: f64, inside_band: f64) -> Self {
71        self.regularization_outside_band = outside_band;
72        self.regularization_inside_band = inside_band;
73        self
74    }
75}
76
77pub fn deconvolve(
78    system_output: &TimeSignal,
79    system_input: &TimeSignal,
80    config: &DeconvolutionConfig,
81) -> Result<TimeSignal, DeconvolutionError> {
82    if system_output.sample_rate() != system_input.sample_rate() {
83        return Err(DeconvolutionError::SampleRateMismatch {
84            output: system_output.sample_rate(),
85            input: system_input.sample_rate(),
86        });
87    }
88
89    if system_output.num_channels() != system_input.num_channels() {
90        return Err(DeconvolutionError::ChannelMismatch {
91            output: system_output.num_channels(),
92            input: system_input.num_channels(),
93        });
94    }
95
96    let output_len = system_output.num_time_steps();
97    let input_len = system_input.num_time_steps();
98    if output_len == 0 || input_len == 0 {
99        return Err(DeconvolutionError::EmptySignal);
100    }
101
102    let fft_len = config.fft_len.unwrap_or(output_len.max(input_len));
103    if fft_len < output_len || fft_len < input_len {
104        return Err(DeconvolutionError::FftLengthTooShort {
105            fft_len,
106            output_len,
107            input_len,
108        });
109    }
110
111    let padded_output = pad_zeros(system_output, fft_len - output_len, PadMode::After)?;
112    let padded_input = pad_zeros(system_input, fft_len - input_len, PadMode::After)?;
113
114    let output_freq = padded_output.into_freq();
115    let inverse_input = regularized_spectrum_inversion(&padded_input.into_freq(), config)?;
116    let mut response_freq = FreqSignal::zeros(
117        output_freq.num_channels(),
118        output_freq.num_freq_bins(),
119        output_freq.sample_rate(),
120        Some(fft_len),
121    )?;
122
123    for ((mut out_channel, output_channel), inverse_channel) in response_freq
124        .channel_iter_mut()
125        .zip(output_freq.channel_iter())
126        .zip(inverse_input.channel_iter())
127    {
128        out_channel.assign(&(&output_channel * &inverse_channel));
129    }
130
131    Ok(response_freq.into_time())
132}
133
134fn regularized_spectrum_inversion(
135    signal: &FreqSignal,
136    config: &DeconvolutionConfig,
137) -> Result<FreqSignal, DeconvolutionError> {
138    let nyquist = signal.nyquist();
139    let regularized_band = config.regularized_band.clone().unwrap_or(0.0..nyquist);
140
141    validate_frequency_range(&regularized_band, nyquist)?;
142
143    let mut regularization =
144        Array1::from_elem(signal.num_freq_bins(), config.regularization_inside_band);
145
146    let outside = Array1::from_elem(signal.num_freq_bins(), config.regularization_outside_band);
147    let lower_crossfade_start = signal.nearest_freq_index(regularized_band.start / 2_f64.sqrt());
148    let lower_crossfade_end = signal.nearest_freq_index(regularized_band.start);
149    regularization = crossfade(
150        &outside,
151        &regularization,
152        lower_crossfade_start..lower_crossfade_end,
153    );
154
155    if regularized_band.end < nyquist {
156        let upper_crossfade_start = signal.nearest_freq_index(regularized_band.end);
157        let upper_crossfade_end =
158            signal.nearest_freq_index((regularized_band.end * 2_f64.sqrt()).min(nyquist));
159        regularization = crossfade(
160            &regularization,
161            &outside,
162            upper_crossfade_start..upper_crossfade_end,
163        );
164    }
165
166    let max_squared_magnitude = max_squared_magnitude(signal);
167    regularization *= max_squared_magnitude;
168
169    let mut inverse = FreqSignal::zeros(
170        signal.num_channels(),
171        signal.num_freq_bins(),
172        signal.sample_rate(),
173        Some(signal.num_time_steps()),
174    )?;
175
176    for ((mut out_channel, in_channel), reg_channel) in inverse
177        .channel_iter_mut()
178        .zip(signal.channel_iter())
179        .zip(std::iter::repeat(&regularization))
180    {
181        for ((out_bin, in_bin), reg) in out_channel
182            .iter_mut()
183            .zip(in_channel.iter())
184            .zip(reg_channel.iter())
185        {
186            *out_bin = in_bin.conj() / (in_bin.norm_sqr() + *reg);
187        }
188    }
189
190    Ok(inverse)
191}
192
193fn validate_frequency_range(range: &Range<f64>, nyquist: f64) -> Result<(), DeconvolutionError> {
194    if range.start < 0.0 || range.start >= range.end || range.end > nyquist {
195        return Err(DeconvolutionError::InvalidFrequencyRange {
196            start: range.start,
197            end: range.end,
198            nyquist,
199        });
200    }
201
202    Ok(())
203}
204
205fn max_squared_magnitude(signal: &FreqSignal) -> f64 {
206    signal
207        .freq_data()
208        .iter()
209        .map(|value| value.norm_sqr())
210        .fold(0.0_f64, f64::max)
211}
212
213fn crossfade(first: &Array1<f64>, second: &Array1<f64>, range: Range<usize>) -> Array1<f64> {
214    assert_eq!(first.len(), second.len());
215    assert!(range.start <= first.len());
216    assert!(range.end <= first.len());
217
218    let len = first.len();
219    let crossfade_len = range.end.saturating_sub(range.start);
220    let mut output = Array1::zeros(len);
221
222    for i in 0..range.start {
223        output[i] = first[i];
224    }
225
226    if crossfade_len > 0 {
227        for step in 0..crossfade_len {
228            let position = (step + 1) as f64 / (crossfade_len + 1) as f64;
229            let second_weight = 0.5 - 0.5 * (PI * position).cos();
230            let first_weight = 1.0 - second_weight;
231            let idx = range.start + step;
232            output[idx] = first[idx] * first_weight + second[idx] * second_weight;
233        }
234    }
235
236    for i in range.end..len {
237        output[i] = second[i];
238    }
239
240    output
241}
242
243#[cfg(test)]
244mod tests {
245    use approx::assert_abs_diff_eq;
246    use ndarray::{arr1, arr2};
247
248    use super::*;
249
250    #[test]
251    fn deconvolve_recovers_known_response_for_impulse_input() {
252        let system_input = TimeSignal::new(arr2(&[[1.0, 0.0, 0.0, 0.0]]), 48_000.0).unwrap();
253        let expected_response =
254            TimeSignal::new(arr2(&[[0.5, -0.25, 0.125, 0.0]]), 48_000.0).unwrap();
255
256        let recovered = deconvolve(
257            &expected_response,
258            &system_input,
259            &DeconvolutionConfig::default(),
260        )
261        .unwrap();
262
263        assert_abs_diff_eq!(
264            recovered.channel(0),
265            expected_response.channel(0),
266            epsilon = 1e-9
267        );
268    }
269
270    #[test]
271    fn deconvolve_rejects_sample_rate_mismatch() {
272        let system_output = TimeSignal::zeros(1, 16, 44_100.0).unwrap();
273        let system_input = TimeSignal::zeros(1, 16, 48_000.0).unwrap();
274
275        let result = deconvolve(
276            &system_output,
277            &system_input,
278            &DeconvolutionConfig::default(),
279        );
280
281        assert!(matches!(
282            result,
283            Err(DeconvolutionError::SampleRateMismatch {
284                output: 44_100.0,
285                input: 48_000.0
286            })
287        ));
288    }
289
290    #[test]
291    fn deconvolve_rejects_channel_mismatch() {
292        let system_output = TimeSignal::zeros(2, 16, 48_000.0).unwrap();
293        let system_input = TimeSignal::zeros(1, 16, 48_000.0).unwrap();
294
295        let result = deconvolve(
296            &system_output,
297            &system_input,
298            &DeconvolutionConfig::default(),
299        );
300
301        assert!(matches!(
302            result,
303            Err(DeconvolutionError::ChannelMismatch {
304                output: 2,
305                input: 1
306            })
307        ));
308    }
309
310    #[test]
311    fn deconvolve_rejects_fft_lengths_shorter_than_inputs() {
312        let system_output = TimeSignal::zeros(1, 16, 48_000.0).unwrap();
313        let system_input = TimeSignal::zeros(1, 8, 48_000.0).unwrap();
314
315        let result = deconvolve(
316            &system_output,
317            &system_input,
318            &DeconvolutionConfig::default().with_fft_len(15),
319        );
320
321        assert!(matches!(
322            result,
323            Err(DeconvolutionError::FftLengthTooShort {
324                fft_len: 15,
325                output_len: 16,
326                input_len: 8
327            })
328        ));
329    }
330
331    #[test]
332    fn deconvolve_rejects_invalid_frequency_ranges() {
333        let system_output = TimeSignal::zeros(1, 16, 48_000.0).unwrap();
334        let system_input = TimeSignal::zeros(1, 16, 48_000.0).unwrap();
335
336        let result = deconvolve(
337            &system_output,
338            &system_input,
339            &DeconvolutionConfig::default().with_regularized_band(2_000.0..1_000.0),
340        );
341        assert!(matches!(
342            result,
343            Err(DeconvolutionError::InvalidFrequencyRange { .. })
344        ));
345
346        let result = deconvolve(
347            &system_output,
348            &system_input,
349            &DeconvolutionConfig::default().with_regularized_band(100.0..30_000.0),
350        );
351        assert!(matches!(
352            result,
353            Err(DeconvolutionError::InvalidFrequencyRange { .. })
354        ));
355    }
356
357    #[test]
358    fn spectrum_inversion_matches_complex_formula() {
359        use num::complex::Complex64;
360
361        let signal = TimeSignal::new(
362            arr2(&[[0.8, -0.3, 0.5, 0.1, -0.2, 0.05, 0.0, 0.3]]),
363            48_000.0,
364        )
365        .unwrap()
366        .into_freq();
367        // Same factor inside and outside the band gives a constant regularization.
368        let factor = 1e-2;
369        let config = DeconvolutionConfig::default()
370            .with_regularized_band(3_000.0..12_000.0)
371            .with_regularization(factor, factor);
372
373        let inverse = regularized_spectrum_inversion(&signal, &config).unwrap();
374
375        let reg = factor * max_squared_magnitude(&signal);
376        for (h, inv) in signal.channel(0).iter().zip(inverse.channel(0).iter()) {
377            let expected = h.conj() / (h.conj() * *h + Complex64::from(reg));
378            assert_abs_diff_eq!(inv.re, expected.re, epsilon = 1e-12);
379            assert_abs_diff_eq!(inv.im, expected.im, epsilon = 1e-12);
380        }
381    }
382
383    #[test]
384    fn crossfade_transitions_between_arrays() {
385        let first = arr1(&[1.0, 1.0, 1.0, 1.0, 1.0]);
386        let second = arr1(&[0.0, 0.0, 0.0, 0.0, 0.0]);
387
388        let output = crossfade(&first, &second, 1..4);
389
390        assert_abs_diff_eq!(output[0], 1.0, epsilon = 1e-12);
391        assert!(output[1] < 1.0 && output[1] > output[2]);
392        assert_abs_diff_eq!(output[2], 0.5, epsilon = 1e-12);
393        assert!(output[3] > 0.0 && output[3] < output[2]);
394        assert_abs_diff_eq!(output[4], 0.0, epsilon = 1e-12);
395    }
396}