pyin_rs/
pyin.rs

1use std::{collections::HashMap, ops::Range};
2
3use crate::{
4    core::YinCore,
5    hmm::{PitchCandidate, PitchHmm},
6    PitchDetector,
7};
8
9const PYIN_N_THRESHOLDS: usize = 100;
10const PYIN_MIN_THRESHOLD: f64 = 0.025;
11
12// Beta distribution with mean=0.1, alpha=1 and beta=18
13const BETA_DISTRIBUTION_0: [f64; 100] = [
14    0.000000, 0.029069, 0.048836, 0.061422, 0.068542, 0.071571, 0.071607, 0.069516, 0.065976,
15    0.061512, 0.056523, 0.051309, 0.046089, 0.041021, 0.036211, 0.031727, 0.027608, 0.023871,
16    0.020517, 0.017534, 0.014903, 0.012601, 0.010601, 0.008875, 0.007393, 0.006130, 0.005059,
17    0.004155, 0.003397, 0.002765, 0.002239, 0.001806, 0.001449, 0.001157, 0.000920, 0.000727,
18    0.000572, 0.000448, 0.000349, 0.000271, 0.000209, 0.000160, 0.000122, 0.000092, 0.000070,
19    0.000052, 0.000039, 0.000029, 0.000021, 0.000015, 0.000011, 0.000008, 0.000006, 0.000004,
20    0.000003, 0.000002, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000,
21    0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
22    0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
23    0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
24    0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
25    0.000000,
26];
27
28/** Probabilistic YIN pitch detection: https://www.eecs.qmul.ac.uk/~simond/pub/2014/MauchDixon-PYIN-ICASSP2014.pdf */
29pub struct Pyin {
30    core: YinCore,
31    hmm: PitchHmm,
32}
33
34impl Pyin {
35    pub fn new(input_size: usize, sample_rate: usize) -> Self {
36        let hmm = PitchHmm::new(0.5, None);
37        let core = YinCore::new(input_size, sample_rate);
38
39        Self { core, hmm }
40    }
41
42    fn probabilistic_threshold(&self, frequency_range: Option<Range<f64>>) -> Vec<PitchCandidate> {
43        let tau_range = self.core.calculate_tau_range(frequency_range);
44
45        // probability distribution of tau
46        let mut tau_prob_dist = HashMap::new();
47
48        for n in 0..PYIN_N_THRESHOLDS {
49            let threshold = (n + 1) as f64 * PYIN_MIN_THRESHOLD;
50            let tau = self.core.threshold(threshold, &tau_range);
51
52            if tau >= 0 {
53                *tau_prob_dist.entry(tau as usize).or_insert(0.0) += BETA_DISTRIBUTION_0[n];
54            }
55        }
56
57        let mut pitch_candidates = Vec::new();
58        for (tau, probability) in tau_prob_dist {
59            let f0 = self.core.sample_rate as f64 / self.core.parabolic_interpolation(tau);
60
61            if f0 != -0.0 {
62                pitch_candidates.push(PitchCandidate::new(f0, probability));
63            }
64        }
65
66        pitch_candidates
67    }
68}
69
70impl PitchDetector for Pyin {
71    fn pitch(&mut self, audio_buffer: &[f64], frequency_range: Option<Range<f64>>) -> f64 {
72        self.core.preprocess(audio_buffer);
73        let f0_estimates = self.probabilistic_threshold(frequency_range);
74
75        self.core.fft.clear();
76        self.hmm.inference(&f0_estimates)
77    }
78}