use super::{ChannelCalibration, CrosstalkCalibration, HitEnvelope, NoiseFloorStats};
pub fn analyze_noise_floor(samples: &[f32], sample_rate: u32) -> NoiseFloorStats {
if samples.is_empty() {
return NoiseFloorStats {
peak: 0.0,
rms: 0.0,
low_freq_energy: 0.0,
};
}
let peak = samples.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let sum_sq: f64 = samples.iter().map(|s| (*s as f64) * (*s as f64)).sum();
let rms = (sum_sq / samples.len() as f64).sqrt() as f32;
let window_size = (sample_rate as usize / 100).max(1);
let low_freq_energy = if samples.len() >= window_size {
let mut running_sum: f64 = samples[..window_size].iter().map(|s| *s as f64).sum();
let mut lf_sum_sq: f64 = 0.0;
let count = samples.len() - window_size + 1;
for i in 0..count {
let avg = (running_sum / window_size as f64) as f32;
lf_sum_sq += (avg as f64) * (avg as f64);
if i + window_size < samples.len() {
running_sum += samples[i + window_size] as f64;
running_sum -= samples[i] as f64;
}
}
(lf_sum_sq / count as f64).sqrt() as f32
} else {
rms
};
NoiseFloorStats {
peak,
rms,
low_freq_energy,
}
}
pub(crate) fn detection_threshold(noise_floor: &NoiseFloorStats) -> f32 {
(noise_floor.peak * 5.0).max(0.005)
}
pub fn detect_hits(
samples: &[f32],
noise_floor: &NoiseFloorStats,
sample_rate: u32,
) -> Vec<HitEnvelope> {
let threshold = detection_threshold(noise_floor);
let holdoff_samples = (sample_rate as f64 * 0.050) as usize;
let mut hits = Vec::new();
let mut i = 0;
while i < samples.len() {
let abs_val = samples[i].abs();
if abs_val >= threshold {
let onset_sample = i;
let mut peak_amplitude: f32 = abs_val;
let mut peak_sample = i;
let mut consecutive_below: usize = 0;
let mut decay_sample = i;
i += 1;
while i < samples.len() {
let v = samples[i].abs();
if v > peak_amplitude {
peak_amplitude = v;
peak_sample = i;
}
if v < threshold {
if consecutive_below == 0 {
decay_sample = i; }
consecutive_below += 1;
if consecutive_below >= holdoff_samples {
break;
}
} else {
consecutive_below = 0;
}
i += 1;
}
if consecutive_below < holdoff_samples {
decay_sample = if i >= samples.len() {
samples.len() - 1
} else {
i
};
}
let ring_threshold = noise_floor.peak.max(0.001);
let mut ring_end_sample = None;
let mut j = decay_sample;
let mut ring_consecutive: usize = 0;
while j < samples.len() {
if samples[j].abs() < ring_threshold {
ring_consecutive += 1;
if ring_consecutive >= holdoff_samples {
ring_end_sample = Some(j - holdoff_samples + 1);
break;
}
} else {
ring_consecutive = 0;
}
j += 1;
}
hits.push(HitEnvelope {
peak_amplitude,
onset_sample,
peak_sample,
decay_sample,
ring_end_sample,
});
let resume_from = ring_end_sample
.map(|re| re + holdoff_samples)
.unwrap_or(decay_sample + holdoff_samples);
i = resume_from.max(i);
} else {
i += 1;
}
}
hits
}
pub fn derive_channel_params(
channel: u16,
noise_floor: &NoiseFloorStats,
hits: &[HitEnvelope],
sample_rate: u32,
) -> ChannelCalibration {
let threshold = detection_threshold(noise_floor);
let max_hit_amplitude = hits.iter().map(|h| h.peak_amplitude).fold(0.0f32, f32::max);
let gain = if max_hit_amplitude > 0.0 {
(0.95 / max_hit_amplitude).clamp(0.1, 50.0)
} else {
1.0
};
let scan_time_ms = if !hits.is_empty() {
let mut attack_times: Vec<f32> = hits
.iter()
.map(|h| {
let samples = (h.peak_sample - h.onset_sample) as f32;
samples / sample_rate as f32 * 1000.0
})
.collect();
attack_times.sort_unstable_by(f32::total_cmp);
let median = attack_times[attack_times.len() / 2];
(median.ceil() as u32).max(1)
} else {
5
};
let retrigger_time_ms = if !hits.is_empty() {
let mut decay_times: Vec<f32> = hits
.iter()
.map(|h| {
let samples = h.decay_sample.saturating_sub(h.peak_sample) as f32;
samples / sample_rate as f32 * 1000.0
})
.collect();
decay_times.sort_unstable_by(f32::total_cmp);
let median = decay_times[decay_times.len() / 2];
((median * 1.2).ceil() as u32).max(5)
} else {
30
};
let highpass_freq =
if noise_floor.rms > 0.0 && noise_floor.low_freq_energy / noise_floor.rms > 0.5 {
Some(80.0)
} else {
None
};
let dynamic_threshold_decay_ms = if !hits.is_empty() {
let ring_durations: Vec<f32> = hits
.iter()
.filter_map(|h| {
h.ring_end_sample.map(|re| {
let samples = (re - h.decay_sample) as f32;
samples / sample_rate as f32 * 1000.0
})
})
.collect();
if !ring_durations.is_empty() {
let mut sorted = ring_durations;
sorted.sort_unstable_by(f32::total_cmp);
let median = sorted[sorted.len() / 2];
if median > 5.0 {
Some(median.ceil() as u32)
} else {
None
}
} else {
None
}
} else {
None
};
ChannelCalibration {
channel,
threshold,
gain,
scan_time_ms,
retrigger_time_ms,
highpass_freq,
dynamic_threshold_decay_ms,
num_hits_detected: hits.len(),
noise_floor_peak: noise_floor.peak,
max_hit_amplitude,
}
}
pub(crate) fn analyze_crosstalk(
all_samples: &[Vec<f32>],
all_hits: &[Vec<HitEnvelope>],
noise_floors: &[NoiseFloorStats],
sample_rate: u32,
) -> CrosstalkCalibration {
let window_samples = ((5.0 / 1000.0) * sample_rate as f64).ceil() as usize;
let mut max_offset: usize = 0;
let mut max_ratio: f32 = 0.0;
let mut found_crosstalk = false;
for (ch, hits) in all_hits.iter().enumerate() {
for hit in hits {
let center = hit.peak_sample;
for (other_ch, other_samples) in all_samples.iter().enumerate() {
if other_ch == ch {
continue;
}
let other_noise = noise_floors[other_ch].peak.max(0.001);
let crosstalk_detect_threshold = other_noise * 3.0;
let start = center.saturating_sub(window_samples);
let end = (center + window_samples).min(other_samples.len());
for (idx, sample) in other_samples.iter().enumerate().take(end).skip(start) {
let v = sample.abs();
if v > crosstalk_detect_threshold {
found_crosstalk = true;
max_offset = max_offset.max(idx.abs_diff(center));
let ratio = v / other_noise;
max_ratio = max_ratio.max(ratio);
}
}
}
}
}
if found_crosstalk {
let window_ms =
((max_offset as f64 / sample_rate as f64 * 1000.0).ceil() as u32 + 1).max(2);
let threshold = (max_ratio * 1.5).max(2.0);
CrosstalkCalibration {
crosstalk_window_ms: Some(window_ms),
crosstalk_threshold: Some(threshold),
}
} else {
CrosstalkCalibration {
crosstalk_window_ms: None,
crosstalk_threshold: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_transient(
noise_level: f32,
peak: f32,
attack_samples: usize,
decay_samples: usize,
total_samples: usize,
onset: usize,
) -> Vec<f32> {
let mut samples = vec![0.0; total_samples];
for (i, s) in samples.iter_mut().enumerate() {
*s = noise_level * ((i as f32 * 0.7).sin() * 0.5 + (i as f32 * 1.3).cos() * 0.5);
}
if onset < total_samples {
for j in 0..attack_samples.min(total_samples - onset) {
let t = j as f32 / attack_samples as f32;
let idx = onset + j;
if idx < total_samples {
samples[idx] = peak * t;
}
}
let peak_pos = onset + attack_samples;
for j in 0..decay_samples {
let t = 1.0 - (j as f32 / decay_samples as f32);
let idx = peak_pos + j;
if idx < total_samples {
samples[idx] = peak * t;
}
}
}
samples
}
#[test]
fn test_analyze_noise_floor_silence() {
let samples = vec![0.0; 44100];
let stats = analyze_noise_floor(&samples, 44100);
assert_eq!(stats.peak, 0.0);
assert_eq!(stats.rms, 0.0);
assert_eq!(stats.low_freq_energy, 0.0);
}
#[test]
fn test_analyze_noise_floor_with_noise() {
let samples: Vec<f32> = (0..44100).map(|i| 0.002 * (i as f32 * 0.1).sin()).collect();
let stats = analyze_noise_floor(&samples, 44100);
assert!(stats.peak > 0.0 && stats.peak <= 0.002);
assert!(stats.rms > 0.0 && stats.rms < stats.peak);
}
#[test]
fn test_detect_hits_single_hit() {
let samples = make_transient(0.001, 0.8, 50, 500, 44100, 5000);
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
};
let hits = detect_hits(&samples, &nf, 44100);
assert_eq!(hits.len(), 1);
assert!(hits[0].peak_amplitude > 0.7);
assert!(hits[0].onset_sample >= 5000);
assert!(hits[0].peak_sample > hits[0].onset_sample);
}
#[test]
fn test_detect_hits_multiple_hits() {
let sample_rate = 44100;
let total = sample_rate * 3; let mut samples = vec![0.0f32; total];
for (i, s) in samples.iter_mut().enumerate() {
*s = 0.001 * (i as f32 * 0.7).sin();
}
for &onset in &[10000, 50000, 90000] {
for j in 0..50 {
let idx = onset + j;
if idx < total {
samples[idx] = 0.7 * (j as f32 / 50.0);
}
}
for j in 0..2000 {
let idx = onset + 50 + j;
if idx < total {
samples[idx] = 0.7 * (1.0 - j as f32 / 2000.0);
}
}
}
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
};
let hits = detect_hits(&samples, &nf, sample_rate as u32);
assert_eq!(hits.len(), 3);
}
#[test]
fn test_detect_hits_no_hits() {
let samples: Vec<f32> = (0..44100).map(|i| 0.001 * (i as f32 * 0.3).sin()).collect();
let nf = NoiseFloorStats {
peak: 0.003,
rms: 0.002,
low_freq_energy: 0.001,
};
let hits = detect_hits(&samples, &nf, 44100);
assert!(hits.is_empty());
}
#[test]
fn test_detect_hits_with_ringing() {
let sample_rate: u32 = 44100;
let total = sample_rate as usize * 2;
let noise = 0.001;
let mut samples = vec![0.0f32; total];
for (i, s) in samples.iter_mut().enumerate() {
*s = noise * (i as f32 * 0.7).sin();
}
let onset = 5000;
let peak = 0.8;
let attack = 30;
let decay = 200;
let ring_duration = 4000;
for j in 0..attack {
let idx = onset + j;
samples[idx] = peak * (j as f32 / attack as f32);
}
let peak_pos = onset + attack;
for j in 0..decay {
let idx = peak_pos + j;
if idx < total {
samples[idx] = peak * (1.0 - j as f32 / decay as f32);
}
}
let ring_start = peak_pos + decay;
for j in 0..ring_duration {
let idx = ring_start + j;
if idx < total {
let ring_level =
noise * 5.0 * (1.0 - j as f32 / ring_duration as f32) + noise * 0.5;
samples[idx] = ring_level * (j as f32 * 2.0).sin();
}
}
let nf = NoiseFloorStats {
peak: noise,
rms: noise * 0.7,
low_freq_energy: noise * 0.3,
};
let hits = detect_hits(&samples, &nf, sample_rate);
assert_eq!(hits.len(), 1);
assert!(hits[0].ring_end_sample.is_some());
}
#[test]
fn test_derive_params_basic() {
let nf = NoiseFloorStats {
peak: 0.003,
rms: 0.002,
low_freq_energy: 0.0005,
};
let hits = vec![HitEnvelope {
peak_amplitude: 0.72,
onset_sample: 1000,
peak_sample: 1100,
decay_sample: 2200,
ring_end_sample: None,
}];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert_eq!(cal.channel, 1);
assert!((cal.threshold - 0.015).abs() < 0.001);
assert!((cal.gain - 1.319).abs() < 0.1);
assert!(cal.scan_time_ms >= 1);
assert!(cal.retrigger_time_ms >= 5);
assert_eq!(cal.highpass_freq, None); }
#[test]
fn test_derive_params_highpass() {
let nf = NoiseFloorStats {
peak: 0.003,
rms: 0.002,
low_freq_energy: 0.0015, };
let hits = vec![HitEnvelope {
peak_amplitude: 0.5,
onset_sample: 1000,
peak_sample: 1050,
decay_sample: 2000,
ring_end_sample: None,
}];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert_eq!(cal.highpass_freq, Some(80.0));
}
#[test]
fn test_derive_params_dynamic_threshold() {
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0002,
};
let hits = vec![HitEnvelope {
peak_amplitude: 0.6,
onset_sample: 1000,
peak_sample: 1050,
decay_sample: 1500,
ring_end_sample: Some(1500 + 1000),
}];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert!(cal.dynamic_threshold_decay_ms.is_some());
let decay = cal.dynamic_threshold_decay_ms.unwrap();
assert!(decay > 5);
}
#[test]
fn test_crosstalk_detected() {
let sample_rate: u32 = 44100;
let total = 44100;
let mut ch0 = vec![0.0f32; total];
for j in 0..100 {
let idx = 5000 + j;
ch0[idx] = 0.8 * (1.0 - j as f32 / 100.0);
}
let mut ch1 = vec![0.0f32; total];
for j in 0..50 {
let idx = 5010 + j;
if idx < total {
ch1[idx] = 0.05 * (1.0 - j as f32 / 50.0);
}
}
let nf0 = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
};
let nf1 = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
};
let hits0 = vec![HitEnvelope {
peak_amplitude: 0.8,
onset_sample: 5000,
peak_sample: 5000,
decay_sample: 5100,
ring_end_sample: None,
}];
let hits1 = vec![];
let all_samples = vec![ch0, ch1];
let all_hits = vec![hits0, hits1];
let noise_floors = vec![nf0, nf1];
let ct = analyze_crosstalk(&all_samples, &all_hits, &noise_floors, sample_rate);
assert!(ct.crosstalk_window_ms.is_some());
assert!(ct.crosstalk_threshold.is_some());
assert!(ct.crosstalk_threshold.unwrap() >= 2.0);
}
#[test]
fn test_crosstalk_absent() {
let total = 44100;
let mut ch0 = vec![0.0f32; total];
for j in 0..100 {
ch0[5000 + j] = 0.8 * (1.0 - j as f32 / 100.0);
}
let mut ch1 = vec![0.0f32; total];
for j in 0..100 {
ch1[30000 + j] = 0.6 * (1.0 - j as f32 / 100.0);
}
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
};
let hits0 = vec![HitEnvelope {
peak_amplitude: 0.8,
onset_sample: 5000,
peak_sample: 5000,
decay_sample: 5100,
ring_end_sample: None,
}];
let hits1 = vec![HitEnvelope {
peak_amplitude: 0.6,
onset_sample: 30000,
peak_sample: 30000,
decay_sample: 30100,
ring_end_sample: None,
}];
let all_samples = vec![ch0, ch1];
let all_hits = vec![hits0, hits1];
let noise_floors = vec![
NoiseFloorStats {
peak: nf.peak,
rms: nf.rms,
low_freq_energy: nf.low_freq_energy,
},
NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
},
];
let ct = analyze_crosstalk(&all_samples, &all_hits, &noise_floors, 44100);
assert!(ct.crosstalk_window_ms.is_none());
assert!(ct.crosstalk_threshold.is_none());
}
#[test]
fn test_detect_hits_oscillating_signal_counts_as_one() {
let sample_rate: u32 = 44100;
let total = sample_rate as usize * 2;
let noise = 0.001;
let mut samples = vec![0.0f32; total];
for (i, s) in samples.iter_mut().enumerate() {
*s = noise * (i as f32 * 0.7).sin();
}
let onset = 5000;
let peak = 0.6;
let decay_tau = 3000.0f32; let osc_freq = 800.0; let ring_len = 15000; for j in 0..ring_len {
let idx = onset + j;
if idx >= total {
break;
}
let t = j as f32;
let envelope = peak * (-t / decay_tau).exp();
let osc = (2.0 * std::f32::consts::PI * osc_freq * t / sample_rate as f32).sin();
samples[idx] = envelope * osc;
}
let nf = NoiseFloorStats {
peak: noise,
rms: noise * 0.7,
low_freq_energy: noise * 0.3,
};
let hits = detect_hits(&samples, &nf, sample_rate);
assert_eq!(
hits.len(),
1,
"Oscillating decay from a single tap should be detected as 1 hit, got {}",
hits.len()
);
assert!(hits[0].peak_amplitude > 0.4);
}
#[test]
fn test_analyze_noise_floor_empty() {
let stats = analyze_noise_floor(&[], 44100);
assert_eq!(stats.peak, 0.0);
assert_eq!(stats.rms, 0.0);
assert_eq!(stats.low_freq_energy, 0.0);
}
#[test]
fn test_analyze_noise_floor_short_samples() {
let samples: Vec<f32> = (0..200).map(|i| 0.002 * (i as f32 * 0.1).sin()).collect();
let stats = analyze_noise_floor(&samples, 44100);
assert!(stats.peak > 0.0);
assert!(stats.rms > 0.0);
assert_eq!(stats.low_freq_energy, stats.rms);
}
#[test]
fn test_detect_hits_hit_at_end_of_buffer() {
let total = 5000;
let noise = 0.001;
let mut samples = vec![0.0f32; total];
for (i, s) in samples.iter_mut().enumerate() {
*s = noise * (i as f32 * 0.7).sin();
}
let onset = total - 100;
for j in 0..50 {
let idx = onset + j;
if idx < total {
samples[idx] = 0.8 * (1.0 - j as f32 / 50.0);
}
}
let nf = NoiseFloorStats {
peak: noise,
rms: noise * 0.7,
low_freq_energy: noise * 0.3,
};
let hits = detect_hits(&samples, &nf, 44100);
assert_eq!(hits.len(), 1);
assert!(hits[0].peak_amplitude > 0.5);
}
#[test]
fn test_derive_channel_params_no_hits() {
let nf = NoiseFloorStats {
peak: 0.003,
rms: 0.002,
low_freq_energy: 0.0005,
};
let cal = derive_channel_params(1, &nf, &[], 44100);
assert_eq!(cal.gain, 1.0);
assert_eq!(cal.scan_time_ms, 5);
assert_eq!(cal.retrigger_time_ms, 30);
assert_eq!(cal.dynamic_threshold_decay_ms, None);
assert_eq!(cal.num_hits_detected, 0);
}
#[test]
fn test_derive_channel_params_hits_without_ring() {
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0002,
};
let hits = vec![
HitEnvelope {
peak_amplitude: 0.5,
onset_sample: 1000,
peak_sample: 1050,
decay_sample: 2000,
ring_end_sample: None,
},
HitEnvelope {
peak_amplitude: 0.6,
onset_sample: 10000,
peak_sample: 10040,
decay_sample: 11000,
ring_end_sample: None,
},
];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert_eq!(cal.dynamic_threshold_decay_ms, None);
assert_eq!(cal.num_hits_detected, 2);
}
#[test]
fn test_derive_channel_params_short_ring() {
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0002,
};
let hits = vec![HitEnvelope {
peak_amplitude: 0.6,
onset_sample: 1000,
peak_sample: 1050,
decay_sample: 1500,
ring_end_sample: Some(1510),
}];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert_eq!(cal.dynamic_threshold_decay_ms, None);
}
#[test]
fn test_detection_threshold_minimum_floor() {
let nf = NoiseFloorStats {
peak: 0.0001,
rms: 0.00005,
low_freq_energy: 0.00002,
};
let t = detection_threshold(&nf);
assert_eq!(t, 0.005); }
#[test]
fn test_detection_threshold_zero_noise() {
let nf = NoiseFloorStats {
peak: 0.0,
rms: 0.0,
low_freq_energy: 0.0,
};
let t = detection_threshold(&nf);
assert_eq!(t, 0.005);
}
#[test]
fn test_detection_threshold_high_noise() {
let nf = NoiseFloorStats {
peak: 0.01,
rms: 0.007,
low_freq_energy: 0.003,
};
let t = detection_threshold(&nf);
assert!((t - 0.05).abs() < 0.001); }
#[test]
fn test_analyze_noise_floor_constant_signal() {
let samples = vec![0.01; 1000];
let stats = analyze_noise_floor(&samples, 44100);
assert!((stats.peak - 0.01).abs() < 0.0001);
assert!((stats.rms - 0.01).abs() < 0.0001);
}
#[test]
fn test_analyze_noise_floor_single_sample() {
let samples = vec![0.05];
let stats = analyze_noise_floor(&samples, 44100);
assert!((stats.peak - 0.05).abs() < 0.001);
assert!((stats.rms - 0.05).abs() < 0.001);
assert_eq!(stats.low_freq_energy, stats.rms);
}
#[test]
fn test_analyze_noise_floor_low_sample_rate() {
let samples: Vec<f32> = (0..50).map(|i| 0.003 * (i as f32 * 0.2).sin()).collect();
let stats = analyze_noise_floor(&samples, 100);
assert!(stats.peak > 0.0);
assert!(stats.rms > 0.0);
assert!(stats.low_freq_energy >= 0.0);
}
#[test]
fn test_analyze_noise_floor_negative_values() {
let samples: Vec<f32> = (0..1000).map(|i| -0.005 * (i as f32 * 0.3).sin()).collect();
let stats = analyze_noise_floor(&samples, 44100);
assert!(stats.peak > 0.0);
assert!(stats.peak <= 0.005);
assert!(stats.rms > 0.0);
}
#[test]
fn test_detect_hits_empty_samples() {
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
};
let hits = detect_hits(&[], &nf, 44100);
assert!(hits.is_empty());
}
#[test]
fn test_detect_hits_all_below_threshold() {
let samples: Vec<f32> = (0..10000).map(|i| 0.004 * (i as f32 * 0.1).sin()).collect();
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
};
let hits = detect_hits(&samples, &nf, 44100);
assert!(hits.is_empty());
}
#[test]
fn test_detect_hits_decay_not_completed_mid_buffer() {
let sample_rate: u32 = 44100;
let noise = 0.001;
let threshold = (noise * 5.0f32).max(0.005);
let onset = 100;
let above_len = 3000;
let total = onset + above_len + 10;
let mut samples = vec![0.0f32; total];
for (i, s) in samples.iter_mut().enumerate() {
*s = noise * 0.1 * (i as f32 * 0.7).sin();
}
for j in 0..above_len {
let idx = onset + j;
if idx < total {
let level = if j % 100 < 5 {
threshold * 0.5
} else {
threshold * 2.0
};
samples[idx] = level;
}
}
let nf = NoiseFloorStats {
peak: noise,
rms: noise * 0.7,
low_freq_energy: noise * 0.3,
};
let hits = detect_hits(&samples, &nf, sample_rate);
assert_eq!(hits.len(), 1);
}
#[test]
fn test_detect_hits_ring_end_not_found() {
let sample_rate: u32 = 44100;
let holdoff = (sample_rate as f64 * 0.050) as usize;
let noise = 0.001;
let total = 10000;
let mut samples = vec![0.0f32; total];
for (i, s) in samples.iter_mut().enumerate() {
*s = noise * 0.1 * (i as f32 * 0.7).sin();
}
let onset = 100;
for j in 0..50 {
let idx = onset + j;
if idx < total {
samples[idx] = 0.5 * (1.0 - j as f32 / 50.0);
}
}
let decay_end = onset + 50 + holdoff;
for (idx, sample) in samples
.iter_mut()
.enumerate()
.skip(decay_end)
.take(total - decay_end)
{
*sample = 0.002 * (idx as f32 * 3.0).sin();
}
let nf = NoiseFloorStats {
peak: noise,
rms: noise * 0.7,
low_freq_energy: noise * 0.3,
};
let hits = detect_hits(&samples, &nf, sample_rate);
assert_eq!(hits.len(), 1);
}
#[test]
fn test_detect_hits_peak_tracking() {
let sample_rate: u32 = 44100;
let total = sample_rate as usize;
let mut samples = vec![0.0f32; total];
let onset = 1000;
let ramp_len = 200;
let peak_val = 0.9;
for j in 0..ramp_len {
let idx = onset + j;
if idx < total {
samples[idx] = peak_val * (j as f32 / ramp_len as f32);
}
}
let peak_pos = onset + ramp_len;
for j in 0..100 {
let idx = peak_pos + j;
if idx < total {
samples[idx] = peak_val * (1.0 - j as f32 / 100.0);
}
}
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
};
let hits = detect_hits(&samples, &nf, sample_rate);
assert_eq!(hits.len(), 1);
assert!((hits[0].peak_amplitude - peak_val).abs() < 0.01);
assert!(hits[0].peak_sample >= onset + ramp_len - 5);
assert!(hits[0].peak_sample <= onset + ramp_len + 5);
}
#[test]
fn test_derive_channel_params_gain_clamp_high() {
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0002,
};
let hits = vec![HitEnvelope {
peak_amplitude: 0.01,
onset_sample: 1000,
peak_sample: 1050,
decay_sample: 2000,
ring_end_sample: None,
}];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert_eq!(cal.gain, 50.0);
}
#[test]
fn test_derive_channel_params_gain_clamp_low() {
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0002,
};
let hits = vec![HitEnvelope {
peak_amplitude: 100.0,
onset_sample: 1000,
peak_sample: 1050,
decay_sample: 2000,
ring_end_sample: None,
}];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert_eq!(cal.gain, 0.1);
}
#[test]
fn test_derive_channel_params_multiple_hits_median() {
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0002,
};
let hits = vec![
HitEnvelope {
peak_amplitude: 0.5,
onset_sample: 1000,
peak_sample: 1010,
decay_sample: 2000,
ring_end_sample: None,
},
HitEnvelope {
peak_amplitude: 0.6,
onset_sample: 10000,
peak_sample: 10100,
decay_sample: 11000,
ring_end_sample: None,
},
HitEnvelope {
peak_amplitude: 0.4,
onset_sample: 20000,
peak_sample: 20050,
decay_sample: 21000,
ring_end_sample: None,
},
];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert!(cal.scan_time_ms >= 1);
assert_eq!(cal.num_hits_detected, 3);
assert!((cal.max_hit_amplitude - 0.6).abs() < 0.001);
}
#[test]
fn test_derive_channel_params_zero_rms_no_highpass() {
let nf = NoiseFloorStats {
peak: 0.0,
rms: 0.0,
low_freq_energy: 0.0,
};
let cal = derive_channel_params(1, &nf, &[], 44100);
assert_eq!(cal.highpass_freq, None);
}
#[test]
fn test_derive_channel_params_multiple_hits_with_mixed_ring() {
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0002,
};
let hits = vec![
HitEnvelope {
peak_amplitude: 0.5,
onset_sample: 1000,
peak_sample: 1050,
decay_sample: 1500,
ring_end_sample: Some(1500 + 1000),
},
HitEnvelope {
peak_amplitude: 0.6,
onset_sample: 10000,
peak_sample: 10040,
decay_sample: 10500,
ring_end_sample: None,
},
HitEnvelope {
peak_amplitude: 0.4,
onset_sample: 20000,
peak_sample: 20060,
decay_sample: 20500,
ring_end_sample: Some(20500 + 500),
},
];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert!(cal.dynamic_threshold_decay_ms.is_some());
assert_eq!(cal.num_hits_detected, 3);
}
#[test]
fn test_derive_channel_params_retrigger_minimum() {
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0002,
};
let hits = vec![HitEnvelope {
peak_amplitude: 0.5,
onset_sample: 1000,
peak_sample: 1050,
decay_sample: 1051,
ring_end_sample: None,
}];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert_eq!(cal.retrigger_time_ms, 5);
}
#[test]
fn test_derive_channel_params_scan_time_minimum() {
let nf = NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0002,
};
let hits = vec![HitEnvelope {
peak_amplitude: 0.5,
onset_sample: 1000,
peak_sample: 1000,
decay_sample: 2000,
ring_end_sample: None,
}];
let cal = derive_channel_params(1, &nf, &hits, 44100);
assert_eq!(cal.scan_time_ms, 1);
}
#[test]
fn test_derive_channel_params_threshold_value() {
let nf = NoiseFloorStats {
peak: 0.002,
rms: 0.001,
low_freq_energy: 0.0005,
};
let cal = derive_channel_params(1, &nf, &[], 44100);
assert!((cal.threshold - 0.01).abs() < 0.0001);
assert_eq!(cal.noise_floor_peak, 0.002);
assert_eq!(cal.max_hit_amplitude, 0.0);
}
#[test]
fn test_crosstalk_single_channel() {
let samples = vec![vec![0.0f32; 44100]];
let hits = vec![vec![HitEnvelope {
peak_amplitude: 0.8,
onset_sample: 5000,
peak_sample: 5000,
decay_sample: 5100,
ring_end_sample: None,
}]];
let noise_floors = vec![NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
}];
let ct = analyze_crosstalk(&samples, &hits, &noise_floors, 44100);
assert!(ct.crosstalk_window_ms.is_none());
assert!(ct.crosstalk_threshold.is_none());
}
#[test]
fn test_crosstalk_no_hits() {
let samples = vec![vec![0.0f32; 44100], vec![0.0f32; 44100]];
let hits: Vec<Vec<HitEnvelope>> = vec![vec![], vec![]];
let noise_floors = vec![
NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
},
NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
},
];
let ct = analyze_crosstalk(&samples, &hits, &noise_floors, 44100);
assert!(ct.crosstalk_window_ms.is_none());
assert!(ct.crosstalk_threshold.is_none());
}
#[test]
fn test_crosstalk_three_channels() {
let total = 44100;
let mut ch0 = vec![0.0f32; total];
let mut ch1 = vec![0.0f32; total];
let mut ch2 = vec![0.0f32; total];
for j in 0..100 {
ch0[5000 + j] = 0.8 * (1.0 - j as f32 / 100.0);
}
for j in 0..30 {
ch1[5005 + j] = 0.04 * (1.0 - j as f32 / 30.0);
}
for j in 0..20 {
ch2[5008 + j] = 0.03 * (1.0 - j as f32 / 20.0);
}
let hits0 = vec![HitEnvelope {
peak_amplitude: 0.8,
onset_sample: 5000,
peak_sample: 5000,
decay_sample: 5100,
ring_end_sample: None,
}];
let ct = analyze_crosstalk(
&[ch0, ch1, ch2],
&[hits0, vec![], vec![]],
&[
NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
},
NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
},
NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
},
],
44100,
);
assert!(ct.crosstalk_window_ms.is_some());
assert!(ct.crosstalk_threshold.is_some());
}
#[test]
fn test_crosstalk_window_minimum() {
let total = 44100;
let mut ch0 = vec![0.0f32; total];
let mut ch1 = vec![0.0f32; total];
for j in 0..100 {
ch0[5000 + j] = 0.8 * (1.0 - j as f32 / 100.0);
}
ch1[5000] = 0.05;
let hits0 = vec![HitEnvelope {
peak_amplitude: 0.8,
onset_sample: 5000,
peak_sample: 5000,
decay_sample: 5100,
ring_end_sample: None,
}];
let ct = analyze_crosstalk(
&[ch0, ch1],
&[hits0, vec![]],
&[
NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
},
NoiseFloorStats {
peak: 0.001,
rms: 0.0007,
low_freq_energy: 0.0003,
},
],
44100,
);
assert!(ct.crosstalk_window_ms.is_some());
assert!(ct.crosstalk_window_ms.unwrap() >= 2);
}
}