1use crate::util::{self, pow_db};
5use ferraus_core::AudioBuffer;
6use serde::Serialize;
7
8#[derive(Clone, Debug, Serialize)]
10pub struct StereoBand {
11 pub low_hz: f64,
12 pub high_hz: f64,
13 pub correlation: f64,
15 pub width: f64,
17 pub mono_loss_db: f64,
19 pub level_db: f64,
21}
22
23#[derive(Clone, Debug, Serialize)]
25pub struct StereoReport {
26 pub channels: usize,
27 pub correlation: f64,
29 pub correlation_below_120hz: f64,
31 pub side_to_mid_db: f64,
33 pub width: f64,
35 pub mono_loss_db: f64,
37 pub mono_compatibility: f64,
39 pub balance_db: f64,
41 pub bands: Vec<StereoBand>,
42}
43
44pub const STEREO_BANDS: [(f64, f64); 8] = [
46 (20.0, 120.0),
47 (120.0, 250.0),
48 (250.0, 500.0),
49 (500.0, 1000.0),
50 (1000.0, 2000.0),
51 (2000.0, 4000.0),
52 (4000.0, 8000.0),
53 (8000.0, 20000.0),
54];
55
56pub fn correlation(a: &[f32], b: &[f32]) -> f64 {
58 let (mut ab, mut aa, mut bb) = (0.0f64, 0.0f64, 0.0f64);
59 for (x, y) in a.iter().zip(b) {
60 let (x, y) = (*x as f64, *y as f64);
61 ab += x * y;
62 aa += x * x;
63 bb += y * y;
64 }
65 if aa <= 1e-20 || bb <= 1e-20 {
66 return if aa <= 1e-20 && bb <= 1e-20 { 1.0 } else { 0.0 };
67 }
68 ab / (aa * bb).sqrt()
69}
70
71pub fn low_correlation(buf: &AudioBuffer, cutoff_hz: f64) -> f64 {
73 if buf.channels() < 2 {
74 return 1.0;
75 }
76 let sr = buf.sample_rate as f64;
77 let l = util::highpass4(&util::lowpass4(&buf.data[0], sr, cutoff_hz), sr, 20.0);
79 let r = util::highpass4(&util::lowpass4(&buf.data[1], sr, cutoff_hz), sr, 20.0);
80 correlation(&l, &r)
81}
82
83pub fn stereo_analysis(buf: &AudioBuffer) -> StereoReport {
85 let sr = buf.sample_rate as f64;
86 let nyq = sr / 2.0;
87 if buf.channels() < 2 {
88 let e: f64 = buf.data[0].iter().map(|v| (*v as f64).powi(2)).sum::<f64>() / buf.frames().max(1) as f64;
89 return StereoReport {
90 channels: buf.channels(),
91 correlation: 1.0,
92 correlation_below_120hz: 1.0,
93 side_to_mid_db: f64::NEG_INFINITY,
94 width: 0.0,
95 mono_loss_db: 0.0,
96 mono_compatibility: 1.0,
97 balance_db: 0.0,
98 bands: STEREO_BANDS
99 .iter()
100 .filter(|(lo, _)| *lo < nyq)
101 .map(|(lo, hi)| StereoBand {
102 low_hz: *lo,
103 high_hz: hi.min(nyq),
104 correlation: 1.0,
105 width: 0.0,
106 mono_loss_db: 0.0,
107 level_db: pow_db(e),
108 })
109 .collect(),
110 };
111 }
112 let dc = |c: &[f32]| (c.iter().map(|v| *v as f64).sum::<f64>() / c.len().max(1) as f64) as f32;
114 let (dl, dr) = (dc(&buf.data[0]), dc(&buf.data[1]));
115 let l: Vec<f32> = buf.data[0].iter().map(|v| v - dl).collect();
116 let r: Vec<f32> = buf.data[1].iter().map(|v| v - dr).collect();
117 let (l, r) = (&l, &r);
118 let n = buf.frames().max(1) as f64;
119 let (mut el, mut er, mut em, mut es) = (0.0f64, 0.0, 0.0, 0.0);
120 for (a, b) in l.iter().zip(r) {
121 let (a, b) = (*a as f64, *b as f64);
122 el += a * a;
123 er += b * b;
124 em += ((a + b) / 2.0).powi(2);
125 es += ((a - b) / 2.0).powi(2);
126 }
127 let corr = correlation(l, r);
128 let corr_low = low_correlation(buf, 120.0);
129 let width = if em + es > 1e-20 { es / (em + es) } else { 0.0 };
130 let mono_loss = if el + er > 1e-20 { pow_db(2.0 * em / (el + er)).max(-120.0) } else { 0.0 };
131
132 let n_fft = 4096.min(util::next_pow2(buf.frames().max(512)));
134 let hop = n_fft / 2;
135 let fl = util::stft_complex(l, n_fft, hop);
136 let fr = util::stft_complex(r, n_fft, hop);
137 let df = sr / n_fft as f64;
138 let mut bands = Vec::new();
139 let mut score_acc = 0.0;
140 let mut weight_acc = 0.0;
141 for (lo, hi) in STEREO_BANDS.iter().filter(|(lo, _)| *lo < nyq) {
142 let hi = hi.min(nyq);
143 let k0 = ((lo / df).ceil() as usize).max(1);
144 let k1 = ((hi / df).floor() as usize).min(n_fft / 2);
145 let (mut sl, mut sr_, mut x, mut sm, mut ss) = (0.0f64, 0.0, 0.0, 0.0, 0.0);
146 for (a, b) in fl.iter().zip(&fr) {
147 for k in k0..=k1.max(k0) {
148 let (p, q) = (a[k], b[k]);
149 sl += p.norm_sqr() as f64;
150 sr_ += q.norm_sqr() as f64;
151 x += (p * q.conj()).re as f64;
152 sm += ((p + q) * 0.5).norm_sqr() as f64;
153 ss += ((p - q) * 0.5).norm_sqr() as f64;
154 }
155 }
156 let c = if sl > 1e-20 && sr_ > 1e-20 { x / (sl * sr_).sqrt() } else { 1.0 };
157 let w = if sm + ss > 1e-20 { ss / (sm + ss) } else { 0.0 };
158 let loss = if sl + sr_ > 1e-20 { pow_db(2.0 * sm / (sl + sr_)).max(-120.0) } else { 0.0 };
159 let energy = (sl + sr_) / 2.0;
160 let ms = energy * 2.0 / (n_fft as f64 * (3.0 * n_fft as f64 / 8.0)) / fl.len().max(1) as f64;
162 score_acc += energy * (1.0 + loss / 6.0).clamp(0.0, 1.0);
163 weight_acc += energy;
164 bands.push(StereoBand {
165 low_hz: *lo,
166 high_hz: hi,
167 correlation: c,
168 width: w,
169 mono_loss_db: loss,
170 level_db: pow_db(ms),
171 });
172 }
173 let mono_compat = if weight_acc > 0.0 { score_acc / weight_acc } else { 1.0 };
174 StereoReport {
175 channels: buf.channels(),
176 correlation: corr,
177 correlation_below_120hz: corr_low,
178 side_to_mid_db: if em > 1e-20 && es > 1e-20 {
179 10.0 * (es / em).log10()
180 } else if es <= 1e-20 {
181 f64::NEG_INFINITY
182 } else {
183 f64::INFINITY
184 },
185 width,
186 mono_loss_db: mono_loss,
187 mono_compatibility: mono_compat,
188 balance_db: pow_db(el / n) - pow_db(er / n),
189 bands,
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use ferraus_core::testing::{noise, sine, stereo};
197
198 #[test]
199 fn mono_identical_and_out_of_phase() {
200 let s = stereo(sine(48000, 440.0, 1.0, 0.5));
201 let r = stereo_analysis(&s);
202 assert!((r.correlation - 1.0).abs() < 1e-6);
203 assert!(r.width < 1e-6);
204 assert!(r.mono_compatibility > 0.99);
205 let mut o = s.clone();
206 o.data[1].iter_mut().for_each(|v| *v = -*v);
207 let r = stereo_analysis(&o);
208 assert!(r.correlation < -0.99);
209 assert!(r.width > 0.99);
210 assert!(r.mono_compatibility < 0.05);
211 let m = stereo_analysis(&sine(48000, 440.0, 1.0, 0.5));
212 assert_eq!(m.correlation, 1.0);
213 }
214
215 #[test]
216 fn uncorrelated_and_low_band() {
217 let a = noise(48000, 2.0, 0.3, 1);
218 let b = noise(48000, 2.0, 0.3, 2);
219 let buf = AudioBuffer::from_planar(48000, vec![a.data[0].clone(), b.data[0].clone()]);
220 let r = stereo_analysis(&buf);
221 assert!(r.correlation.abs() < 0.05);
222 assert!((r.width - 0.5).abs() < 0.05);
223 assert!((r.mono_loss_db + 3.0).abs() < 0.3);
224 assert!((r.mono_compatibility - 0.5).abs() < 0.1);
225 let bass = sine(48000, 60.0, 2.0, 0.4).data[0].clone();
227 let hi = sine(48000, 2000.0, 2.0, 0.4).data[0].clone();
228 let l: Vec<f32> = bass.iter().zip(&hi).map(|(b, h)| b + h).collect();
229 let rr: Vec<f32> = bass.iter().zip(&hi).map(|(b, h)| -b + h).collect();
230 let buf = AudioBuffer::from_planar(48000, vec![l, rr]);
231 let r = stereo_analysis(&buf);
232 assert!(r.correlation_below_120hz < -0.9, "{}", r.correlation_below_120hz);
233 assert!(r.bands[0].correlation < -0.9);
234 assert!(r.bands[5].correlation > 0.9);
235 }
236}