1use hound::WavReader;
11use math_audio_iir_fir::{Biquad, BiquadFilterType};
12use rustfft::num_complex::Complex;
13use rustfft::FftPlanner;
14use std::f32::consts::PI;
15use std::io::Write;
16use std::path::Path;
17
18#[derive(Debug, Clone)]
20pub struct MicrophoneCompensation {
21 pub frequencies: Vec<f32>,
23 pub spl_db: Vec<f32>,
25}
26
27impl MicrophoneCompensation {
28 pub fn apply_to_sweep(
43 &self,
44 signal: &[f32],
45 start_freq: f32,
46 end_freq: f32,
47 sample_rate: u32,
48 inverse: bool,
49 ) -> Vec<f32> {
50 let duration = signal.len() as f32 / sample_rate as f32;
51 let mut compensated = Vec::with_capacity(signal.len());
52
53 let debug_points = [0, signal.len() / 4, signal.len() / 2, 3 * signal.len() / 4];
55
56 for (i, &sample) in signal.iter().enumerate() {
57 let t = i as f32 / sample_rate as f32;
58
59 let freq = start_freq * ((t * (end_freq / start_freq).ln()) / duration).exp();
62
63 let comp_db = self.interpolate_at(freq);
65
66 let gain_db = if inverse { -comp_db } else { comp_db };
68
69 let gain = 10_f32.powf(gain_db / 20.0);
71
72 if debug_points.contains(&i) {
74 log::info!(
75 "[apply_to_sweep] t={:.3}s, freq={:.1}Hz, comp_db={:.2}dB, gain_db={:.2}dB, gain={:.3}x",
76 t,
77 freq,
78 comp_db,
79 gain_db,
80 gain
81 );
82 }
83
84 compensated.push(sample * gain);
85 }
86
87 log::info!(
88 "[apply_to_sweep] Processed {} samples, duration={:.2}s",
89 signal.len(),
90 duration
91 );
92 compensated
93 }
94
95 pub fn from_file(path: &Path) -> Result<Self, String> {
101 use std::fs::File;
102 use std::io::{BufRead, BufReader};
103
104 log::debug!("[MicrophoneCompensation] Loading from {:?}", path);
105
106 let file = File::open(path)
107 .map_err(|e| format!("Failed to open compensation file {:?}: {}", path, e))?;
108 let reader = BufReader::new(file);
109
110 let is_txt_file = path
112 .extension()
113 .and_then(|e| e.to_str())
114 .map(|e| e.to_lowercase() == "txt")
115 .unwrap_or(false);
116
117 if is_txt_file {
118 log::info!(
119 "[MicrophoneCompensation] Detected .txt file - assuming space/tab-separated without header"
120 );
121 }
122
123 let mut frequencies = Vec::new();
124 let mut spl_db = Vec::new();
125
126 for (line_num, line) in reader.lines().enumerate() {
127 let line = line.map_err(|e| format!("Failed to read line {}: {}", line_num + 1, e))?;
128 let line = line.trim();
129
130 if line.is_empty() || line.starts_with('#') {
132 continue;
133 }
134
135 if !is_txt_file && line.starts_with("frequency") {
137 continue;
138 }
139
140 if is_txt_file {
142 let first_char = line.chars().next().unwrap_or(' ');
143 if !first_char.is_ascii_digit() && first_char != '-' && first_char != '+' {
144 log::info!(
145 "[MicrophoneCompensation] Skipping non-numeric line {}: '{}'",
146 line_num + 1,
147 line
148 );
149 continue;
150 }
151 }
152
153 let parts: Vec<&str> = if is_txt_file {
155 let comma_parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
158 if comma_parts.len() >= 2
159 && comma_parts[0].parse::<f32>().is_ok()
160 && comma_parts[1].parse::<f32>().is_ok()
161 {
162 comma_parts
163 } else {
164 let tab_parts: Vec<&str> = line.split('\t').map(|s| s.trim()).collect();
166 if tab_parts.len() >= 2
167 && tab_parts[0].parse::<f32>().is_ok()
168 && tab_parts[1].parse::<f32>().is_ok()
169 {
170 tab_parts
171 } else {
172 line.split_whitespace().collect()
174 }
175 }
176 } else {
177 line.split(',').collect()
179 };
180
181 if parts.len() < 2 {
182 let separator = if is_txt_file {
183 "separator (comma/tab/space)"
184 } else {
185 "comma"
186 };
187 return Err(format!(
188 "Invalid format at line {}: expected {} with 2+ values but got '{}'",
189 line_num + 1,
190 separator,
191 line
192 ));
193 }
194
195 let freq: f32 = parts[0]
196 .trim()
197 .parse()
198 .map_err(|e| format!("Invalid frequency at line {}: {}", line_num + 1, e))?;
199 let spl: f32 = parts[1]
200 .trim()
201 .parse()
202 .map_err(|e| format!("Invalid SPL at line {}: {}", line_num + 1, e))?;
203
204 frequencies.push(freq);
205 spl_db.push(spl);
206 }
207
208 if frequencies.is_empty() {
209 return Err(format!("No compensation data found in {:?}", path));
210 }
211
212 for i in 1..frequencies.len() {
214 if frequencies[i] <= frequencies[i - 1] {
215 return Err(format!(
216 "Frequencies must be strictly increasing: found {} after {} at line {}",
217 frequencies[i],
218 frequencies[i - 1],
219 i + 1
220 ));
221 }
222 }
223
224 log::info!(
225 "[MicrophoneCompensation] Loaded {} calibration points: {:.1} Hz - {:.1} Hz",
226 frequencies.len(),
227 frequencies[0],
228 frequencies[frequencies.len() - 1]
229 );
230 log::info!(
231 "[MicrophoneCompensation] SPL range: {:.2} dB to {:.2} dB",
232 spl_db.iter().fold(f32::INFINITY, |a, &b| a.min(b)),
233 spl_db.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b))
234 );
235
236 Ok(Self { frequencies, spl_db })
237 }
238
239 pub fn interpolate_at(&self, freq: f32) -> f32 {
244 if freq < self.frequencies[0] || freq > self.frequencies[self.frequencies.len() - 1] {
245 return 0.0;
247 }
248
249 let idx = match self
251 .frequencies
252 .binary_search_by(|f| f.partial_cmp(&freq).unwrap_or(std::cmp::Ordering::Equal))
253 {
254 Ok(i) => return self.spl_db[i], Err(i) => i,
256 };
257
258 if idx == 0 {
259 return self.spl_db[0];
260 }
261 if idx >= self.frequencies.len() {
262 return self.spl_db[self.frequencies.len() - 1];
263 }
264
265 let f0 = self.frequencies[idx - 1];
267 let f1 = self.frequencies[idx];
268 let s0 = self.spl_db[idx - 1];
269 let s1 = self.spl_db[idx];
270
271 let t = (freq - f0) / (f1 - f0);
272 s0 + t * (s1 - s0)
273 }
274}
275
276#[derive(Debug, Clone)]
282pub struct WavAnalysisConfig {
283 pub num_points: usize,
285 pub min_freq: f32,
287 pub max_freq: f32,
289 pub fft_size: Option<usize>,
291 pub overlap: f32,
293 pub single_fft: bool,
295 pub pink_compensation: bool,
297 pub no_window: bool,
299}
300
301impl Default for WavAnalysisConfig {
302 fn default() -> Self {
303 Self {
304 num_points: 2000,
305 min_freq: 20.0,
306 max_freq: 20000.0,
307 fft_size: None,
308 overlap: 0.5,
309 single_fft: false,
310 pink_compensation: false,
311 no_window: false,
312 }
313 }
314}
315
316impl WavAnalysisConfig {
317 pub fn for_log_sweep() -> Self {
319 Self {
320 single_fft: true,
321 pink_compensation: true,
322 no_window: true,
323 ..Default::default()
324 }
325 }
326
327 pub fn for_impulse_response() -> Self {
329 Self {
330 single_fft: true,
331 ..Default::default()
332 }
333 }
334
335 pub fn for_stationary() -> Self {
337 Self::default()
338 }
339}
340
341#[derive(Debug, Clone)]
343pub struct WavAnalysisOutput {
344 pub frequencies: Vec<f32>,
346 pub magnitude_db: Vec<f32>,
348 pub phase_deg: Vec<f32>,
350}
351
352pub fn analyze_wav_buffer(
362 samples: &[f32],
363 sample_rate: u32,
364 config: &WavAnalysisConfig,
365) -> Result<WavAnalysisOutput, String> {
366 if samples.is_empty() {
367 return Err("Signal is empty".to_string());
368 }
369
370 let fft_size = if config.single_fft {
372 config
373 .fft_size
374 .unwrap_or_else(|| wav_next_power_of_two(samples.len()))
375 } else {
376 config.fft_size.unwrap_or(16384)
377 };
378
379 let (freqs, magnitudes_db, phases_deg) = if config.single_fft {
381 compute_single_fft_spectrum_internal(samples, sample_rate, fft_size, config.no_window)?
382 } else {
383 compute_welch_spectrum_internal(samples, sample_rate, fft_size, config.overlap)?
384 };
385
386 let log_freqs = generate_log_frequencies(config.num_points, config.min_freq, config.max_freq);
388
389 let mut interp_mag = interpolate_log(&freqs, &magnitudes_db, &log_freqs);
391 let interp_phase = interpolate_log(&freqs, &phases_deg, &log_freqs);
392
393 if config.pink_compensation {
395 let ref_freq = 1000.0;
396 for (i, freq) in log_freqs.iter().enumerate() {
397 if *freq > 0.0 {
398 let correction = 10.0 * (freq / ref_freq).log10();
399 interp_mag[i] += correction;
400 }
401 }
402 }
403
404 Ok(WavAnalysisOutput {
405 frequencies: log_freqs,
406 magnitude_db: interp_mag,
407 phase_deg: interp_phase,
408 })
409}
410
411pub fn analyze_wav_file(
420 path: &Path,
421 config: &WavAnalysisConfig,
422) -> Result<WavAnalysisOutput, String> {
423 let (samples, sample_rate) = load_wav_mono_with_rate(path)?;
424 analyze_wav_buffer(&samples, sample_rate, config)
425}
426
427fn load_wav_mono_with_rate(path: &Path) -> Result<(Vec<f32>, u32), String> {
429 let mut reader =
430 WavReader::open(path).map_err(|e| format!("Failed to open WAV file: {}", e))?;
431
432 let spec = reader.spec();
433 let sample_rate = spec.sample_rate;
434 let channels = spec.channels as usize;
435
436 let samples: Result<Vec<f32>, _> = match spec.sample_format {
437 hound::SampleFormat::Float => reader.samples::<f32>().collect(),
438 hound::SampleFormat::Int => {
439 let max_val = (1_i64 << (spec.bits_per_sample - 1)) as f32;
440 reader
441 .samples::<i32>()
442 .map(|s| s.map(|v| v as f32 / max_val))
443 .collect()
444 }
445 };
446
447 let samples = samples.map_err(|e| format!("Failed to read samples: {}", e))?;
448
449 let mono = if channels == 1 {
451 samples
452 } else {
453 samples
454 .chunks(channels)
455 .map(|chunk| chunk.iter().sum::<f32>() / channels as f32)
456 .collect()
457 };
458
459 Ok((mono, sample_rate))
460}
461
462pub fn write_wav_analysis_csv(result: &WavAnalysisOutput, path: &Path) -> Result<(), String> {
468 let mut file =
469 std::fs::File::create(path).map_err(|e| format!("Failed to create CSV: {}", e))?;
470
471 writeln!(file, "frequency_hz,spl_db,phase_deg")
472 .map_err(|e| format!("Failed to write CSV header: {}", e))?;
473
474 for i in 0..result.frequencies.len() {
475 writeln!(
476 file,
477 "{:.2},{:.2},{:.2}",
478 result.frequencies[i], result.magnitude_db[i], result.phase_deg[i]
479 )
480 .map_err(|e| format!("Failed to write CSV row: {}", e))?;
481 }
482
483 Ok(())
484}
485
486fn compute_welch_spectrum_internal(
488 signal: &[f32],
489 sample_rate: u32,
490 fft_size: usize,
491 overlap: f32,
492) -> Result<(Vec<f32>, Vec<f32>, Vec<f32>), String> {
493 if signal.is_empty() {
494 return Err("Signal is empty".to_string());
495 }
496
497 let overlap_samples = (fft_size as f32 * overlap.clamp(0.0, 0.95)) as usize;
498 let hop_size = fft_size - overlap_samples;
499
500 let num_windows = if signal.len() >= fft_size {
501 1 + (signal.len() - fft_size) / hop_size
502 } else {
503 1
504 };
505
506 let num_bins = fft_size / 2;
507 let mut magnitude_sum = vec![0.0_f32; num_bins];
508 let mut phase_real_sum = vec![0.0_f32; num_bins];
509 let mut phase_imag_sum = vec![0.0_f32; num_bins];
510
511 let hann_window: Vec<f32> = (0..fft_size)
513 .map(|i| 0.5 * (1.0 - ((2.0 * PI * i as f32) / (fft_size as f32 - 1.0)).cos()))
514 .collect();
515
516 let window_power: f32 = hann_window.iter().map(|&w| w * w).sum();
517 let scale_factor = 2.0 / window_power;
518
519 let mut planner = FftPlanner::new();
520 let fft = planner.plan_fft_forward(fft_size);
521
522 let mut windowed = vec![0.0_f32; fft_size];
523 let mut buffer = vec![Complex::new(0.0, 0.0); fft_size];
524
525 for window_idx in 0..num_windows {
526 let start = window_idx * hop_size;
527 let end = (start + fft_size).min(signal.len());
528 let window_len = end - start;
529
530 for i in 0..window_len {
532 windowed[i] = signal[start + i] * hann_window[i];
533 }
534 for i in window_len..fft_size {
536 windowed[i] = 0.0;
537 }
538
539 for (i, &val) in windowed.iter().enumerate() {
541 buffer[i] = Complex::new(val, 0.0);
542 }
543
544 fft.process(&mut buffer);
545
546 for i in 0..num_bins {
547 let mag = buffer[i].norm() * scale_factor.sqrt();
548 magnitude_sum[i] += mag * mag;
549 phase_real_sum[i] += buffer[i].re;
550 phase_imag_sum[i] += buffer[i].im;
551 }
552 }
553
554 let magnitudes_db: Vec<f32> = magnitude_sum
555 .iter()
556 .map(|&mag_sq| {
557 let mag = (mag_sq / num_windows as f32).sqrt();
558 if mag > 1e-10 {
559 20.0 * mag.log10()
560 } else {
561 -200.0
562 }
563 })
564 .collect();
565
566 let phases_deg: Vec<f32> = phase_real_sum
567 .iter()
568 .zip(phase_imag_sum.iter())
569 .map(|(&re, &im)| (im / num_windows as f32).atan2(re / num_windows as f32) * 180.0 / PI)
570 .collect();
571
572 let freqs: Vec<f32> = (0..num_bins)
573 .map(|i| i as f32 * sample_rate as f32 / fft_size as f32)
574 .collect();
575
576 Ok((freqs, magnitudes_db, phases_deg))
577}
578
579fn compute_single_fft_spectrum_internal(
581 signal: &[f32],
582 sample_rate: u32,
583 fft_size: usize,
584 no_window: bool,
585) -> Result<(Vec<f32>, Vec<f32>, Vec<f32>), String> {
586 if signal.is_empty() {
587 return Err("Signal is empty".to_string());
588 }
589
590 let mut windowed = vec![0.0_f32; fft_size];
591 let copy_len = signal.len().min(fft_size);
592 windowed[..copy_len].copy_from_slice(&signal[..copy_len]);
593
594 let window_scale_factor = if no_window {
595 1.0
596 } else {
597 let hann_window: Vec<f32> = (0..fft_size)
598 .map(|i| 0.5 * (1.0 - ((2.0 * PI * i as f32) / (fft_size as f32 - 1.0)).cos()))
599 .collect();
600
601 for (i, sample) in windowed.iter_mut().enumerate() {
602 *sample *= hann_window[i];
603 }
604
605 hann_window.iter().map(|&w| w * w).sum::<f32>()
606 };
607
608 let mut buffer: Vec<Complex<f32>> = windowed.iter().map(|&x| Complex::new(x, 0.0)).collect();
609
610 let mut planner = FftPlanner::new();
611 let fft = planner.plan_fft_forward(fft_size);
612 fft.process(&mut buffer);
613
614 let scale_factor = if no_window {
615 (2.0 / fft_size as f32).sqrt()
616 } else {
617 (2.0 / window_scale_factor).sqrt()
618 };
619
620 let num_bins = fft_size / 2;
621 let magnitudes_db: Vec<f32> = buffer[..num_bins]
622 .iter()
623 .map(|c| {
624 let mag = c.norm() * scale_factor;
625 if mag > 1e-10 {
626 20.0 * mag.log10()
627 } else {
628 -200.0
629 }
630 })
631 .collect();
632
633 let phases_deg: Vec<f32> = buffer[..num_bins]
634 .iter()
635 .map(|c| c.arg() * 180.0 / PI)
636 .collect();
637
638 let freqs: Vec<f32> = (0..num_bins)
639 .map(|i| i as f32 * sample_rate as f32 / fft_size as f32)
640 .collect();
641
642 Ok((freqs, magnitudes_db, phases_deg))
643}
644
645fn wav_next_power_of_two(n: usize) -> usize {
647 let mut p = 1;
648 while p < n {
649 p *= 2;
650 }
651 p.min(1048576)
652}
653
654fn generate_log_frequencies(num_points: usize, min_freq: f32, max_freq: f32) -> Vec<f32> {
656 let log_min = min_freq.ln();
657 let log_max = max_freq.ln();
658 let step = (log_max - log_min) / (num_points - 1) as f32;
659
660 (0..num_points)
661 .map(|i| (log_min + i as f32 * step).exp())
662 .collect()
663}
664
665fn interpolate_log(x: &[f32], y: &[f32], x_new: &[f32]) -> Vec<f32> {
667 x_new
668 .iter()
669 .map(|&freq| {
670 let idx = x.iter().position(|&f| f >= freq).unwrap_or(x.len() - 1);
671
672 if idx == 0 {
673 return y[0];
674 }
675
676 let x0 = x[idx - 1];
677 let x1 = x[idx];
678 let y0 = y[idx - 1];
679 let y1 = y[idx];
680
681 if x1 <= x0 {
682 return y0;
683 }
684
685 let t = (freq - x0) / (x1 - x0);
686 y0 + t * (y1 - y0)
687 })
688 .collect()
689}
690
691#[derive(Debug, Clone)]
697pub struct AnalysisResult {
698 pub frequencies: Vec<f32>,
700 pub spl_db: Vec<f32>,
702 pub phase_deg: Vec<f32>,
704 pub estimated_lag_samples: isize,
706 pub impulse_response: Vec<f32>,
708 pub impulse_time_ms: Vec<f32>,
710 pub excess_group_delay_ms: Vec<f32>,
712 pub thd_percent: Vec<f32>,
714 pub harmonic_distortion_db: Vec<Vec<f32>>,
716 pub rt60_ms: Vec<f32>,
718 pub clarity_c50_db: Vec<f32>,
720 pub clarity_c80_db: Vec<f32>,
722 pub spectrogram_db: Vec<Vec<f32>>,
724}
725
726pub fn analyze_recording(
737 recorded_path: &Path,
738 reference_signal: &[f32],
739 sample_rate: u32,
740 sweep_range: Option<(f32, f32)>,
741) -> Result<AnalysisResult, String> {
742 log::info!("[FFT Analysis] Loading recorded file: {:?}", recorded_path);
744 let recorded = load_wav_mono(recorded_path)?;
745 log::info!(
746 "[FFT Analysis] Loaded {} samples from recording",
747 recorded.len()
748 );
749 log::info!(
750 "[FFT Analysis] Reference has {} samples",
751 reference_signal.len()
752 );
753
754 if recorded.is_empty() {
755 return Err("Recorded signal is empty!".to_string());
756 }
757 if reference_signal.is_empty() {
758 return Err("Reference signal is empty!".to_string());
759 }
760
761 let recorded = &recorded[..];
763 let reference = reference_signal;
764
765 let ref_max = reference
767 .iter()
768 .map(|&x| x.abs())
769 .fold(0.0_f32, |a, b| a.max(b));
770 let rec_max = recorded
771 .iter()
772 .map(|&x| x.abs())
773 .fold(0.0_f32, |a, b| a.max(b));
774 let ref_rms = (reference.iter().map(|&x| x * x).sum::<f32>() / reference.len() as f32).sqrt();
775 let rec_rms = (recorded.iter().map(|&x| x * x).sum::<f32>() / recorded.len() as f32).sqrt();
776
777 log::info!(
778 "[FFT Analysis] Reference: max={:.4}, RMS={:.4}",
779 ref_max,
780 ref_rms
781 );
782 log::info!(
783 "[FFT Analysis] Recorded: max={:.4}, RMS={:.4}",
784 rec_max,
785 rec_rms
786 );
787
788 log::info!(
790 "[FFT Analysis] First 5 reference samples: {:?}",
791 &reference[..5.min(reference.len())]
792 );
793 log::info!(
794 "[FFT Analysis] First 5 recorded samples: {:?}",
795 &recorded[..5.min(recorded.len())]
796 );
797
798 let check_len = reference.len().min(recorded.len());
800 let mut identical_count = 0;
801 for (r, c) in reference[..check_len]
802 .iter()
803 .zip(recorded[..check_len].iter())
804 {
805 if (r - c).abs() < 1e-6 {
806 identical_count += 1;
807 }
808 }
809 log::info!(
810 "[FFT Analysis] Identical samples: {}/{} ({:.1}%)",
811 identical_count,
812 check_len,
813 identical_count as f32 * 100.0 / check_len as f32
814 );
815
816 let lag = estimate_lag(reference, recorded)?;
818
819 log::info!(
820 "[FFT Analysis] Estimated lag: {} samples ({:.2} ms)",
821 lag,
822 lag as f32 * 1000.0 / sample_rate as f32
823 );
824
825 let (aligned_ref, aligned_rec) = if lag >= 0 {
828 let lag_usize = lag as usize;
829 if lag_usize >= recorded.len() {
830 return Err("Lag is larger than recorded signal length".to_string());
831 }
832 (reference, &recorded[lag_usize..])
834 } else {
835 let lag_usize = (-lag) as usize;
837 if lag_usize >= reference.len() {
838 return Err("Negative lag is larger than reference signal length".to_string());
839 }
840 (&reference[lag_usize..], &recorded[..])
842 };
843
844 log::info!(
845 "[FFT Analysis] Aligned lengths: ref={}, rec={} (tail included)",
846 aligned_ref.len(),
847 aligned_rec.len()
848 );
849
850 let fft_size = next_power_of_two(aligned_ref.len().max(aligned_rec.len()));
852
853 let ref_spectrum = compute_fft(aligned_ref, fft_size, WindowType::Tukey(0.1))?;
854 let rec_spectrum = compute_fft(aligned_rec, fft_size, WindowType::Tukey(0.1))?;
855
856 let num_output_points = 2000;
858 let log_start = 20.0_f32.ln();
859 let log_end = 20000.0_f32.ln();
860
861 let mut frequencies = Vec::with_capacity(num_output_points);
862 let mut spl_db = Vec::with_capacity(num_output_points);
863 let mut phase_deg = Vec::with_capacity(num_output_points);
864
865 let freq_resolution = sample_rate as f32 / fft_size as f32;
866 let num_bins = fft_size / 2; let mut skipped_count = 0;
870 for i in 0..num_output_points {
871 let target_freq =
873 (log_start + (log_end - log_start) * i as f32 / (num_output_points - 1) as f32).exp();
874
875 let octave_fraction = 1.0 / 48.0;
878 let freq_lower = target_freq * 2.0_f32.powf(-octave_fraction);
879 let freq_upper = target_freq * 2.0_f32.powf(octave_fraction);
880
881 let bin_lower = ((freq_lower / freq_resolution).floor() as usize).max(1);
883 let bin_upper = ((freq_upper / freq_resolution).ceil() as usize).min(num_bins);
884
885 if bin_lower > bin_upper || bin_upper >= ref_spectrum.len() {
886 if skipped_count < 5 {
887 log::info!(
888 "[FFT Analysis] Skipping freq {:.1} Hz: bin_lower={}, bin_upper={}, ref_spectrum.len()={}",
889 target_freq,
890 bin_lower,
891 bin_upper,
892 ref_spectrum.len()
893 );
894 }
895 skipped_count += 1;
896 continue; }
898
899 let mut sum_magnitude = 0.0;
901 let mut sum_sin = 0.0; let mut sum_cos = 0.0;
903 let mut bin_count = 0;
904
905 for k in bin_lower..=bin_upper {
906 if k >= ref_spectrum.len() {
907 break;
908 }
909
910 let ref_mag_sq = ref_spectrum[k].norm_sqr();
913 let transfer_function = if ref_mag_sq > 1e-20 {
914 rec_spectrum[k] / ref_spectrum[k]
915 } else {
916 Complex::new(0.0, 0.0)
917 };
918 let magnitude = transfer_function.norm();
919
920 let cross_spectrum = ref_spectrum[k].conj() * rec_spectrum[k];
922 let mut phase_rad = cross_spectrum.arg();
923
924 phase_rad = phase_rad.sin().atan2(phase_rad.cos());
926
927 sum_magnitude += magnitude;
929 sum_sin += phase_rad.sin();
930 sum_cos += phase_rad.cos();
931 bin_count += 1;
932 }
933
934 if bin_count == 0 {
935 continue; }
937
938 let avg_magnitude = sum_magnitude / bin_count as f32;
940
941 let db = 20.0 * avg_magnitude.max(1e-10).log10();
943
944 if frequencies.len() < 5 {
946 log::info!(
947 "[FFT Analysis] freq={:.1} Hz: avg_magnitude={:.6}, dB={:.2}",
948 target_freq,
949 avg_magnitude,
950 db
951 );
952 }
953
954 let avg_phase_rad = sum_sin.atan2(sum_cos);
956 let phase = avg_phase_rad * 180.0 / PI;
957
958 frequencies.push(target_freq);
959 spl_db.push(db);
960 phase_deg.push(phase);
961 }
962
963 log::info!(
964 "[FFT Analysis] Generated {} frequency points for CSV output",
965 frequencies.len()
966 );
967 log::info!(
968 "[FFT Analysis] Skipped {} frequency points (out of {})",
969 skipped_count,
970 num_output_points
971 );
972
973 if !spl_db.is_empty() {
974 let min_spl = spl_db.iter().fold(f32::INFINITY, |a, &b| a.min(b));
975 let max_spl = spl_db.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
976 log::info!(
977 "[FFT Analysis] SPL range: {:.2} dB to {:.2} dB",
978 min_spl,
979 max_spl
980 );
981 }
982
983 let mut transfer_function = vec![Complex::new(0.0, 0.0); fft_size];
986 for k in 0..fft_size {
987 let ref_mag_sq = ref_spectrum[k].norm_sqr();
990 if ref_mag_sq > 1e-20 {
991 transfer_function[k] = rec_spectrum[k] / ref_spectrum[k];
992 }
993 }
994
995 let mut planner = FftPlanner::new();
997 let ifft = planner.plan_fft_inverse(fft_size);
998 ifft.process(&mut transfer_function);
999
1000 let norm = 1.0 / fft_size as f32;
1005 let mut impulse_response: Vec<f32> = transfer_function.iter().map(|c| c.re * norm).collect();
1006
1007 let peak_idx = impulse_response
1011 .iter()
1012 .enumerate()
1013 .max_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap())
1014 .map(|(i, _)| i)
1015 .unwrap_or(0);
1016
1017 let pre_ring_samples = (0.005 * sample_rate as f32) as usize; let shift_amount = if peak_idx > pre_ring_samples {
1020 peak_idx - pre_ring_samples
1021 } else {
1022 0
1023 };
1024
1025 if shift_amount > 0 {
1026 impulse_response.rotate_left(shift_amount);
1027 log::info!(
1028 "[FFT Analysis] IR peak was at index {}, shifted by {} samples to put peak near beginning",
1029 peak_idx,
1030 shift_amount
1031 );
1032 }
1033
1034 let _ir_duration_sec = fft_size as f32 / sample_rate as f32;
1036 let impulse_time_ms: Vec<f32> = (0..fft_size)
1037 .map(|i| i as f32 / sample_rate as f32 * 1000.0)
1038 .collect();
1039
1040 let (thd_percent, harmonic_distortion_db) = if let Some((start, end)) = sweep_range {
1042 let duration = reference_signal.len() as f32 / sample_rate as f32;
1045 compute_thd_from_ir(
1046 &impulse_response,
1047 sample_rate as f32,
1048 &frequencies,
1049 &spl_db,
1050 start,
1051 end,
1052 duration,
1053 )
1054 } else {
1055 (vec![0.0; frequencies.len()], Vec::new())
1056 };
1057
1058 let excess_group_delay_ms = vec![0.0; frequencies.len()];
1061
1062 let ir_max = impulse_response
1065 .iter()
1066 .fold(0.0f32, |a, &b| a.max(b.abs()));
1067 let ir_len = impulse_response.len();
1068 log::info!(
1069 "[Analysis] Impulse response: len={}, max_abs={:.6}, sample_rate={}",
1070 ir_len,
1071 ir_max,
1072 sample_rate
1073 );
1074
1075 let rt60_ms = compute_rt60_spectrum(&impulse_response, sample_rate as f32, &frequencies);
1076 let (clarity_c50_db, clarity_c80_db) =
1077 compute_clarity_spectrum(&impulse_response, sample_rate as f32, &frequencies);
1078
1079 if !rt60_ms.is_empty() {
1081 let rt60_min = rt60_ms.iter().fold(f32::INFINITY, |a, &b| a.min(b));
1082 let rt60_max = rt60_ms.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
1083 log::info!(
1084 "[Analysis] RT60 range: {:.1} - {:.1} ms",
1085 rt60_min,
1086 rt60_max
1087 );
1088 }
1089 if !clarity_c50_db.is_empty() {
1090 let c50_min = clarity_c50_db.iter().fold(f32::INFINITY, |a, &b| a.min(b));
1091 let c50_max = clarity_c50_db
1092 .iter()
1093 .fold(f32::NEG_INFINITY, |a, &b| a.max(b));
1094 log::info!(
1095 "[Analysis] Clarity C50 range: {:.1} - {:.1} dB",
1096 c50_min,
1097 c50_max
1098 );
1099 }
1100
1101 let (spectrogram_db, _, _) =
1103 compute_spectrogram(&impulse_response, sample_rate as f32, 512, 128);
1104
1105 Ok(AnalysisResult {
1106 frequencies,
1107 spl_db,
1108 phase_deg,
1109 estimated_lag_samples: lag,
1110 impulse_response,
1111 impulse_time_ms,
1112 excess_group_delay_ms,
1113 thd_percent,
1114 harmonic_distortion_db,
1115 rt60_ms,
1116 clarity_c50_db,
1117 clarity_c80_db,
1118 spectrogram_db,
1119 })
1120}
1121
1122fn compute_thd_from_ir(
1126 impulse: &[f32],
1127 sample_rate: f32,
1128 frequencies: &[f32],
1129 fundamental_db: &[f32],
1130 start_freq: f32,
1131 end_freq: f32,
1132 duration: f32,
1133) -> (Vec<f32>, Vec<Vec<f32>>) {
1134 if frequencies.is_empty() {
1135 return (Vec::new(), Vec::new());
1136 }
1137
1138 let n = impulse.len();
1139 if n == 0 {
1140 return (vec![0.0; frequencies.len()], Vec::new());
1141 }
1142
1143 let num_harmonics = 4; let mut harmonics_db = vec![vec![-120.0; frequencies.len()]; num_harmonics];
1146
1147 let peak_idx = impulse
1149 .iter()
1150 .enumerate()
1151 .max_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap())
1152 .map(|(i, _)| i)
1153 .unwrap_or(0);
1154
1155 let sweep_ratio = end_freq / start_freq;
1156 log::debug!(
1157 "[THD] Impulse len={}, peak_idx={}, duration={:.3}s, sweep {:.0}-{:.0} Hz (ratio {:.1})",
1158 n,
1159 peak_idx,
1160 duration,
1161 start_freq,
1162 end_freq,
1163 sweep_ratio
1164 );
1165
1166 for k_idx in 0..num_harmonics {
1168 let harmonic_order = k_idx + 2; let dt = duration * (harmonic_order as f32).ln() / sweep_ratio.ln();
1173 let dn = (dt * sample_rate).round() as isize;
1174
1175 let center = peak_idx as isize - dn;
1177 let center_wrapped = center.rem_euclid(n as isize) as usize;
1178
1179 let dt_next_rel = duration
1181 * ((harmonic_order as f32 + 1.0).ln() - (harmonic_order as f32).ln())
1182 / sweep_ratio.ln();
1183 let win_len = ((dt_next_rel * sample_rate * 0.8).max(256.0) as usize).min(n / 2);
1184
1185 let mut harmonic_ir = vec![0.0f32; win_len];
1187 let mut max_harmonic_sample = 0.0f32;
1188 for i in 0..win_len {
1189 let src_idx =
1190 (center - (win_len as isize / 2) + i as isize).rem_euclid(n as isize) as usize;
1191 let w = 0.5 * (1.0 - (2.0 * PI * i as f32 / (win_len as f32 - 1.0)).cos());
1193 harmonic_ir[i] = impulse[src_idx] * w;
1194 max_harmonic_sample = max_harmonic_sample.max(harmonic_ir[i].abs());
1195 }
1196
1197 if k_idx == 0 {
1198 log::debug!(
1199 "[THD] H{}: dt={:.3}s, dn={}, center_wrapped={}, win_len={}, max_sample={:.2e}",
1200 harmonic_order,
1201 dt,
1202 dn,
1203 center_wrapped,
1204 win_len,
1205 max_harmonic_sample
1206 );
1207 }
1208
1209 let fft_size = next_power_of_two(win_len);
1211 let nyquist_bin = fft_size / 2; if let Ok(spectrum) = compute_fft_padded(&harmonic_ir, fft_size) {
1213 let freq_resolution = sample_rate / fft_size as f32;
1214
1215 for (i, &f) in frequencies.iter().enumerate() {
1216 let bin = (f / freq_resolution).round() as usize;
1217 if bin < nyquist_bin && bin < spectrum.len() {
1219 let mag = spectrum[bin].norm() * fft_size as f32;
1221 if mag > 1e-6 {
1223 harmonics_db[k_idx][i] = 20.0 * mag.log10();
1224 }
1225 }
1226 }
1227 }
1228 }
1229
1230 if !frequencies.is_empty() {
1232 let mid_idx = frequencies.len() / 2;
1233 log::debug!(
1234 "[THD] Harmonic levels at {:.0} Hz: H2={:.1}dB, H3={:.1}dB, H4={:.1}dB, H5={:.1}dB, fundamental={:.1}dB",
1235 frequencies[mid_idx],
1236 harmonics_db[0][mid_idx],
1237 harmonics_db[1][mid_idx],
1238 harmonics_db[2][mid_idx],
1239 harmonics_db[3][mid_idx],
1240 fundamental_db[mid_idx]
1241 );
1242 }
1243
1244 let mut thd_percent = Vec::with_capacity(frequencies.len());
1246 for i in 0..frequencies.len() {
1247 let fundamental = 10.0f32.powf(fundamental_db[i] / 20.0);
1248 let mut harmonic_sum_sq = 0.0;
1249
1250 for k in 0..num_harmonics {
1251 let h_mag = 10.0f32.powf(harmonics_db[k][i] / 20.0);
1252 harmonic_sum_sq += h_mag * h_mag;
1253 }
1254
1255 let thd = if fundamental > 1e-9 {
1257 (harmonic_sum_sq.sqrt() / fundamental) * 100.0
1258 } else {
1259 0.0
1260 };
1261 thd_percent.push(thd);
1262 }
1263
1264 if !thd_percent.is_empty() {
1266 let max_thd = thd_percent.iter().fold(0.0f32, |a, &b| a.max(b));
1267 let min_thd = thd_percent.iter().fold(f32::INFINITY, |a, &b| a.min(b));
1268 log::debug!("[THD] THD range: {:.4}% to {:.4}%", min_thd, max_thd);
1269 }
1270
1271 (thd_percent, harmonics_db)
1272}
1273
1274pub fn write_analysis_csv(
1287 result: &AnalysisResult,
1288 output_path: &Path,
1289 compensation: Option<&MicrophoneCompensation>,
1290) -> Result<(), String> {
1291 use std::fs::File;
1292 use std::io::Write;
1293
1294 log::info!(
1295 "[write_analysis_csv] Writing {} frequency points to {:?}",
1296 result.frequencies.len(),
1297 output_path
1298 );
1299
1300 if let Some(comp) = compensation {
1301 log::info!(
1302 "[write_analysis_csv] Applying inverse microphone compensation ({} calibration points)",
1303 comp.frequencies.len()
1304 );
1305 }
1306
1307 if result.frequencies.is_empty() {
1308 return Err("Cannot write CSV: Analysis result has no frequency points!".to_string());
1309 }
1310
1311 let mut file =
1312 File::create(output_path).map_err(|e| format!("Failed to create CSV file: {}", e))?;
1313
1314 writeln!(
1316 file,
1317 "frequency_hz,spl_db,phase_deg,thd_percent,rt60_ms,c50_db,c80_db,group_delay_ms"
1318 )
1319 .map_err(|e| format!("Failed to write header: {}", e))?;
1320
1321 for i in 0..result.frequencies.len() {
1323 let freq = result.frequencies[i];
1324 let mut spl = result.spl_db[i];
1325
1326 if let Some(comp) = compensation {
1329 let mic_deviation = comp.interpolate_at(freq);
1330 spl -= mic_deviation;
1331 }
1332
1333 let phase = result.phase_deg[i];
1334 let thd = result.thd_percent.get(i).copied().unwrap_or(0.0);
1335 let rt60 = result.rt60_ms.get(i).copied().unwrap_or(0.0);
1336 let c50 = result.clarity_c50_db.get(i).copied().unwrap_or(0.0);
1337 let c80 = result.clarity_c80_db.get(i).copied().unwrap_or(0.0);
1338 let gd = result.excess_group_delay_ms.get(i).copied().unwrap_or(0.0);
1339
1340 writeln!(
1341 file,
1342 "{:.6},{:.3},{:.6},{:.6},{:.3},{:.3},{:.3},{:.6}",
1343 freq, spl, phase, thd, rt60, c50, c80, gd
1344 )
1345 .map_err(|e| format!("Failed to write data: {}", e))?;
1346 }
1347
1348 log::info!(
1349 "[write_analysis_csv] Successfully wrote {} data rows to CSV",
1350 result.frequencies.len()
1351 );
1352
1353 Ok(())
1354}
1355
1356pub fn read_analysis_csv(csv_path: &Path) -> Result<AnalysisResult, String> {
1361 use std::fs::File;
1362 use std::io::{BufRead, BufReader};
1363
1364 let file = File::open(csv_path).map_err(|e| format!("Failed to open CSV: {}", e))?;
1365 let reader = BufReader::new(file);
1366 let mut lines = reader.lines();
1367
1368 let header = lines
1370 .next()
1371 .ok_or("Empty CSV file")?
1372 .map_err(|e| format!("Failed to read header: {}", e))?;
1373
1374 let columns: Vec<&str> = header.split(',').map(|s| s.trim()).collect();
1375 let has_extended_format = columns.len() >= 8;
1376
1377 let mut frequencies = Vec::new();
1378 let mut spl_db = Vec::new();
1379 let mut phase_deg = Vec::new();
1380 let mut thd_percent = Vec::new();
1381 let mut rt60_ms = Vec::new();
1382 let mut clarity_c50_db = Vec::new();
1383 let mut clarity_c80_db = Vec::new();
1384 let mut excess_group_delay_ms = Vec::new();
1385
1386 for line in lines {
1387 let line = line.map_err(|e| format!("Failed to read line: {}", e))?;
1388 let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
1389
1390 if parts.len() < 3 {
1391 continue;
1392 }
1393
1394 let freq: f32 = parts[0].parse().unwrap_or(0.0);
1395 let spl: f32 = parts[1].parse().unwrap_or(0.0);
1396 let phase: f32 = parts[2].parse().unwrap_or(0.0);
1397
1398 frequencies.push(freq);
1399 spl_db.push(spl);
1400 phase_deg.push(phase);
1401
1402 if has_extended_format && parts.len() >= 8 {
1403 thd_percent.push(parts[3].parse().unwrap_or(0.0));
1404 rt60_ms.push(parts[4].parse().unwrap_or(0.0));
1405 clarity_c50_db.push(parts[5].parse().unwrap_or(0.0));
1406 clarity_c80_db.push(parts[6].parse().unwrap_or(0.0));
1407 excess_group_delay_ms.push(parts[7].parse().unwrap_or(0.0));
1408 }
1409 }
1410
1411 let n = frequencies.len();
1413 if thd_percent.is_empty() {
1414 thd_percent = vec![0.0; n];
1415 rt60_ms = vec![0.0; n];
1416 clarity_c50_db = vec![0.0; n];
1417 clarity_c80_db = vec![0.0; n];
1418 excess_group_delay_ms = vec![0.0; n];
1419 }
1420
1421 Ok(AnalysisResult {
1422 frequencies,
1423 spl_db,
1424 phase_deg,
1425 estimated_lag_samples: 0,
1426 impulse_response: Vec::new(),
1427 impulse_time_ms: Vec::new(),
1428 thd_percent,
1429 harmonic_distortion_db: Vec::new(),
1430 rt60_ms,
1431 clarity_c50_db,
1432 clarity_c80_db,
1433 excess_group_delay_ms,
1434 spectrogram_db: Vec::new(),
1435 })
1436}
1437
1438#[derive(Debug, Clone, Copy)]
1440enum WindowType {
1441 Hann,
1442 Tukey(f32), }
1444
1445fn estimate_lag(reference: &[f32], recorded: &[f32]) -> Result<isize, String> {
1456 let len = reference.len().min(recorded.len());
1457
1458 let fft_size = next_power_of_two(len * 2);
1460
1461 let ref_fft = compute_fft(reference, fft_size, WindowType::Hann)?;
1463 let rec_fft = compute_fft(recorded, fft_size, WindowType::Hann)?;
1464
1465 let mut cross_corr_fft: Vec<Complex<f32>> = ref_fft
1467 .iter()
1468 .zip(rec_fft.iter())
1469 .map(|(x, y)| x.conj() * y)
1470 .collect();
1471
1472 let mut planner = FftPlanner::new();
1474 let ifft = planner.plan_fft_inverse(fft_size);
1475 ifft.process(&mut cross_corr_fft);
1476
1477 let mut max_val = 0.0;
1479 let mut max_idx = 0;
1480
1481 for (i, &val) in cross_corr_fft.iter().enumerate() {
1482 let magnitude = val.norm();
1483 if magnitude > max_val {
1484 max_val = magnitude;
1485 max_idx = i;
1486 }
1487 }
1488
1489 Ok(if max_idx <= fft_size / 2 {
1491 max_idx as isize
1492 } else {
1493 max_idx as isize - fft_size as isize
1494 })
1495}
1496
1497fn compute_fft(
1507 signal: &[f32],
1508 fft_size: usize,
1509 window_type: WindowType,
1510) -> Result<Vec<Complex<f32>>, String> {
1511 let windowed = match window_type {
1513 WindowType::Hann => apply_hann_window(signal),
1514 WindowType::Tukey(alpha) => apply_tukey_window(signal, alpha),
1515 };
1516
1517 compute_fft_padded(&windowed, fft_size)
1518}
1519
1520fn compute_fft_padded(signal: &[f32], fft_size: usize) -> Result<Vec<Complex<f32>>, String> {
1522 let mut buffer: Vec<Complex<f32>> = signal.iter().map(|&x| Complex::new(x, 0.0)).collect();
1524 buffer.resize(fft_size, Complex::new(0.0, 0.0));
1525
1526 let mut planner = FftPlanner::new();
1528 let fft = planner.plan_fft_forward(fft_size);
1529 fft.process(&mut buffer);
1530
1531 let norm_factor = 1.0 / fft_size as f32;
1533 for val in buffer.iter_mut() {
1534 *val *= norm_factor;
1535 }
1536
1537 Ok(buffer)
1538}
1539
1540fn apply_hann_window(signal: &[f32]) -> Vec<f32> {
1542 let len = signal.len();
1543 if len < 2 {
1544 return signal.to_vec();
1545 }
1546 signal
1547 .iter()
1548 .enumerate()
1549 .map(|(i, &x)| {
1550 let window = 0.5 * (1.0 - (2.0 * PI * i as f32 / (len - 1) as f32).cos());
1551 x * window
1552 })
1553 .collect()
1554}
1555
1556fn apply_tukey_window(signal: &[f32], alpha: f32) -> Vec<f32> {
1561 let len = signal.len();
1562 if len < 2 {
1563 return signal.to_vec();
1564 }
1565
1566 let alpha = alpha.clamp(0.0, 1.0);
1567 let limit = (alpha * (len as f32 - 1.0) / 2.0).round() as usize;
1568
1569 if limit == 0 {
1570 return signal.to_vec();
1571 }
1572
1573 signal
1574 .iter()
1575 .enumerate()
1576 .map(|(i, &x)| {
1577 let w = if i < limit {
1578 0.5 * (1.0 - (PI * i as f32 / limit as f32).cos())
1580 } else if i >= len - limit {
1581 let n = len - 1 - i;
1583 0.5 * (1.0 - (PI * n as f32 / limit as f32).cos())
1584 } else {
1585 1.0
1587 };
1588 x * w
1589 })
1590 .collect()
1591}
1592
1593fn next_power_of_two(n: usize) -> usize {
1595 if n == 0 {
1596 return 1;
1597 }
1598 n.next_power_of_two()
1599}
1600
1601fn load_wav_mono_channel(path: &Path, channel_index: Option<usize>) -> Result<Vec<f32>, String> {
1608 let mut reader =
1609 WavReader::open(path).map_err(|e| format!("Failed to open WAV file: {}", e))?;
1610
1611 let spec = reader.spec();
1612 let channels = spec.channels as usize;
1613
1614 log::info!(
1615 "[load_wav_mono_channel] WAV file: {} channels, {} Hz, {:?} format",
1616 channels,
1617 spec.sample_rate,
1618 spec.sample_format
1619 );
1620
1621 let samples: Result<Vec<f32>, _> = match spec.sample_format {
1623 hound::SampleFormat::Float => reader.samples::<f32>().collect(),
1624 hound::SampleFormat::Int => reader
1625 .samples::<i32>()
1626 .map(|s| s.map(|v| v as f32 / i32::MAX as f32))
1627 .collect(),
1628 };
1629
1630 let samples = samples.map_err(|e| format!("Failed to read samples: {}", e))?;
1631 log::info!(
1632 "[load_wav_mono_channel] Read {} total samples",
1633 samples.len()
1634 );
1635
1636 if channels == 1 {
1638 log::info!(
1639 "[load_wav_mono_channel] File is already mono, returning {} samples",
1640 samples.len()
1641 );
1642 return Ok(samples);
1643 }
1644
1645 if let Some(ch_idx) = channel_index {
1647 if ch_idx >= channels {
1649 return Err(format!(
1650 "Channel index {} out of range (file has {} channels)",
1651 ch_idx, channels
1652 ));
1653 }
1654 log::info!(
1655 "[load_wav_mono_channel] Extracting channel {} from {} channels",
1656 ch_idx,
1657 channels
1658 );
1659 Ok(samples
1660 .chunks(channels)
1661 .map(|chunk| chunk[ch_idx])
1662 .collect())
1663 } else {
1664 log::info!(
1666 "[load_wav_mono_channel] Averaging {} channels to mono",
1667 channels
1668 );
1669 Ok(samples
1670 .chunks(channels)
1671 .map(|chunk| chunk.iter().sum::<f32>() / channels as f32)
1672 .collect())
1673 }
1674}
1675
1676fn load_wav_mono(path: &Path) -> Result<Vec<f32>, String> {
1678 load_wav_mono_channel(path, None)
1679}
1680
1681pub fn smooth_response_f64(frequencies: &[f64], values: &[f64], octaves: f64) -> Vec<f64> {
1687 if octaves <= 0.0 || frequencies.is_empty() || values.is_empty() {
1688 return values.to_vec();
1689 }
1690
1691 let mut smoothed = Vec::with_capacity(values.len());
1692
1693 for (i, ¢er_freq) in frequencies.iter().enumerate() {
1694 if center_freq <= 0.0 {
1695 smoothed.push(values[i]);
1696 continue;
1697 }
1698
1699 let ratio = 2.0_f64.powf(octaves / 2.0);
1701 let low_freq = center_freq / ratio;
1702 let high_freq = center_freq * ratio;
1703
1704 let mut sum = 0.0;
1706 let mut count = 0;
1707
1708 for (j, &freq) in frequencies.iter().enumerate() {
1709 if freq >= low_freq && freq <= high_freq {
1710 sum += values[j];
1711 count += 1;
1712 }
1713 }
1714
1715 if count > 0 {
1716 smoothed.push(sum / count as f64);
1717 } else {
1718 smoothed.push(values[i]);
1719 }
1720 }
1721
1722 smoothed
1723}
1724
1725pub fn smooth_response_f32(frequencies: &[f32], values: &[f32], octaves: f32) -> Vec<f32> {
1727 if octaves <= 0.0 || frequencies.is_empty() || values.is_empty() {
1728 return values.to_vec();
1729 }
1730
1731 let mut smoothed = Vec::with_capacity(values.len());
1732
1733 let half_octave_ratio = 2.0_f32.powf(octaves / 2.0);
1735
1736 for (i, ¢er_freq) in frequencies.iter().enumerate() {
1737 if center_freq <= 0.0 {
1738 smoothed.push(values[i]);
1739 continue;
1740 }
1741
1742 let low_freq = center_freq / half_octave_ratio;
1743 let high_freq = center_freq * half_octave_ratio;
1744
1745 let mut sum = 0.0_f32;
1746 let mut count = 0;
1747
1748 for (j, &freq) in frequencies.iter().enumerate() {
1749 if freq >= low_freq && freq <= high_freq {
1750 sum += values[j];
1751 count += 1;
1752 }
1753 }
1754
1755 if count > 0 {
1756 smoothed.push(sum / count as f32);
1757 } else {
1758 smoothed.push(values[i]);
1759 }
1760 }
1761
1762 smoothed
1763}
1764
1765pub fn compute_group_delay(frequencies: &[f32], phase_deg: &[f32]) -> Vec<f32> {
1768 if frequencies.len() < 2 {
1769 return vec![0.0; frequencies.len()];
1770 }
1771
1772 let mut group_delay_ms = Vec::with_capacity(frequencies.len());
1773
1774 for i in 0..frequencies.len() {
1775 let delay = if i == 0 {
1776 let df = frequencies[1] - frequencies[0];
1778 let dp = phase_deg[1] - phase_deg[0];
1779 if df.abs() > 1e-6 {
1780 -dp / df / 360.0 * 1000.0 } else {
1782 0.0
1783 }
1784 } else if i == frequencies.len() - 1 {
1785 let df = frequencies[i] - frequencies[i - 1];
1787 let dp = phase_deg[i] - phase_deg[i - 1];
1788 if df.abs() > 1e-6 {
1789 -dp / df / 360.0 * 1000.0
1790 } else {
1791 0.0
1792 }
1793 } else {
1794 let df = frequencies[i + 1] - frequencies[i - 1];
1796 let dp = phase_deg[i + 1] - phase_deg[i - 1];
1797 if df.abs() > 1e-6 {
1798 -dp / df / 360.0 * 1000.0
1799 } else {
1800 0.0
1801 }
1802 };
1803 group_delay_ms.push(delay);
1804 }
1805
1806 group_delay_ms
1807}
1808
1809pub fn compute_impulse_response_from_fr(
1811 frequencies: &[f32],
1812 magnitude_db: &[f32],
1813 phase_deg: &[f32],
1814 sample_rate: f32,
1815) -> (Vec<f32>, Vec<f32>) {
1816 let num_samples = 512;
1819 let time_step = 1.0 / sample_rate;
1820
1821 let mut impulse = vec![0.0_f32; num_samples];
1822 let times: Vec<f32> = (0..num_samples)
1823 .map(|i| i as f32 * time_step * 1000.0) .collect();
1825
1826 for (i, time) in times.iter().enumerate() {
1829 let t_sec = time / 1000.0;
1830 for (j, (&freq, &mag_db)) in frequencies.iter().zip(magnitude_db.iter()).enumerate() {
1831 if freq > 0.0 && freq < sample_rate / 2.0 {
1832 let mag_linear = 10.0_f32.powf(mag_db / 20.0);
1833 let phase_rad = phase_deg[j] * PI / 180.0;
1834 impulse[i] += mag_linear * (2.0 * PI * freq * t_sec + phase_rad).cos();
1835 }
1836 }
1837 }
1838
1839 let max_val = impulse.iter().map(|v| v.abs()).fold(0.0_f32, f32::max);
1841 if max_val > 0.0 {
1842 for v in &mut impulse {
1843 *v /= max_val;
1844 }
1845 }
1846
1847 (times, impulse)
1848}
1849
1850fn compute_schroeder_decay(impulse: &[f32]) -> Vec<f32> {
1852 let mut energy = 0.0;
1853 let mut decay = vec![0.0; impulse.len()];
1854
1855 for i in (0..impulse.len()).rev() {
1857 energy += impulse[i] * impulse[i];
1858 decay[i] = energy;
1859 }
1860
1861 let max_energy = decay.first().copied().unwrap_or(1.0);
1863 if max_energy > 0.0 {
1864 for v in &mut decay {
1865 *v /= max_energy;
1866 }
1867 }
1868
1869 decay
1870}
1871
1872pub fn compute_rt60_broadband(impulse: &[f32], sample_rate: f32) -> f32 {
1875 let decay = compute_schroeder_decay(impulse);
1876 let decay_db: Vec<f32> = decay
1877 .iter()
1878 .map(|&v| 10.0 * v.max(1e-9).log10())
1879 .collect();
1880
1881 let t_minus_5 = decay_db.iter().position(|&v| v < -5.0);
1883 let t_minus_25 = decay_db.iter().position(|&v| v < -25.0);
1884
1885 match (t_minus_5, t_minus_25) {
1886 (Some(start), Some(end)) => {
1887 if end > start {
1888 let dt = (end - start) as f32 / sample_rate; dt * 3.0 } else {
1891 0.0
1892 }
1893 }
1894 _ => 0.0,
1895 }
1896}
1897
1898pub fn compute_clarity_broadband(impulse: &[f32], sample_rate: f32) -> (f32, f32) {
1901 let mut energy_0_50 = 0.0;
1902 let mut energy_50_inf = 0.0;
1903 let mut energy_0_80 = 0.0;
1904 let mut energy_80_inf = 0.0;
1905
1906 let samp_50ms = (0.050 * sample_rate) as usize;
1907 let samp_80ms = (0.080 * sample_rate) as usize;
1908
1909 for (i, &samp) in impulse.iter().enumerate() {
1910 let sq = samp * samp;
1911
1912 if i < samp_50ms {
1913 energy_0_50 += sq;
1914 } else {
1915 energy_50_inf += sq;
1916 }
1917
1918 if i < samp_80ms {
1919 energy_0_80 += sq;
1920 } else {
1921 energy_80_inf += sq;
1922 }
1923 }
1924
1925 const MAX_CLARITY_DB: f32 = 60.0;
1928
1929 let c50 = if energy_50_inf > 1e-12 && energy_0_50 > 1e-12 {
1930 let ratio = energy_0_50 / energy_50_inf;
1931 (10.0 * ratio.log10()).clamp(-MAX_CLARITY_DB, MAX_CLARITY_DB)
1932 } else if energy_0_50 > energy_50_inf {
1933 MAX_CLARITY_DB } else {
1935 -MAX_CLARITY_DB };
1937
1938 let c80 = if energy_80_inf > 1e-12 && energy_0_80 > 1e-12 {
1939 let ratio = energy_0_80 / energy_80_inf;
1940 (10.0 * ratio.log10()).clamp(-MAX_CLARITY_DB, MAX_CLARITY_DB)
1941 } else if energy_0_80 > energy_80_inf {
1942 MAX_CLARITY_DB } else {
1944 -MAX_CLARITY_DB };
1946
1947 (c50, c80)
1948}
1949
1950pub fn compute_rt60_spectrum(impulse: &[f32], sample_rate: f32, frequencies: &[f32]) -> Vec<f32> {
1952 if impulse.is_empty() {
1953 return vec![0.0; frequencies.len()];
1954 }
1955
1956 let centers = [
1958 63.0f32, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0,
1959 ];
1960 let mut band_rt60s = Vec::with_capacity(centers.len());
1961 let mut valid_centers = Vec::with_capacity(centers.len());
1962
1963 for &freq in ¢ers {
1965 if freq >= sample_rate / 2.0 {
1967 continue;
1968 }
1969
1970 let mut biquad = Biquad::new(
1973 BiquadFilterType::Bandpass,
1974 freq as f64,
1975 sample_rate as f64,
1976 1.414,
1977 0.0,
1978 );
1979
1980 let mut filtered: Vec<f64> = impulse.iter().map(|&x| x as f64).collect();
1982 biquad.process_block(&mut filtered);
1983 let filtered_f32: Vec<f32> = filtered.iter().map(|&x| x as f32).collect();
1984
1985 let rt60 = compute_rt60_broadband(&filtered_f32, sample_rate);
1987
1988 band_rt60s.push(rt60);
1989 valid_centers.push(freq);
1990 }
1991
1992 log::info!(
1994 "[RT60] Per-band values: {:?}",
1995 valid_centers
1996 .iter()
1997 .zip(band_rt60s.iter())
1998 .map(|(f, v)| format!("{:.0}Hz:{:.1}ms", f, v))
1999 .collect::<Vec<_>>()
2000 );
2001
2002 if valid_centers.is_empty() {
2003 return vec![0.0; frequencies.len()];
2004 }
2005
2006 interpolate_log(&valid_centers, &band_rt60s, frequencies)
2008}
2009
2010pub fn compute_clarity_spectrum(
2013 impulse: &[f32],
2014 sample_rate: f32,
2015 frequencies: &[f32],
2016) -> (Vec<f32>, Vec<f32>) {
2017 if impulse.is_empty() || frequencies.is_empty() {
2018 return (vec![0.0; frequencies.len()], vec![0.0; frequencies.len()]);
2019 }
2020
2021 let centers = [
2023 63.0f32, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0,
2024 ];
2025 let mut band_c50s = Vec::with_capacity(centers.len());
2026 let mut band_c80s = Vec::with_capacity(centers.len());
2027 let mut valid_centers = Vec::with_capacity(centers.len());
2028
2029 let samp_50ms = (0.050 * sample_rate) as usize;
2031 let samp_80ms = (0.080 * sample_rate) as usize;
2032
2033 for &freq in ¢ers {
2035 if freq >= sample_rate / 2.0 {
2036 continue;
2037 }
2038
2039 let mut biquad1 = Biquad::new(
2041 BiquadFilterType::Bandpass,
2042 freq as f64,
2043 sample_rate as f64,
2044 0.707, 0.0,
2046 );
2047 let mut biquad2 = Biquad::new(
2048 BiquadFilterType::Bandpass,
2049 freq as f64,
2050 sample_rate as f64,
2051 0.707,
2052 0.0,
2053 );
2054
2055 let mut filtered: Vec<f64> = impulse.iter().map(|&x| x as f64).collect();
2056 biquad1.process_block(&mut filtered);
2057 biquad2.process_block(&mut filtered);
2058
2059 let mut energy_0_50 = 0.0f64;
2061 let mut energy_50_inf = 0.0f64;
2062 let mut energy_0_80 = 0.0f64;
2063 let mut energy_80_inf = 0.0f64;
2064
2065 for (i, &samp) in filtered.iter().enumerate() {
2066 let sq = samp * samp;
2067
2068 if i < samp_50ms {
2069 energy_0_50 += sq;
2070 } else {
2071 energy_50_inf += sq;
2072 }
2073
2074 if i < samp_80ms {
2075 energy_0_80 += sq;
2076 } else {
2077 energy_80_inf += sq;
2078 }
2079 }
2080
2081 const MAX_CLARITY_DB: f32 = 40.0;
2084 const MIN_ENERGY: f64 = 1e-20;
2085
2086 let c50 = if energy_50_inf > MIN_ENERGY && energy_0_50 > MIN_ENERGY {
2087 let ratio = energy_0_50 / energy_50_inf;
2088 (10.0 * ratio.log10() as f32).clamp(-MAX_CLARITY_DB, MAX_CLARITY_DB)
2089 } else if energy_0_50 > energy_50_inf {
2090 MAX_CLARITY_DB
2091 } else {
2092 -MAX_CLARITY_DB
2093 };
2094
2095 let c80 = if energy_80_inf > MIN_ENERGY && energy_0_80 > MIN_ENERGY {
2096 let ratio = energy_0_80 / energy_80_inf;
2097 (10.0 * ratio.log10() as f32).clamp(-MAX_CLARITY_DB, MAX_CLARITY_DB)
2098 } else if energy_0_80 > energy_80_inf {
2099 MAX_CLARITY_DB
2100 } else {
2101 -MAX_CLARITY_DB
2102 };
2103
2104 band_c50s.push(c50);
2105 band_c80s.push(c80);
2106 valid_centers.push(freq);
2107 }
2108
2109 log::info!(
2111 "[Clarity] Per-band C50: {:?}",
2112 valid_centers
2113 .iter()
2114 .zip(band_c50s.iter())
2115 .map(|(f, v)| format!("{:.0}Hz:{:.1}dB", f, v))
2116 .collect::<Vec<_>>()
2117 );
2118
2119 if valid_centers.is_empty() {
2120 return (vec![0.0; frequencies.len()], vec![0.0; frequencies.len()]);
2121 }
2122
2123 let c50_interp = interpolate_log(&valid_centers, &band_c50s, frequencies);
2125 let c80_interp = interpolate_log(&valid_centers, &band_c80s, frequencies);
2126
2127 (c50_interp, c80_interp)
2128}
2129
2130pub fn compute_spectrogram(
2134 impulse: &[f32],
2135 sample_rate: f32,
2136 window_size: usize,
2137 hop_size: usize,
2138) -> (Vec<Vec<f32>>, Vec<f32>, Vec<f32>) {
2139 use rustfft::num_complex::Complex;
2140 use rustfft::FftPlanner;
2141
2142 if impulse.len() < window_size {
2143 return (Vec::new(), Vec::new(), Vec::new());
2144 }
2145
2146 let num_frames = (impulse.len() - window_size) / hop_size;
2147 let mut spectrogram = Vec::with_capacity(num_frames);
2148 let mut times = Vec::with_capacity(num_frames);
2149
2150 let window: Vec<f32> = (0..window_size)
2152 .map(|i| 0.5 * (1.0 - (2.0 * PI * i as f32 / (window_size as f32 - 1.0)).cos()))
2153 .collect();
2154
2155 let mut planner = FftPlanner::new();
2157 let fft = planner.plan_fft_forward(window_size);
2158
2159 for i in 0..num_frames {
2160 let start = i * hop_size;
2161 let time_ms = (start as f32 / sample_rate) * 1000.0;
2162 times.push(time_ms);
2163
2164 let mut buffer: Vec<Complex<f32>> = (0..window_size)
2165 .map(|j| {
2166 let sample = impulse.get(start + j).copied().unwrap_or(0.0);
2167 Complex::new(sample * window[j], 0.0)
2168 })
2169 .collect();
2170
2171 fft.process(&mut buffer);
2172
2173 let magnitude_db: Vec<f32> = buffer[..window_size / 2]
2176 .iter()
2177 .map(|c| {
2178 let mag = c.norm();
2179 if mag > 1e-9 {
2180 20.0 * mag.log10()
2181 } else {
2182 -180.0
2183 }
2184 })
2185 .collect();
2186
2187 spectrogram.push(magnitude_db);
2188 }
2189
2190 let num_bins = window_size / 2;
2192 let freq_step = sample_rate / window_size as f32;
2193 let freqs: Vec<f32> = (0..num_bins).map(|i| i as f32 * freq_step).collect();
2194
2195 (spectrogram, freqs, times)
2196}
2197
2198#[cfg(test)]
2199mod tests {
2200 use super::*;
2201
2202 #[test]
2203 fn test_next_power_of_two() {
2204 assert_eq!(next_power_of_two(1), 1);
2205 assert_eq!(next_power_of_two(2), 2);
2206 assert_eq!(next_power_of_two(3), 4);
2207 assert_eq!(next_power_of_two(1000), 1024);
2208 assert_eq!(next_power_of_two(1024), 1024);
2209 assert_eq!(next_power_of_two(1025), 2048);
2210 }
2211
2212 #[test]
2213 fn test_hann_window() {
2214 let signal = vec![1.0; 100];
2215 let windowed = apply_hann_window(&signal);
2216
2217 assert!(windowed[0].abs() < 0.01);
2219 assert!(windowed[99].abs() < 0.01);
2220
2221 assert!((windowed[50] - 1.0).abs() < 0.01);
2223 }
2224
2225 #[test]
2226 fn test_estimate_lag_zero() {
2227 let signal = vec![1.0, 2.0, 3.0, 4.0, 5.0];
2229 let lag = estimate_lag(&signal, &signal).unwrap();
2230 assert_eq!(lag, 0);
2231 }
2232
2233 #[test]
2234 fn test_estimate_lag_positive() {
2235 let mut reference = vec![0.0; 100];
2238 let mut recorded = vec![0.0; 100];
2239
2240 for i in 10..20 {
2242 reference[i] = (i - 10) as f32 / 10.0;
2243 }
2244 for i in 15..25 {
2246 recorded[i] = (i - 15) as f32 / 10.0;
2247 }
2248
2249 let lag = estimate_lag(&reference, &recorded).unwrap();
2250 assert_eq!(lag, 5, "Recorded signal is delayed by 5 samples");
2251 }
2252
2253 #[test]
2254 fn test_identical_signals_have_zero_lag() {
2255 let signal = vec![1.0, 2.0, 3.0, 4.0, 5.0];
2258 let lag = estimate_lag(&signal, &signal).unwrap();
2259 assert_eq!(lag, 0, "Identical signals should have zero lag");
2260 }
2261}