Skip to content

Commit d78bed0

Browse files
committed
Added YIN algorithm
1 parent db3f011 commit d78bed0

File tree

3 files changed

+297
-0
lines changed

3 files changed

+297
-0
lines changed

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod math;
1616
pub mod navigation;
1717
pub mod number_theory;
1818
pub mod searching;
19+
pub mod signal_analysis;
1920
pub mod sorting;
2021
pub mod string;
2122

src/signal_analysis/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mod yin;
2+
pub use self::yin::{Yin, YinResult};

src/signal_analysis/yin.rs

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
use std::f64;
2+
3+
#[derive(Clone, Debug)]
4+
pub struct YinResult {
5+
sample_rate: f64,
6+
best_lag: usize,
7+
cmndf: Vec<f64>,
8+
}
9+
10+
impl YinResult {
11+
pub fn get_frequency(&self) -> f64 {
12+
self.sample_rate / self.best_lag as f64
13+
}
14+
15+
pub fn get_frequency_with_interpolation(&self) -> f64 {
16+
let best_lag_with_interpolation = parabolic_interpolation(self.best_lag, &self.cmndf);
17+
self.sample_rate / best_lag_with_interpolation
18+
}
19+
}
20+
21+
fn parabolic_interpolation(lag: usize, cmndf: &[f64]) -> f64 {
22+
let x0 = lag.saturating_sub(1); // max(0, lag-1)
23+
let x2 = usize::min(cmndf.len() - 1, lag + 1);
24+
let s0 = cmndf[x0];
25+
let s1 = cmndf[lag];
26+
let s2 = cmndf[x2];
27+
let denom = s0 - 2.0 * s1 + s2;
28+
if denom == 0.0 {
29+
return lag as f64;
30+
}
31+
let delta = (s0 - s2) / (2.0 * denom);
32+
lag as f64 + delta
33+
}
34+
35+
#[derive(Clone, Debug)]
36+
pub struct Yin {
37+
threshold: f64,
38+
min_lag: usize,
39+
max_lag: usize,
40+
sample_rate: f64,
41+
}
42+
43+
impl Yin {
44+
pub fn init(
45+
threshold: f64,
46+
min_expected_frequency: f64,
47+
max_expected_frequency: f64,
48+
sample_rate: f64,
49+
) -> Yin {
50+
let min_lag = (sample_rate / max_expected_frequency) as usize;
51+
let max_lag = (sample_rate / min_expected_frequency) as usize;
52+
Yin {
53+
threshold,
54+
min_lag,
55+
max_lag,
56+
sample_rate,
57+
}
58+
}
59+
60+
pub fn yin(&self, frequencies: &[f64]) -> YinResult {
61+
let df = df_values(frequencies, self.max_lag);
62+
let cmndf = cmndf_values(&df, self.max_lag);
63+
let best_lag = find_cmndf_argmin(&cmndf, self.min_lag, self.max_lag, self.threshold);
64+
YinResult {
65+
sample_rate: self.sample_rate,
66+
best_lag,
67+
cmndf,
68+
}
69+
}
70+
}
71+
72+
#[allow(clippy::needless_range_loop)]
73+
fn df_values(frequencies: &[f64], max_lag: usize) -> Vec<f64> {
74+
let mut df_list = vec![0.0; max_lag + 1];
75+
for lag in 1..=max_lag {
76+
df_list[lag] = df(frequencies, lag);
77+
}
78+
df_list
79+
}
80+
81+
fn df(f: &[f64], lag: usize) -> f64 {
82+
let mut sum = 0.0;
83+
let n = f.len();
84+
for i in 0..(n - lag) {
85+
let diff = f[i] - f[i + lag];
86+
sum += diff * diff;
87+
}
88+
sum
89+
}
90+
91+
// Cumulative Mean Normalized Difference Function
92+
fn cmndf_values(df: &[f64], max_lag: usize) -> Vec<f64> {
93+
let mut cmndf = vec![0.0; max_lag + 1];
94+
cmndf[0] = 1.0;
95+
let mut sum = 0.0;
96+
for lag in 1..=max_lag {
97+
sum += df[lag];
98+
cmndf[lag] = lag as f64 * df[lag] / if sum == 0.0 { 1e-10 } else { sum };
99+
}
100+
cmndf
101+
}
102+
103+
fn find_cmndf_argmin(cmndf: &[f64], min_lag: usize, max_lag: usize, threshold: f64) -> usize {
104+
let mut lag = min_lag;
105+
while lag <= max_lag {
106+
if cmndf[lag] < threshold {
107+
while lag < max_lag && cmndf[lag + 1] < cmndf[lag] {
108+
lag += 1;
109+
}
110+
return lag;
111+
}
112+
lag += 1;
113+
}
114+
0
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use super::*;
120+
121+
fn generate_sine_wave(frequency: f64, sample_rate: f64, duration_secs: f64) -> Vec<f64> {
122+
let total_samples = (sample_rate * duration_secs).round() as usize;
123+
let two_pi_f = 2.0 * std::f64::consts::PI * frequency;
124+
125+
(0..total_samples)
126+
.map(|n| {
127+
let t = n as f64 / sample_rate;
128+
(two_pi_f * t).sin()
129+
})
130+
.collect()
131+
}
132+
133+
fn diff_from_actual_frequency_smaller_than_threshold(
134+
result_frequency: f64,
135+
actual_frequency: f64,
136+
threshold: f64,
137+
) -> bool {
138+
let result_diff_from_actual_freq = (result_frequency - actual_frequency).abs();
139+
result_diff_from_actual_freq < threshold
140+
}
141+
142+
fn interpolation_better_than_raw_result(result: YinResult, frequency: f64) -> bool {
143+
let result_frequency = result.get_frequency();
144+
let refined_frequency = result.get_frequency_with_interpolation();
145+
let result_diff = (result_frequency - frequency).abs();
146+
let refined_diff = (refined_frequency - frequency).abs();
147+
refined_diff < result_diff
148+
}
149+
150+
#[test]
151+
fn test_simple_sine() {
152+
let sample_rate = 1000.0;
153+
let frequency = 12.0;
154+
let seconds = 10.0;
155+
let signal = generate_sine_wave(frequency, sample_rate, seconds);
156+
157+
let min_expected_frequency = 10.0;
158+
let max_expected_frequency = 100.0;
159+
160+
let yin = Yin::init(
161+
0.1,
162+
min_expected_frequency,
163+
max_expected_frequency,
164+
sample_rate,
165+
);
166+
let result = yin.yin(signal.as_slice());
167+
168+
assert!(diff_from_actual_frequency_smaller_than_threshold(
169+
result.get_frequency(),
170+
frequency,
171+
1.0
172+
));
173+
assert!(diff_from_actual_frequency_smaller_than_threshold(
174+
result.get_frequency_with_interpolation(),
175+
frequency,
176+
1.0,
177+
));
178+
179+
assert!(interpolation_better_than_raw_result(result, frequency));
180+
}
181+
182+
#[test]
183+
fn test_sine_frequency_range() {
184+
let sample_rate = 10000.0;
185+
for freq in 10..50 {
186+
let frequency = freq as f64;
187+
let seconds = 2.0;
188+
let signal = generate_sine_wave(frequency, sample_rate, seconds);
189+
190+
let min_expected_frequency = 5.0;
191+
let max_expected_frequency = 100.0;
192+
193+
let yin = Yin::init(
194+
0.1,
195+
min_expected_frequency,
196+
max_expected_frequency,
197+
sample_rate,
198+
);
199+
let result = yin.yin(signal.as_slice());
200+
201+
if (sample_rate as i32 % freq) == 0 {
202+
assert_eq!(result.get_frequency(), frequency);
203+
} else {
204+
assert!(diff_from_actual_frequency_smaller_than_threshold(
205+
result.get_frequency(),
206+
frequency,
207+
1.0
208+
));
209+
assert!(diff_from_actual_frequency_smaller_than_threshold(
210+
result.get_frequency_with_interpolation(),
211+
frequency,
212+
1.0,
213+
));
214+
215+
assert!(interpolation_better_than_raw_result(result, frequency));
216+
}
217+
}
218+
}
219+
220+
#[test]
221+
fn test_harmonic_sines() {
222+
let sample_rate = 44100.0;
223+
let seconds = 2.0;
224+
let frequency_1 = 50.0; // Minimal/Fundamental frequency - this is what YIN should find
225+
let signal_1 = generate_sine_wave(frequency_1, sample_rate, seconds);
226+
let frequency_2 = 150.0;
227+
let signal_2 = generate_sine_wave(frequency_2, sample_rate, seconds);
228+
let frequency_3 = 300.0;
229+
let signal_3 = generate_sine_wave(frequency_3, sample_rate, seconds);
230+
231+
let min_expected_frequency = 10.0;
232+
let max_expected_frequency = 500.0;
233+
234+
let yin = Yin::init(
235+
0.1,
236+
min_expected_frequency,
237+
max_expected_frequency,
238+
sample_rate,
239+
);
240+
241+
let total_samples = (sample_rate * seconds).round() as usize;
242+
let combined_signal: Vec<f64> = (0..total_samples)
243+
.map(|n| signal_1[n] + signal_2[n] + signal_3[n])
244+
.collect();
245+
246+
let result = yin.yin(&combined_signal);
247+
248+
assert!(diff_from_actual_frequency_smaller_than_threshold(
249+
result.get_frequency(),
250+
frequency_1,
251+
1.0
252+
));
253+
}
254+
255+
#[test]
256+
fn test_unharmonic_sines() {
257+
let sample_rate = 44100.0;
258+
let seconds = 2.0;
259+
let frequency_1 = 50.0;
260+
let signal_1 = generate_sine_wave(frequency_1, sample_rate, seconds);
261+
let frequency_2 = 66.0;
262+
let signal_2 = generate_sine_wave(frequency_2, sample_rate, seconds);
263+
let frequency_3 = 300.0;
264+
let signal_3 = generate_sine_wave(frequency_3, sample_rate, seconds);
265+
266+
let min_expected_frequency = 10.0;
267+
let max_expected_frequency = 500.0;
268+
269+
let yin = Yin::init(
270+
0.1,
271+
min_expected_frequency,
272+
max_expected_frequency,
273+
sample_rate,
274+
);
275+
276+
let total_samples = (sample_rate * seconds).round() as usize;
277+
let combined_signal: Vec<f64> = (0..total_samples)
278+
.map(|n| signal_1[n] + signal_2[n] + signal_3[n])
279+
.collect();
280+
281+
let result = yin.yin(&combined_signal);
282+
283+
let expected_frequency = (frequency_1 - frequency_2).abs();
284+
assert!(diff_from_actual_frequency_smaller_than_threshold(
285+
result.get_frequency(),
286+
expected_frequency,
287+
1.0
288+
));
289+
assert!(interpolation_better_than_raw_result(
290+
result,
291+
expected_frequency
292+
));
293+
}
294+
}

0 commit comments

Comments
 (0)