@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.
Files changed (2) hide show
  1. package/mcleod.js +68 -0
  2. 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
+ }