1import Foundation
2
3/// Monophonic fundamental-frequency estimator using the YIN algorithm
4/// (de Cheveigné & Kawahara, 2002) with parabolic interpolation.
5final class YINDetector {
6 var sampleRate: Double
7 let windowSize: Int
8 private let threshold: Float
9 private let rmsGate: Float
10 private let minClarity: Double
11
12 // Scratch buffers reused across calls to avoid per-frame allocation.
13 private var diff: [Float]
14 private var cmnd: [Float]
15
16 init(sampleRate: Double, windowSize: Int = 2048, threshold: Float = 0.15,
17 rmsGate: Float = 0.006, minClarity: Double = 0.5) {
18 self.sampleRate = sampleRate
19 self.windowSize = windowSize
20 self.threshold = threshold
21 self.rmsGate = rmsGate
22 self.minClarity = minClarity
23 self.diff = [Float](repeating: 0, count: windowSize / 2)
24 self.cmnd = [Float](repeating: 1, count: windowSize / 2)
25 }
26
27 struct Result {
28 let frequency: Double
29 let clarity: Double // 0…1, higher == more periodic
30 let level: Double // RMS, 0…1
31 }
32
33 /// Returns a pitch estimate for `samples` (must be `windowSize` long) or nil
34 /// if the signal is too quiet or not periodic enough.
35 func detect(_ samples: UnsafePointer<Float>, count: Int) -> Result? {
36 guard count >= windowSize else { return nil }
37 let half = windowSize / 2
38
39 // --- RMS gate: ignore near-silence ---
40 var sumSq: Float = 0
41 for i in 0..<windowSize { let v = samples[i]; sumSq += v * v }
42 let rms = (sumSq / Float(windowSize)).squareRoot()
43 if rms < rmsGate { return nil }
44 let level = Double(min(1, rms * 6))
45
46 // --- Difference function ---
47 diff[0] = 0
48 for tau in 1..<half {
49 var sum: Float = 0
50 var j = 0
51 while j < half {
52 let d = samples[j] - samples[j + tau]
53 sum += d * d
54 j += 1
55 }
56 diff[tau] = sum
57 }
58
59 // --- Cumulative mean normalized difference ---
60 cmnd[0] = 1
61 var running: Float = 0
62 for tau in 1..<half {
63 running += diff[tau]
64 cmnd[tau] = running > 0 ? diff[tau] * Float(tau) / running : 1
65 }
66
67 // --- Absolute threshold ---
68 var tauEst = -1
69 var tau = 2
70 while tau < half - 1 {
71 if cmnd[tau] < threshold {
72 while tau + 1 < half && cmnd[tau + 1] < cmnd[tau] { tau += 1 }
73 tauEst = tau
74 break
75 }
76 tau += 1
77 }
78 guard tauEst > 0 else { return nil }
79
80 // --- Parabolic interpolation around the dip ---
81 let betterTau = parabolicRefine(tauEst)
82 guard betterTau > 0 else { return nil }
83 let freq = sampleRate / betterTau
84 guard freq >= 40, freq <= 2000 else { return nil }
85
86 let clarity = Double(max(0, min(1, 1 - cmnd[tauEst])))
87 guard clarity >= minClarity else { return nil }
88 return Result(frequency: freq, clarity: clarity, level: level)
89 }
90
91 private func parabolicRefine(_ tau: Int) -> Double {
92 let half = windowSize / 2
93 let x0 = tau > 1 ? tau - 1 : tau
94 let x2 = tau + 1 < half ? tau + 1 : tau
95 if x0 == tau { return Double(cmnd[tau] <= cmnd[x2] ? tau : x2) }
96 if x2 == tau { return Double(cmnd[tau] <= cmnd[x0] ? tau : x0) }
97 let s0 = cmnd[x0], s1 = cmnd[tau], s2 = cmnd[x2]
98 let denom = 2 * (2 * s1 - s2 - s0)
99 if denom == 0 { return Double(tau) }
100 return Double(tau) + Double(s2 - s0) / Double(denom)
101 }
102}