From d78bed0291b29daaba4eacf85d0c936529383f06 Mon Sep 17 00:00:00 2001 From: winkt0 <238452092+winkt0@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:01:51 +0100 Subject: [PATCH 1/6] Added YIN algorithm --- src/lib.rs | 1 + src/signal_analysis/mod.rs | 2 + src/signal_analysis/yin.rs | 294 +++++++++++++++++++++++++++++++++++++ 3 files changed, 297 insertions(+) create mode 100644 src/signal_analysis/mod.rs create mode 100644 src/signal_analysis/yin.rs diff --git a/src/lib.rs b/src/lib.rs index 910bf05de06..2267721cc11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ pub mod math; pub mod navigation; pub mod number_theory; pub mod searching; +pub mod signal_analysis; pub mod sorting; pub mod string; diff --git a/src/signal_analysis/mod.rs b/src/signal_analysis/mod.rs new file mode 100644 index 00000000000..bf6c15333ff --- /dev/null +++ b/src/signal_analysis/mod.rs @@ -0,0 +1,2 @@ +mod yin; +pub use self::yin::{Yin, YinResult}; diff --git a/src/signal_analysis/yin.rs b/src/signal_analysis/yin.rs new file mode 100644 index 00000000000..d19bc7c96ff --- /dev/null +++ b/src/signal_analysis/yin.rs @@ -0,0 +1,294 @@ +use std::f64; + +#[derive(Clone, Debug)] +pub struct YinResult { + sample_rate: f64, + best_lag: usize, + cmndf: Vec, +} + +impl YinResult { + pub fn get_frequency(&self) -> f64 { + self.sample_rate / self.best_lag as f64 + } + + pub fn get_frequency_with_interpolation(&self) -> f64 { + let best_lag_with_interpolation = parabolic_interpolation(self.best_lag, &self.cmndf); + self.sample_rate / best_lag_with_interpolation + } +} + +fn parabolic_interpolation(lag: usize, cmndf: &[f64]) -> f64 { + let x0 = lag.saturating_sub(1); // max(0, lag-1) + let x2 = usize::min(cmndf.len() - 1, lag + 1); + let s0 = cmndf[x0]; + let s1 = cmndf[lag]; + let s2 = cmndf[x2]; + let denom = s0 - 2.0 * s1 + s2; + if denom == 0.0 { + return lag as f64; + } + let delta = (s0 - s2) / (2.0 * denom); + lag as f64 + delta +} + +#[derive(Clone, Debug)] +pub struct Yin { + threshold: f64, + min_lag: usize, + max_lag: usize, + sample_rate: f64, +} + +impl Yin { + pub fn init( + threshold: f64, + min_expected_frequency: f64, + max_expected_frequency: f64, + sample_rate: f64, + ) -> Yin { + let min_lag = (sample_rate / max_expected_frequency) as usize; + let max_lag = (sample_rate / min_expected_frequency) as usize; + Yin { + threshold, + min_lag, + max_lag, + sample_rate, + } + } + + pub fn yin(&self, frequencies: &[f64]) -> YinResult { + let df = df_values(frequencies, self.max_lag); + let cmndf = cmndf_values(&df, self.max_lag); + let best_lag = find_cmndf_argmin(&cmndf, self.min_lag, self.max_lag, self.threshold); + YinResult { + sample_rate: self.sample_rate, + best_lag, + cmndf, + } + } +} + +#[allow(clippy::needless_range_loop)] +fn df_values(frequencies: &[f64], max_lag: usize) -> Vec { + let mut df_list = vec![0.0; max_lag + 1]; + for lag in 1..=max_lag { + df_list[lag] = df(frequencies, lag); + } + df_list +} + +fn df(f: &[f64], lag: usize) -> f64 { + let mut sum = 0.0; + let n = f.len(); + for i in 0..(n - lag) { + let diff = f[i] - f[i + lag]; + sum += diff * diff; + } + sum +} + +// Cumulative Mean Normalized Difference Function +fn cmndf_values(df: &[f64], max_lag: usize) -> Vec { + let mut cmndf = vec![0.0; max_lag + 1]; + cmndf[0] = 1.0; + let mut sum = 0.0; + for lag in 1..=max_lag { + sum += df[lag]; + cmndf[lag] = lag as f64 * df[lag] / if sum == 0.0 { 1e-10 } else { sum }; + } + cmndf +} + +fn find_cmndf_argmin(cmndf: &[f64], min_lag: usize, max_lag: usize, threshold: f64) -> usize { + let mut lag = min_lag; + while lag <= max_lag { + if cmndf[lag] < threshold { + while lag < max_lag && cmndf[lag + 1] < cmndf[lag] { + lag += 1; + } + return lag; + } + lag += 1; + } + 0 +} + +#[cfg(test)] +mod tests { + use super::*; + + fn generate_sine_wave(frequency: f64, sample_rate: f64, duration_secs: f64) -> Vec { + let total_samples = (sample_rate * duration_secs).round() as usize; + let two_pi_f = 2.0 * std::f64::consts::PI * frequency; + + (0..total_samples) + .map(|n| { + let t = n as f64 / sample_rate; + (two_pi_f * t).sin() + }) + .collect() + } + + fn diff_from_actual_frequency_smaller_than_threshold( + result_frequency: f64, + actual_frequency: f64, + threshold: f64, + ) -> bool { + let result_diff_from_actual_freq = (result_frequency - actual_frequency).abs(); + result_diff_from_actual_freq < threshold + } + + fn interpolation_better_than_raw_result(result: YinResult, frequency: f64) -> bool { + let result_frequency = result.get_frequency(); + let refined_frequency = result.get_frequency_with_interpolation(); + let result_diff = (result_frequency - frequency).abs(); + let refined_diff = (refined_frequency - frequency).abs(); + refined_diff < result_diff + } + + #[test] + fn test_simple_sine() { + let sample_rate = 1000.0; + let frequency = 12.0; + let seconds = 10.0; + let signal = generate_sine_wave(frequency, sample_rate, seconds); + + let min_expected_frequency = 10.0; + let max_expected_frequency = 100.0; + + let yin = Yin::init( + 0.1, + min_expected_frequency, + max_expected_frequency, + sample_rate, + ); + let result = yin.yin(signal.as_slice()); + + assert!(diff_from_actual_frequency_smaller_than_threshold( + result.get_frequency(), + frequency, + 1.0 + )); + assert!(diff_from_actual_frequency_smaller_than_threshold( + result.get_frequency_with_interpolation(), + frequency, + 1.0, + )); + + assert!(interpolation_better_than_raw_result(result, frequency)); + } + + #[test] + fn test_sine_frequency_range() { + let sample_rate = 10000.0; + for freq in 10..50 { + let frequency = freq as f64; + let seconds = 2.0; + let signal = generate_sine_wave(frequency, sample_rate, seconds); + + let min_expected_frequency = 5.0; + let max_expected_frequency = 100.0; + + let yin = Yin::init( + 0.1, + min_expected_frequency, + max_expected_frequency, + sample_rate, + ); + let result = yin.yin(signal.as_slice()); + + if (sample_rate as i32 % freq) == 0 { + assert_eq!(result.get_frequency(), frequency); + } else { + assert!(diff_from_actual_frequency_smaller_than_threshold( + result.get_frequency(), + frequency, + 1.0 + )); + assert!(diff_from_actual_frequency_smaller_than_threshold( + result.get_frequency_with_interpolation(), + frequency, + 1.0, + )); + + assert!(interpolation_better_than_raw_result(result, frequency)); + } + } + } + + #[test] + fn test_harmonic_sines() { + let sample_rate = 44100.0; + let seconds = 2.0; + let frequency_1 = 50.0; // Minimal/Fundamental frequency - this is what YIN should find + let signal_1 = generate_sine_wave(frequency_1, sample_rate, seconds); + let frequency_2 = 150.0; + let signal_2 = generate_sine_wave(frequency_2, sample_rate, seconds); + let frequency_3 = 300.0; + let signal_3 = generate_sine_wave(frequency_3, sample_rate, seconds); + + let min_expected_frequency = 10.0; + let max_expected_frequency = 500.0; + + let yin = Yin::init( + 0.1, + min_expected_frequency, + max_expected_frequency, + sample_rate, + ); + + let total_samples = (sample_rate * seconds).round() as usize; + let combined_signal: Vec = (0..total_samples) + .map(|n| signal_1[n] + signal_2[n] + signal_3[n]) + .collect(); + + let result = yin.yin(&combined_signal); + + assert!(diff_from_actual_frequency_smaller_than_threshold( + result.get_frequency(), + frequency_1, + 1.0 + )); + } + + #[test] + fn test_unharmonic_sines() { + let sample_rate = 44100.0; + let seconds = 2.0; + let frequency_1 = 50.0; + let signal_1 = generate_sine_wave(frequency_1, sample_rate, seconds); + let frequency_2 = 66.0; + let signal_2 = generate_sine_wave(frequency_2, sample_rate, seconds); + let frequency_3 = 300.0; + let signal_3 = generate_sine_wave(frequency_3, sample_rate, seconds); + + let min_expected_frequency = 10.0; + let max_expected_frequency = 500.0; + + let yin = Yin::init( + 0.1, + min_expected_frequency, + max_expected_frequency, + sample_rate, + ); + + let total_samples = (sample_rate * seconds).round() as usize; + let combined_signal: Vec = (0..total_samples) + .map(|n| signal_1[n] + signal_2[n] + signal_3[n]) + .collect(); + + let result = yin.yin(&combined_signal); + + let expected_frequency = (frequency_1 - frequency_2).abs(); + assert!(diff_from_actual_frequency_smaller_than_threshold( + result.get_frequency(), + expected_frequency, + 1.0 + )); + assert!(interpolation_better_than_raw_result( + result, + expected_frequency + )); + } +} From 8bddbceed3ecf68490946b20f4d0912570b422d9 Mon Sep 17 00:00:00 2001 From: winkt0 <238452092+winkt0@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:10:15 +0100 Subject: [PATCH 2/6] Renamed function names to their full names instead of abbreviations --- src/signal_analysis/yin.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/signal_analysis/yin.rs b/src/signal_analysis/yin.rs index d19bc7c96ff..2b6b348c977 100644 --- a/src/signal_analysis/yin.rs +++ b/src/signal_analysis/yin.rs @@ -58,8 +58,8 @@ impl Yin { } pub fn yin(&self, frequencies: &[f64]) -> YinResult { - let df = df_values(frequencies, self.max_lag); - let cmndf = cmndf_values(&df, self.max_lag); + let df = difference_function_values(frequencies, self.max_lag); + let cmndf = cumulative_mean_normalized_difference_function(&df, self.max_lag); let best_lag = find_cmndf_argmin(&cmndf, self.min_lag, self.max_lag, self.threshold); YinResult { sample_rate: self.sample_rate, @@ -70,15 +70,15 @@ impl Yin { } #[allow(clippy::needless_range_loop)] -fn df_values(frequencies: &[f64], max_lag: usize) -> Vec { +fn difference_function_values(frequencies: &[f64], max_lag: usize) -> Vec { let mut df_list = vec![0.0; max_lag + 1]; for lag in 1..=max_lag { - df_list[lag] = df(frequencies, lag); + df_list[lag] = difference_function(frequencies, lag); } df_list } -fn df(f: &[f64], lag: usize) -> f64 { +fn difference_function(f: &[f64], lag: usize) -> f64 { let mut sum = 0.0; let n = f.len(); for i in 0..(n - lag) { @@ -88,8 +88,7 @@ fn df(f: &[f64], lag: usize) -> f64 { sum } -// Cumulative Mean Normalized Difference Function -fn cmndf_values(df: &[f64], max_lag: usize) -> Vec { +fn cumulative_mean_normalized_difference_function(df: &[f64], max_lag: usize) -> Vec { let mut cmndf = vec![0.0; max_lag + 1]; cmndf[0] = 1.0; let mut sum = 0.0; From 86fcab6b2cf68bf6977c527056c64ebb1ab40063 Mon Sep 17 00:00:00 2001 From: winkt0 <238452092+winkt0@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:19:42 +0100 Subject: [PATCH 3/6] Added YIN to DIRECTORY.md --- DIRECTORY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 1c18dabe4a1..46bb2a3af9f 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -294,6 +294,8 @@ * [Ternary Search Min Max](https://github.com/TheAlgorithms/Rust/blob/master/src/searching/ternary_search_min_max.rs) * [Ternary Search Min Max Recursive](https://github.com/TheAlgorithms/Rust/blob/master/src/searching/ternary_search_min_max_recursive.rs) * [Ternary Search Recursive](https://github.com/TheAlgorithms/Rust/blob/master/src/searching/ternary_search_recursive.rs) + * Signal Analysis + * [YIN](https://github.com/TheAlgorithms/Rust/blob/master/src/signal_analysis/yin.rs) * Sorting * [Bead Sort](https://github.com/TheAlgorithms/Rust/blob/master/src/sorting/bead_sort.rs) * [Binary Insertion Sort](https://github.com/TheAlgorithms/Rust/blob/master/src/sorting/binary_insertion_sort.rs) From 82159fe37af1c67d59a981212f10798bbc23b39d Mon Sep 17 00:00:00 2001 From: winkt0 <238452092+winkt0@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:22:27 +0100 Subject: [PATCH 4/6] Decreased range in tests to decrease test time --- src/signal_analysis/yin.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/signal_analysis/yin.rs b/src/signal_analysis/yin.rs index 2b6b348c977..e706ada9e1d 100644 --- a/src/signal_analysis/yin.rs +++ b/src/signal_analysis/yin.rs @@ -180,8 +180,8 @@ mod tests { #[test] fn test_sine_frequency_range() { - let sample_rate = 10000.0; - for freq in 10..50 { + let sample_rate = 5000.0; + for freq in 30..50 { let frequency = freq as f64; let seconds = 2.0; let signal = generate_sine_wave(frequency, sample_rate, seconds); From d066beea6a15bbe5ebc2c2abe47ae4835a3d04ce Mon Sep 17 00:00:00 2001 From: winkt0 <238452092+winkt0@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:00:06 +0100 Subject: [PATCH 5/6] Refactor: yin returns a Result with possible Err value instead of YinResult. This is necessary to communicate clearly that yin can fail if no argmin for CMNDF is found, for which get_frequency would return inf because the best lag would be 0. Added a test for this case. --- src/signal_analysis/yin.rs | 75 ++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/src/signal_analysis/yin.rs b/src/signal_analysis/yin.rs index e706ada9e1d..271a3df6704 100644 --- a/src/signal_analysis/yin.rs +++ b/src/signal_analysis/yin.rs @@ -57,14 +57,20 @@ impl Yin { } } - pub fn yin(&self, frequencies: &[f64]) -> YinResult { + pub fn yin(&self, frequencies: &[f64]) -> Result { let df = difference_function_values(frequencies, self.max_lag); let cmndf = cumulative_mean_normalized_difference_function(&df, self.max_lag); let best_lag = find_cmndf_argmin(&cmndf, self.min_lag, self.max_lag, self.threshold); - YinResult { - sample_rate: self.sample_rate, - best_lag, - cmndf, + match best_lag { + _ if best_lag == 0 => Err(format!( + "Could not find lag value which minimizes CMNDF below the given threshold {}", + self.threshold + )), + _ => Ok(YinResult { + sample_rate: self.sample_rate, + best_lag, + cmndf, + }), } } } @@ -162,20 +168,23 @@ mod tests { max_expected_frequency, sample_rate, ); + let result = yin.yin(signal.as_slice()); + assert!(result.is_ok()); + let yin_result = result.unwrap(); assert!(diff_from_actual_frequency_smaller_than_threshold( - result.get_frequency(), + yin_result.get_frequency(), frequency, 1.0 )); assert!(diff_from_actual_frequency_smaller_than_threshold( - result.get_frequency_with_interpolation(), + yin_result.get_frequency_with_interpolation(), frequency, 1.0, )); - assert!(interpolation_better_than_raw_result(result, frequency)); + assert!(interpolation_better_than_raw_result(yin_result, frequency)); } #[test] @@ -196,22 +205,24 @@ mod tests { sample_rate, ); let result = yin.yin(signal.as_slice()); + assert!(result.is_ok()); + let yin_result = result.unwrap(); if (sample_rate as i32 % freq) == 0 { - assert_eq!(result.get_frequency(), frequency); + assert_eq!(yin_result.get_frequency(), frequency); } else { assert!(diff_from_actual_frequency_smaller_than_threshold( - result.get_frequency(), + yin_result.get_frequency(), frequency, 1.0 )); assert!(diff_from_actual_frequency_smaller_than_threshold( - result.get_frequency_with_interpolation(), + yin_result.get_frequency_with_interpolation(), frequency, 1.0, )); - assert!(interpolation_better_than_raw_result(result, frequency)); + assert!(interpolation_better_than_raw_result(yin_result, frequency)); } } } @@ -243,9 +254,11 @@ mod tests { .collect(); let result = yin.yin(&combined_signal); + assert!(result.is_ok()); + let yin_result = result.unwrap(); assert!(diff_from_actual_frequency_smaller_than_threshold( - result.get_frequency(), + yin_result.get_frequency(), frequency_1, 1.0 )); @@ -278,16 +291,48 @@ mod tests { .collect(); let result = yin.yin(&combined_signal); + assert!(result.is_ok()); + let yin_result = result.unwrap(); let expected_frequency = (frequency_1 - frequency_2).abs(); assert!(diff_from_actual_frequency_smaller_than_threshold( - result.get_frequency(), + yin_result.get_frequency(), expected_frequency, 1.0 )); assert!(interpolation_better_than_raw_result( - result, + yin_result, expected_frequency )); } + + #[test] + fn test_err() { + let sample_rate = 2500.0; + let seconds = 2.0; + let frequency = 440.0; + + // Can't find frequency 440 between 500 and 700 + let min_expected_frequency = 500.0; + let max_expected_frequency = 700.0; + let yin = Yin::init( + 0.1, + min_expected_frequency, + max_expected_frequency, + sample_rate, + ); + + let signal = generate_sine_wave(frequency, sample_rate, seconds); + let result = yin.yin(&signal); + assert!(result.is_err()); + + let yin_with_suitable_frequency_range = Yin::init( + 0.1, + min_expected_frequency - 100.0, + max_expected_frequency, + sample_rate, + ); + let result = yin_with_suitable_frequency_range.yin(&signal); + assert!(result.is_ok()); + } } From a30b9d7048b2d159bf4dab255b3c465d6cdeb350 Mon Sep 17 00:00:00 2001 From: winkt0 <238452092+winkt0@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:01:26 +0100 Subject: [PATCH 6/6] Replaced inline value 1e-10 with constant as suggested by copilot --- src/signal_analysis/yin.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/signal_analysis/yin.rs b/src/signal_analysis/yin.rs index 271a3df6704..53e378c6b35 100644 --- a/src/signal_analysis/yin.rs +++ b/src/signal_analysis/yin.rs @@ -94,13 +94,14 @@ fn difference_function(f: &[f64], lag: usize) -> f64 { sum } +const EPSILON: f64 = 1e-10; fn cumulative_mean_normalized_difference_function(df: &[f64], max_lag: usize) -> Vec { let mut cmndf = vec![0.0; max_lag + 1]; cmndf[0] = 1.0; let mut sum = 0.0; for lag in 1..=max_lag { sum += df[lag]; - cmndf[lag] = lag as f64 * df[lag] / if sum == 0.0 { 1e-10 } else { sum }; + cmndf[lag] = lag as f64 * df[lag] / if sum == 0.0 { EPSILON } else { sum }; } cmndf }