@audio/pitch-mcleod 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcleod.js +68 -0
- package/package.json +40 -0
package/mcleod.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McLeod Pitch Method (McLeod & Wyvill, 2005).
|
|
3
|
+
* Normalized square difference function → peak picking → parabolic interpolation.
|
|
4
|
+
*
|
|
5
|
+
* @param {Float32Array | Float64Array} data - audio samples (single window)
|
|
6
|
+
* @param {{fs?: number, threshold?: number}} params
|
|
7
|
+
* @returns {{freq: number, clarity: number} | null}
|
|
8
|
+
*/
|
|
9
|
+
export default function mcleod(data, params) {
|
|
10
|
+
let fs = params?.fs || 44100
|
|
11
|
+
let threshold = params?.threshold ?? 0.9
|
|
12
|
+
let len = data.length
|
|
13
|
+
let half = len >> 1
|
|
14
|
+
|
|
15
|
+
// normalized square difference function (NSDF)
|
|
16
|
+
// e1 = Σ x[i]² (constant); e2(τ) = Σ x[i+τ]² computed incrementally
|
|
17
|
+
let nsdf = new Float64Array(half)
|
|
18
|
+
let e1 = 0
|
|
19
|
+
for (let i = 0; i < half; i++) e1 += data[i] * data[i]
|
|
20
|
+
let e2 = e1
|
|
21
|
+
for (let tau = 0; tau < half; tau++) {
|
|
22
|
+
let acf = 0
|
|
23
|
+
for (let i = 0; i < half; i++) acf += data[i] * data[i + tau]
|
|
24
|
+
nsdf[tau] = e1 + e2 > 0 ? 2 * acf / (e1 + e2) : 0
|
|
25
|
+
// slide window: drop x[τ], add x[τ+half]
|
|
26
|
+
let drop = data[tau], add = data[tau + half] || 0
|
|
27
|
+
e2 += add * add - drop * drop
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// find peaks: skip initial positive region, then collect local maxima after each zero crossing
|
|
31
|
+
let peaks = []
|
|
32
|
+
let wasNeg = false
|
|
33
|
+
|
|
34
|
+
for (let tau = 1; tau < half; tau++) {
|
|
35
|
+
if (nsdf[tau] < 0) wasNeg = true
|
|
36
|
+
if (!wasNeg || nsdf[tau] <= 0) continue
|
|
37
|
+
|
|
38
|
+
// entered a positive region after a negative — find local max
|
|
39
|
+
let maxVal = nsdf[tau], maxTau = tau
|
|
40
|
+
while (tau + 1 < half && nsdf[tau + 1] > 0) {
|
|
41
|
+
tau++
|
|
42
|
+
if (nsdf[tau] > maxVal) { maxVal = nsdf[tau]; maxTau = tau }
|
|
43
|
+
}
|
|
44
|
+
peaks.push({ tau: maxTau, val: maxVal })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!peaks.length) return null
|
|
48
|
+
|
|
49
|
+
// pick highest peak above threshold * max
|
|
50
|
+
let best = peaks[0]
|
|
51
|
+
let maxPeak = Math.max(...peaks.map(p => p.val))
|
|
52
|
+
let cutoff = threshold * maxPeak
|
|
53
|
+
|
|
54
|
+
for (let p of peaks) {
|
|
55
|
+
if (p.val >= cutoff) { best = p; break }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// parabolic interpolation
|
|
59
|
+
let tau = best.tau
|
|
60
|
+
if (tau < 1 || tau >= half - 1) return { freq: fs / tau, clarity: best.val }
|
|
61
|
+
|
|
62
|
+
let s0 = nsdf[tau - 1], s1 = nsdf[tau], s2 = nsdf[tau + 1]
|
|
63
|
+
let denom = s0 - 2 * s1 + s2
|
|
64
|
+
let shift = denom !== 0 ? (s0 - s2) / (2 * denom) : 0
|
|
65
|
+
let period = tau + shift
|
|
66
|
+
|
|
67
|
+
return { freq: fs / period, clarity: best.val }
|
|
68
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/pitch-mcleod",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "McLeod Pitch Method (McLeod & Wyvill, 2005)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "mcleod.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./mcleod.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"mcleod.js"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"audio",
|
|
17
|
+
"dsp",
|
|
18
|
+
"pitch",
|
|
19
|
+
"pitch-detection",
|
|
20
|
+
"f0",
|
|
21
|
+
"mcleod"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/audiojs/pitch.git",
|
|
28
|
+
"directory": "packages/pitch-mcleod"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/audiojs/pitch/tree/main/packages/pitch-mcleod",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/audiojs/pitch/issues"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|