audio_lab/signal/ops/
spectrogram.rs1use ndarray::{Array1, Array3, Zip};
2use num::complex::Complex64;
3
4use crate::signal::Spectrogram;
5
6fn one_sided_scale(spec: &Spectrogram, bin: usize) -> f64 {
7 let last_bin = spec.num_freq_bins().saturating_sub(1);
8 if bin == 0 || (spec.window_size().is_multiple_of(2) && bin == last_bin) {
9 1.0
10 } else {
11 2.0
12 }
13}
14
15fn scaled_per_bin(
18 spec: &Spectrogram,
19 denom: f64,
20 value: impl Fn(&Complex64) -> f64,
21) -> Array3<f64> {
22 let scales = Array1::from_shape_fn(spec.num_freq_bins(), |bin| {
23 one_sided_scale(spec, bin) / denom
24 });
25 Zip::from(spec.data())
26 .and_broadcast(&scales)
27 .map_collect(|c, &scale| value(c) * scale)
28}
29
30fn to_db_in_place(mut values: Array3<f64>, factor: f64, floor_linear: f64) -> Array3<f64> {
31 values.mapv_inplace(|v| factor * v.max(floor_linear).log10());
32 values
33}
34
35impl Spectrogram {
36 pub fn raw_magnitude(&self) -> Array3<f64> {
37 self.data().map(|c| c.norm())
38 }
39
40 pub fn raw_magnitude_db(&self, floor_db: f64) -> Array3<f64> {
41 let floor_linear = 10f64.powf(floor_db / 20.0);
42 self.data()
43 .map(|c| 20.0 * c.norm().max(floor_linear).log10())
44 }
45
46 pub fn amplitude_spectrum(&self) -> Array3<f64> {
47 let coherent_sum = self.normalization().coherent_gain() * self.window_size() as f64;
48 scaled_per_bin(self, coherent_sum, |c| c.norm())
49 }
50
51 pub fn amplitude_spectrum_db(&self, floor_db: f64) -> Array3<f64> {
52 let floor_linear = 10f64.powf(floor_db / 20.0);
53 to_db_in_place(self.amplitude_spectrum(), 20.0, floor_linear)
54 }
55
56 pub fn power_spectrum(&self) -> Array3<f64> {
57 let denom = self.window_size() as f64 * self.normalization().window_energy();
58 scaled_per_bin(self, denom, |c| c.norm_sqr())
59 }
60
61 pub fn power_spectrum_db(&self, floor_db: f64) -> Array3<f64> {
62 let floor_linear = 10f64.powf(floor_db / 10.0);
63 to_db_in_place(self.power_spectrum(), 10.0, floor_linear)
64 }
65
66 pub fn power_spectral_density(&self) -> Array3<f64> {
67 let denom = self.sample_rate() * self.normalization().window_energy();
68 scaled_per_bin(self, denom, |c| c.norm_sqr())
69 }
70
71 pub fn power_spectral_density_db(&self, floor_db: f64) -> Array3<f64> {
72 let floor_linear = 10f64.powf(floor_db / 10.0);
73 to_db_in_place(self.power_spectral_density(), 10.0, floor_linear)
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use approx::assert_abs_diff_eq;
80 use ndarray::Array3;
81 use num::complex::Complex64;
82
83 use crate::dsp::{WindowFn, generate_periodic_window};
84 use crate::signal::Spectrogram;
85
86 fn make_spectrogram(window_size: usize) -> Spectrogram {
87 let num_freq_bins = window_size / 2 + 1;
88 let data = Array3::from_shape_fn((2, 3, num_freq_bins), |(ch, frame, bin)| {
89 Complex64::new(
90 (ch + 1) as f64 * (bin as f64 + 0.5),
91 frame as f64 - bin as f64 * 0.25,
92 )
93 });
94 Spectrogram::new(data, 8_000.0, window_size, window_size / 2, WindowFn::Hann).unwrap()
95 }
96
97 #[test]
98 fn scaled_spectra_match_per_element_formulas() {
99 for window_size in [8, 9] {
101 let spec = make_spectrogram(window_size);
102 let last_bin = spec.num_freq_bins() - 1;
103 let scale = |bin: usize| {
104 if bin == 0 || (window_size % 2 == 0 && bin == last_bin) {
105 1.0
106 } else {
107 2.0
108 }
109 };
110 let window = generate_periodic_window(WindowFn::Hann, window_size);
112 let window_energy: f64 = window.iter().map(|w| w * w).sum();
113 let coherent_sum = window.sum();
114 let power_denom = window_size as f64 * window_energy;
115 let psd_denom = 8_000.0 * window_energy;
116
117 let amplitude = spec.amplitude_spectrum();
118 let amplitude_db = spec.amplitude_spectrum_db(-200.0);
119 let power = spec.power_spectrum();
120 let power_db = spec.power_spectrum_db(-200.0);
121 let psd = spec.power_spectral_density();
122 let psd_db = spec.power_spectral_density_db(-200.0);
123
124 for ((ch, frame, bin), c) in spec.data().indexed_iter() {
125 let idx = [ch, frame, bin];
126 let expected_amplitude = c.norm() * scale(bin) / coherent_sum;
127 let expected_power = c.norm_sqr() * scale(bin) / power_denom;
128 let expected_psd = c.norm_sqr() * scale(bin) / psd_denom;
129
130 assert_abs_diff_eq!(amplitude[idx], expected_amplitude, epsilon = 1e-12);
131 assert_abs_diff_eq!(power[idx], expected_power, epsilon = 1e-12);
132 assert_abs_diff_eq!(psd[idx], expected_psd, epsilon = 1e-12);
133 assert_abs_diff_eq!(
134 amplitude_db[idx],
135 20.0 * expected_amplitude.log10(),
136 epsilon = 1e-9
137 );
138 assert_abs_diff_eq!(power_db[idx], 10.0 * expected_power.log10(), epsilon = 1e-9);
139 assert_abs_diff_eq!(psd_db[idx], 10.0 * expected_psd.log10(), epsilon = 1e-9);
140 }
141 }
142 }
143}