use std::f32::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhaseLockingMode {
Identity,
RegionOfInfluence,
Selective,
}
const SNR_STRONG: f32 = 3.0;
const SNR_MEDIUM: f32 = 1.5;
const SNR_RADIUS: usize = 2;
const SELECTIVE_MIN_LOCK_STRENGTH: f32 = 0.05;
const SELECTIVE_PEAK_WEIGHT: f32 = 0.7;
const SELECTIVE_BIN_SNR_WEIGHT: f32 = 0.3;
const SELECTIVE_RELATIVE_MAG_EPS: f32 = 1e-12;
pub fn apply_phase_locking(
mode: PhaseLockingMode,
magnitudes: &[f32],
analysis_phases: &[f32],
synthesis_phases: &mut [f32],
num_bins: usize,
start_bin: usize,
peaks: &mut Vec<usize>,
) {
match mode {
PhaseLockingMode::Identity => {
identity_phase_lock(
magnitudes,
analysis_phases,
synthesis_phases,
num_bins,
start_bin,
peaks,
);
}
PhaseLockingMode::RegionOfInfluence => {
roi_phase_lock(
magnitudes,
analysis_phases,
synthesis_phases,
num_bins,
start_bin,
peaks,
);
}
PhaseLockingMode::Selective => {
selective_phase_lock(
magnitudes,
analysis_phases,
synthesis_phases,
num_bins,
start_bin,
peaks,
);
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn apply_phase_locking_realtime(
mode: PhaseLockingMode,
magnitudes: &[f32],
analysis_phases: &[f32],
synthesis_phases: &mut [f32],
num_bins: usize,
start_bin: usize,
peaks: &mut Vec<usize>,
troughs_scratch: &mut Vec<usize>,
pv_phases_scratch: &mut Vec<f32>,
) {
match mode {
PhaseLockingMode::Identity => identity_phase_lock_realtime(
magnitudes,
analysis_phases,
synthesis_phases,
num_bins,
start_bin,
peaks,
troughs_scratch,
),
PhaseLockingMode::RegionOfInfluence => roi_phase_lock_realtime(
magnitudes,
analysis_phases,
synthesis_phases,
num_bins,
start_bin,
peaks,
troughs_scratch,
pv_phases_scratch,
),
PhaseLockingMode::Selective => selective_phase_lock_realtime(
magnitudes,
analysis_phases,
synthesis_phases,
num_bins,
start_bin,
peaks,
troughs_scratch,
pv_phases_scratch,
),
}
}
pub fn find_spectral_peaks(magnitudes: &[f32], num_bins: usize, start_bin: usize) -> Vec<usize> {
let mut peaks = Vec::with_capacity(num_bins / 4);
fill_spectral_peaks(magnitudes, num_bins, start_bin, &mut peaks);
peaks
}
pub fn find_spectral_troughs(magnitudes: &[f32], num_bins: usize, start_bin: usize) -> Vec<usize> {
let mut troughs = Vec::with_capacity(num_bins / 2);
fill_spectral_troughs(magnitudes, num_bins, start_bin, &mut troughs);
troughs
}
#[inline]
fn fill_spectral_peaks(
magnitudes: &[f32],
num_bins: usize,
start_bin: usize,
peaks_out: &mut Vec<usize>,
) {
peaks_out.clear();
if num_bins < 3 || start_bin >= num_bins {
return;
}
let search_start = start_bin.max(1);
for k in search_start..num_bins - 1 {
if magnitudes[k] > magnitudes[k - 1] && magnitudes[k] > magnitudes[k + 1] {
peaks_out.push(k);
}
}
}
#[inline]
fn fill_spectral_troughs(
magnitudes: &[f32],
num_bins: usize,
start_bin: usize,
troughs_out: &mut Vec<usize>,
) {
troughs_out.clear();
if num_bins < 2 {
if num_bins == 1 {
troughs_out.push(0);
}
return;
}
troughs_out.push(start_bin);
let search_start = (start_bin + 1).max(1);
for k in search_start..num_bins - 1 {
if magnitudes[k] <= magnitudes[k - 1] && magnitudes[k] <= magnitudes[k + 1] {
troughs_out.push(k);
}
}
if num_bins - 1 > start_bin {
troughs_out.push(num_bins - 1);
}
}
pub fn find_influence_region(
peak: usize,
troughs: &[usize],
start_bin: usize,
num_bins: usize,
) -> (usize, usize) {
let start = troughs
.iter()
.rev()
.find(|&&t| t <= peak)
.copied()
.unwrap_or(start_bin);
let end = troughs
.iter()
.find(|&&t| t > peak)
.copied()
.unwrap_or(num_bins.saturating_sub(1));
(start, end)
}
fn identity_phase_lock(
magnitudes: &[f32],
analysis_phases: &[f32],
synthesis_phases: &mut [f32],
num_bins: usize,
start_bin: usize,
peaks: &mut Vec<usize>,
) {
if num_bins < 3 || start_bin >= num_bins {
return;
}
let mut troughs = Vec::new();
identity_phase_lock_realtime(
magnitudes,
analysis_phases,
synthesis_phases,
num_bins,
start_bin,
peaks,
&mut troughs,
);
}
fn roi_phase_lock(
magnitudes: &[f32],
analysis_phases: &[f32],
synthesis_phases: &mut [f32],
num_bins: usize,
start_bin: usize,
peaks: &mut Vec<usize>,
) {
if num_bins < 3 || start_bin >= num_bins {
return;
}
let mut troughs = Vec::new();
let mut pv_phases = Vec::new();
roi_phase_lock_realtime(
magnitudes,
analysis_phases,
synthesis_phases,
num_bins,
start_bin,
peaks,
&mut troughs,
&mut pv_phases,
);
}
fn selective_phase_lock(
magnitudes: &[f32],
analysis_phases: &[f32],
synthesis_phases: &mut [f32],
num_bins: usize,
start_bin: usize,
peaks: &mut Vec<usize>,
) {
if num_bins < 3 || start_bin >= num_bins {
return;
}
let mut troughs = Vec::new();
let mut pv_phases = Vec::new();
selective_phase_lock_realtime(
magnitudes,
analysis_phases,
synthesis_phases,
num_bins,
start_bin,
peaks,
&mut troughs,
&mut pv_phases,
);
}
fn identity_phase_lock_realtime(
magnitudes: &[f32],
analysis_phases: &[f32],
synthesis_phases: &mut [f32],
num_bins: usize,
start_bin: usize,
peaks: &mut Vec<usize>,
troughs_scratch: &mut Vec<usize>,
) {
if num_bins < 3 || start_bin >= num_bins {
return;
}
fill_spectral_peaks(magnitudes, num_bins, start_bin, peaks);
if peaks.is_empty() {
return;
}
fill_spectral_troughs(magnitudes, num_bins, start_bin, troughs_scratch);
apply_identity_phase_locking(
analysis_phases,
synthesis_phases,
peaks,
troughs_scratch,
start_bin,
num_bins,
);
}
#[allow(clippy::too_many_arguments)]
fn roi_phase_lock_realtime(
magnitudes: &[f32],
analysis_phases: &[f32],
synthesis_phases: &mut [f32],
num_bins: usize,
start_bin: usize,
peaks: &mut Vec<usize>,
troughs_scratch: &mut Vec<usize>,
pv_phases_scratch: &mut Vec<f32>,
) {
if num_bins < 3 || start_bin >= num_bins {
return;
}
fill_spectral_peaks(magnitudes, num_bins, start_bin, peaks);
if peaks.is_empty() {
return;
}
fill_spectral_troughs(magnitudes, num_bins, start_bin, troughs_scratch);
pv_phases_scratch.clear();
pv_phases_scratch.extend_from_slice(&synthesis_phases[start_bin..num_bins]);
apply_identity_phase_locking(
analysis_phases,
synthesis_phases,
peaks,
troughs_scratch,
start_bin,
num_bins,
);
for bin in start_bin..num_bins {
if peaks.binary_search(&bin).is_ok() {
continue;
}
let max_dev = compute_adaptive_max_deviation(magnitudes, bin, num_bins);
let expected = pv_phases_scratch[bin - start_bin];
let deviation = wrap_phase(synthesis_phases[bin] - expected);
if deviation.abs() > max_dev {
let clamped_dev = deviation.clamp(-max_dev, max_dev);
synthesis_phases[bin] = expected + clamped_dev;
}
}
}
#[allow(clippy::too_many_arguments)]
fn selective_phase_lock_realtime(
magnitudes: &[f32],
analysis_phases: &[f32],
synthesis_phases: &mut [f32],
num_bins: usize,
start_bin: usize,
peaks: &mut Vec<usize>,
troughs_scratch: &mut Vec<usize>,
pv_phases_scratch: &mut Vec<f32>,
) {
if num_bins < 3 || start_bin >= num_bins {
return;
}
fill_spectral_peaks(magnitudes, num_bins, start_bin, peaks);
if peaks.is_empty() {
return;
}
fill_spectral_troughs(magnitudes, num_bins, start_bin, troughs_scratch);
pv_phases_scratch.clear();
pv_phases_scratch.extend_from_slice(&synthesis_phases[start_bin..num_bins]);
for &peak in peaks.iter() {
let peak_snr = estimate_local_snr(magnitudes, peak, num_bins);
let peak_conf = snr_to_lock_confidence(peak_snr);
if peak_conf <= 0.0 {
continue;
}
let peak_mag = magnitudes[peak].max(SELECTIVE_RELATIVE_MAG_EPS);
let phase_rotation = synthesis_phases[peak] - analysis_phases[peak];
let (region_start, region_end) =
find_influence_region(peak, troughs_scratch, start_bin, num_bins);
let end = region_end.min(num_bins.saturating_sub(1));
for bin in region_start..=end {
if bin == peak || bin < start_bin || bin >= num_bins {
continue;
}
let rel_mag = (magnitudes[bin].max(0.0) / peak_mag).clamp(0.0, 1.0);
if rel_mag <= 0.0 {
continue;
}
let bin_snr = estimate_local_snr(magnitudes, bin, num_bins);
let bin_conf = snr_to_lock_confidence(bin_snr);
let local_weight =
SELECTIVE_PEAK_WEIGHT * rel_mag.sqrt() + SELECTIVE_BIN_SNR_WEIGHT * bin_conf;
let lock_strength = (peak_conf * local_weight).clamp(0.0, 1.0);
if lock_strength < SELECTIVE_MIN_LOCK_STRENGTH {
continue;
}
let pv_phase = pv_phases_scratch[bin - start_bin];
let locked_phase = analysis_phases[bin] + phase_rotation;
let delta = wrap_phase(locked_phase - pv_phase);
synthesis_phases[bin] = pv_phase + lock_strength * delta;
}
}
for bin in start_bin..num_bins {
if peaks.binary_search(&bin).is_ok() {
continue;
}
let max_dev = compute_adaptive_max_deviation(magnitudes, bin, num_bins);
let expected = pv_phases_scratch[bin - start_bin];
let deviation = wrap_phase(synthesis_phases[bin] - expected);
if deviation.abs() > max_dev {
let clamped_dev = deviation.clamp(-max_dev, max_dev);
synthesis_phases[bin] = expected + clamped_dev;
}
}
}
pub fn apply_identity_phase_locking(
analysis_phases: &[f32],
synthesis_phases: &mut [f32],
peaks: &[usize],
troughs: &[usize],
start_bin: usize,
num_bins: usize,
) {
for &peak in peaks {
let (region_start, region_end) = find_influence_region(peak, troughs, start_bin, num_bins);
let phase_rotation = synthesis_phases[peak] - analysis_phases[peak];
let end = region_end.min(num_bins.saturating_sub(1));
for bin in region_start..=end {
if bin != peak && bin >= start_bin && bin < num_bins {
synthesis_phases[bin] = analysis_phases[bin] + phase_rotation;
}
}
}
}
#[inline]
fn compute_adaptive_max_deviation(magnitudes: &[f32], bin: usize, num_bins: usize) -> f32 {
let local_snr = estimate_local_snr(magnitudes, bin, num_bins);
let t = ((local_snr - SNR_MEDIUM) / (SNR_STRONG - SNR_MEDIUM)).clamp(0.0, 1.0);
let min_dev = PI / 4.0; let max_dev = PI / 3.0; min_dev + t * (max_dev - min_dev)
}
#[inline]
fn snr_to_lock_confidence(local_snr: f32) -> f32 {
((local_snr - SNR_MEDIUM) / (SNR_STRONG - SNR_MEDIUM)).clamp(0.0, 1.0)
}
#[inline]
fn estimate_local_snr(magnitudes: &[f32], bin: usize, num_bins: usize) -> f32 {
let start = bin.saturating_sub(SNR_RADIUS);
let end = (bin + SNR_RADIUS + 1).min(num_bins);
let neighborhood = &magnitudes[start..end];
let median = local_median(neighborhood);
if median < 1e-12 {
if magnitudes[bin] > 1e-12 {
return SNR_STRONG + 1.0; }
return 0.0; }
magnitudes[bin] / median
}
#[inline]
fn local_median(values: &[f32]) -> f32 {
debug_assert!(!values.is_empty());
let mut buf = [0.0f32; 2 * SNR_RADIUS + 1];
let n = values.len().min(buf.len());
buf[..n].copy_from_slice(&values[..n]);
let buf = &mut buf[..n];
for i in 1..n {
let mut j = i;
while j > 0 && buf[j - 1] > buf[j] {
buf.swap(j - 1, j);
j -= 1;
}
}
buf[n / 2]
}
#[inline]
fn wrap_phase(phase: f32) -> f32 {
let p = phase + PI;
let two_pi = 2.0 * PI;
p - (p / two_pi).floor() * two_pi - PI
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identity_no_peaks() {
let magnitudes = vec![1.0; 10];
let analysis_phases = vec![0.0; 10];
let mut synthesis_phases = vec![0.5; 10];
let original = synthesis_phases.clone();
let mut peaks = Vec::new();
identity_phase_lock(
&magnitudes,
&analysis_phases,
&mut synthesis_phases,
10,
1,
&mut peaks,
);
assert_eq!(synthesis_phases, original);
}
#[test]
fn test_roi_no_peaks() {
let magnitudes = vec![1.0; 10];
let analysis_phases = vec![0.0; 10];
let mut synthesis_phases = vec![0.5; 10];
let original = synthesis_phases.clone();
let mut peaks = Vec::new();
roi_phase_lock(
&magnitudes,
&analysis_phases,
&mut synthesis_phases,
10,
1,
&mut peaks,
);
assert_eq!(synthesis_phases, original);
}
#[test]
fn test_roi_clamps_deviation() {
let mut magnitudes = vec![0.1; 20];
magnitudes[5] = 1.0; magnitudes[15] = 1.0;
let analysis_phases = vec![0.0; 20];
let mut synthesis_phases: Vec<f32> = (0..20).map(|i| i as f32 * 0.1).collect();
let mut peaks = Vec::new();
roi_phase_lock(
&magnitudes,
&analysis_phases,
&mut synthesis_phases,
20,
1,
&mut peaks,
);
assert_eq!(peaks, vec![5, 15]);
}
#[test]
fn test_selective_no_peaks() {
let magnitudes = vec![1.0; 10];
let analysis_phases = vec![0.0; 10];
let mut synthesis_phases = vec![0.5; 10];
let original = synthesis_phases.clone();
let mut peaks = Vec::new();
selective_phase_lock(
&magnitudes,
&analysis_phases,
&mut synthesis_phases,
10,
1,
&mut peaks,
);
assert_eq!(synthesis_phases, original);
}
#[test]
fn test_selective_moves_bins_toward_locked_phase() {
let num_bins = 32;
let start_bin = 1;
let mut magnitudes = vec![0.01f32; num_bins];
magnitudes[11] = 0.06;
magnitudes[12] = 1.0; magnitudes[13] = 0.05;
let analysis_phases: Vec<f32> = (0..num_bins).map(|i| i as f32 * 0.12).collect();
let mut synthesis_phases: Vec<f32> = (0..num_bins).map(|i| i as f32 * 0.19).collect();
let original = synthesis_phases.clone();
let mut peaks = Vec::new();
selective_phase_lock(
&magnitudes,
&analysis_phases,
&mut synthesis_phases,
num_bins,
start_bin,
&mut peaks,
);
assert!(peaks.contains(&12), "Expected detected peak at bin 12");
let rotation = original[12] - analysis_phases[12];
let locked = analysis_phases[13] + rotation;
let before_err = wrap_phase(original[13] - locked).abs();
let after_err = wrap_phase(synthesis_phases[13] - locked).abs();
assert!(
after_err < before_err,
"Selective locking should move bin 13 toward locked phase (before={}, after={})",
before_err,
after_err
);
}
#[test]
fn test_wrap_phase() {
assert!((wrap_phase(0.0) - 0.0).abs() < 1e-6);
assert!((wrap_phase(PI + 0.1) - (-PI + 0.1)).abs() < 1e-5);
assert!((wrap_phase(-PI - 0.1) - (PI - 0.1)).abs() < 1e-5);
}
#[test]
fn test_find_spectral_peaks_known_spectrum() {
let mut magnitudes = vec![0.1f32; 16];
magnitudes[3] = 1.0;
magnitudes[7] = 0.8;
magnitudes[12] = 0.6;
let peaks = find_spectral_peaks(&magnitudes, 16, 0);
assert!(peaks.contains(&3), "Should find peak at bin 3");
assert!(peaks.contains(&7), "Should find peak at bin 7");
assert!(peaks.contains(&12), "Should find peak at bin 12");
assert_eq!(peaks.len(), 3, "Should find exactly 3 peaks");
}
#[test]
fn test_find_spectral_peaks_respects_start_bin() {
let mut magnitudes = vec![0.1f32; 16];
magnitudes[3] = 1.0;
magnitudes[7] = 0.8;
magnitudes[12] = 0.6;
let peaks = find_spectral_peaks(&magnitudes, 16, 5);
assert!(!peaks.contains(&3), "Should skip peak below start_bin");
assert!(peaks.contains(&7), "Should find peak at bin 7");
assert!(peaks.contains(&12), "Should find peak at bin 12");
assert_eq!(peaks.len(), 2);
}
#[test]
fn test_find_spectral_peaks_empty_and_edge_cases() {
let magnitudes = vec![1.0f32; 2];
assert!(find_spectral_peaks(&magnitudes, 2, 0).is_empty());
let magnitudes = vec![1.0f32; 10];
assert!(find_spectral_peaks(&magnitudes, 10, 0).is_empty());
let magnitudes = vec![0.1, 1.0, 0.1];
assert!(find_spectral_peaks(&magnitudes, 3, 10).is_empty());
}
#[test]
fn test_find_spectral_troughs_known_spectrum() {
let magnitudes = vec![0.1, 0.5, 0.1, 0.05, 0.8, 0.2, 0.1];
let troughs = find_spectral_troughs(&magnitudes, 7, 0);
assert!(troughs.contains(&0), "Should include start_bin as boundary");
assert!(troughs.contains(&6), "Should include last bin as boundary");
assert!(troughs.contains(&3), "Should find trough at bin 3");
}
#[test]
fn test_find_spectral_troughs_with_start_bin() {
let magnitudes = vec![0.1, 0.5, 0.1, 0.05, 0.8, 0.2, 0.1];
let troughs = find_spectral_troughs(&magnitudes, 7, 2);
assert!(
troughs.contains(&2),
"Should include start_bin=2 as boundary"
);
assert!(troughs.contains(&6), "Should include last bin as boundary");
assert!(troughs.contains(&3), "Should find trough at bin 3");
}
#[test]
fn test_find_influence_region_basic() {
let troughs = vec![0, 4, 9, 15];
let (start, end) = find_influence_region(2, &troughs, 0, 16);
assert_eq!(start, 0);
assert_eq!(end, 4);
let (start, end) = find_influence_region(6, &troughs, 0, 16);
assert_eq!(start, 4);
assert_eq!(end, 9);
let (start, end) = find_influence_region(12, &troughs, 0, 16);
assert_eq!(start, 9);
assert_eq!(end, 15);
}
#[test]
fn test_find_influence_region_peak_at_trough_boundary() {
let troughs = vec![0, 5, 10];
let (start, end) = find_influence_region(5, &troughs, 0, 11);
assert_eq!(start, 5);
assert_eq!(end, 10);
}
#[test]
fn test_find_influence_region_no_troughs() {
let troughs: Vec<usize> = vec![];
let (start, end) = find_influence_region(5, &troughs, 0, 16);
assert_eq!(start, 0);
assert_eq!(end, 15);
}
#[test]
fn test_identity_locking_phase_rotation_propagation() {
let num_bins = 16;
let start_bin = 0;
let analysis_phases: Vec<f32> = (0..num_bins).map(|i| i as f32 * 0.2).collect();
let mut synthesis_phases: Vec<f32> = (0..num_bins).map(|i| i as f32 * 0.3).collect();
let peaks = vec![5];
let troughs = vec![0, 3, 8, 15];
let peak_synth_phase = synthesis_phases[5];
let peak_analysis_phase = analysis_phases[5];
let phase_rotation = peak_synth_phase - peak_analysis_phase;
apply_identity_phase_locking(
&analysis_phases,
&mut synthesis_phases,
&peaks,
&troughs,
start_bin,
num_bins,
);
assert!(
(synthesis_phases[5] - peak_synth_phase).abs() < 1e-6,
"Peak bin should keep its phase"
);
for bin in 3..=8 {
if bin == 5 {
continue;
}
let expected = analysis_phases[bin] + phase_rotation;
assert!(
(synthesis_phases[bin] - expected).abs() < 1e-6,
"Bin {} should have rotated phase: expected {}, got {}",
bin,
expected,
synthesis_phases[bin]
);
}
}
#[test]
fn test_identity_locking_different_regions_different_rotations() {
let num_bins = 20;
let start_bin = 0;
let analysis_phases: Vec<f32> = (0..num_bins).map(|i| i as f32 * 0.1).collect();
let mut synthesis_phases: Vec<f32> = vec![0.0; num_bins];
synthesis_phases[4] = 2.0; synthesis_phases[14] = 5.0;
let peaks = vec![4, 14];
let troughs = vec![0, 2, 9, 11, 19];
let rotation_1 = synthesis_phases[4] - analysis_phases[4];
let rotation_2 = synthesis_phases[14] - analysis_phases[14];
apply_identity_phase_locking(
&analysis_phases,
&mut synthesis_phases,
&peaks,
&troughs,
start_bin,
num_bins,
);
for bin in 2..=9 {
if bin == 4 {
continue;
}
let expected = analysis_phases[bin] + rotation_1;
assert!(
(synthesis_phases[bin] - expected).abs() < 1e-6,
"Bin {} (peak 1 region) expected {}, got {}",
bin,
expected,
synthesis_phases[bin]
);
}
for bin in 11..=19 {
if bin == 14 {
continue;
}
let expected = analysis_phases[bin] + rotation_2;
assert!(
(synthesis_phases[bin] - expected).abs() < 1e-6,
"Bin {} (peak 2 region) expected {}, got {}",
bin,
expected,
synthesis_phases[bin]
);
}
}
#[test]
fn test_identity_locking_harmonic_signal() {
let num_bins = 64;
let start_bin = 1;
let mut magnitudes = vec![0.01f32; num_bins];
let peak_bins = [10usize, 20, 30, 40];
let peak_amps = [1.0f32, 0.7, 0.4, 0.2];
for (&p, &a) in peak_bins.iter().zip(peak_amps.iter()) {
for offset in -3i32..=3 {
let bin = (p as i32 + offset) as usize;
if bin < num_bins {
let dist = offset.unsigned_abs() as f32;
magnitudes[bin] = a * (-dist * dist / 2.0).exp();
}
}
}
let analysis_phases: Vec<f32> = (0..num_bins).map(|i| i as f32 * 0.5).collect();
let mut synthesis_phases: Vec<f32> = (0..num_bins).map(|i| i as f32 * 0.52).collect();
let original_synth = synthesis_phases.clone();
let peaks = find_spectral_peaks(&magnitudes, num_bins, start_bin);
let troughs = find_spectral_troughs(&magnitudes, num_bins, start_bin);
apply_identity_phase_locking(
&analysis_phases,
&mut synthesis_phases,
&peaks,
&troughs,
start_bin,
num_bins,
);
for &p in &peaks {
if !peak_bins.contains(&p) {
continue;
}
let rotation = original_synth[p] - analysis_phases[p];
let (region_start, region_end) =
find_influence_region(p, &troughs, start_bin, num_bins);
for bin in region_start..=region_end.min(num_bins - 1) {
if bin == p {
continue;
}
let expected_relative = analysis_phases[bin] - analysis_phases[p]; let actual_relative = synthesis_phases[bin] - synthesis_phases[p]; assert!(
(actual_relative - expected_relative).abs() < 1e-5,
"Bin {} relative to peak {}: expected {}, got {}",
bin,
p,
expected_relative,
actual_relative
);
let expected_abs = analysis_phases[bin] + rotation;
assert!(
(synthesis_phases[bin] - expected_abs).abs() < 1e-5,
"Bin {} absolute phase: expected {}, got {}",
bin,
expected_abs,
synthesis_phases[bin]
);
}
}
}
#[test]
fn test_identity_locking_respects_start_bin() {
let num_bins = 16;
let start_bin = 5;
let analysis_phases = vec![0.0f32; num_bins];
let mut synthesis_phases: Vec<f32> = (0..num_bins).map(|i| i as f32 * 0.1).collect();
let original_sub_bass: Vec<f32> = synthesis_phases[..start_bin].to_vec();
let mut magnitudes = vec![0.1f32; num_bins];
magnitudes[8] = 1.0;
let peaks = find_spectral_peaks(&magnitudes, num_bins, start_bin);
let troughs = find_spectral_troughs(&magnitudes, num_bins, start_bin);
apply_identity_phase_locking(
&analysis_phases,
&mut synthesis_phases,
&peaks,
&troughs,
start_bin,
num_bins,
);
assert_eq!(
&synthesis_phases[..start_bin],
&original_sub_bass[..],
"Sub-bass bins should not be modified"
);
}
#[test]
fn test_chord_stretch_preserves_frequencies() {
let sample_rate = 44100u32;
let fft_size = 4096;
let hop = fft_size / 4;
let stretch_ratio = 1.25;
let num_samples = fft_size * 8;
let freqs = [440.0f32, 554.0, 659.0];
let input: Vec<f32> = (0..num_samples)
.map(|i| {
let t = i as f32 / sample_rate as f32;
freqs
.iter()
.map(|&f| (2.0 * PI * f * t).sin() / freqs.len() as f32)
.sum::<f32>()
})
.collect();
let mut pv = crate::stretch::phase_vocoder::PhaseVocoder::with_options(
fft_size,
hop,
stretch_ratio,
sample_rate,
120.0,
crate::core::window::WindowType::BlackmanHarris,
PhaseLockingMode::RegionOfInfluence,
);
let output = pv.process(&input).unwrap();
assert!(!output.is_empty(), "Output should not be empty");
let analysis_start = output.len() / 4;
let analysis_len = fft_size;
if analysis_start + analysis_len > output.len() {
return; }
let section = &output[analysis_start..analysis_start + analysis_len];
let num_bins = fft_size / 2 + 1;
let bin_freq = sample_rate as f32 / fft_size as f32;
let mut max_mag = 0.0f32;
let mut freq_mags = [0.0f32; 3];
for (idx, &freq) in freqs.iter().enumerate() {
let target_bin = (freq / bin_freq).round() as usize;
let mut mag_sum = 0.0f32;
for offset in -2i32..=2 {
let bin = (target_bin as i32 + offset).max(0) as usize;
if bin < num_bins {
let mut re = 0.0f64;
let mut im = 0.0f64;
for (n, &s) in section.iter().enumerate() {
let angle =
-2.0 * std::f64::consts::PI * bin as f64 * n as f64 / fft_size as f64;
re += s as f64 * angle.cos();
im += s as f64 * angle.sin();
}
mag_sum += (re * re + im * im).sqrt() as f32;
}
}
freq_mags[idx] = mag_sum;
if mag_sum > max_mag {
max_mag = mag_sum;
}
}
let threshold_ratio = 0.25; for (idx, &freq) in freqs.iter().enumerate() {
assert!(
freq_mags[idx] > max_mag * threshold_ratio,
"Frequency {} Hz is too weak after stretching: mag={}, max={}",
freq,
freq_mags[idx],
max_mag
);
}
}
}