@audio/pitch-yin 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/audio-module.js +31 -0
- package/package.json +43 -0
- package/yin.js +54 -0
package/audio-module.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// audio-module manifest — YIN as an analyzer: audio in, no audio out, pitch/clarity
|
|
2
|
+
// emitted per analysis frame via ctx.emit.
|
|
3
|
+
|
|
4
|
+
import yin from './yin.js'
|
|
5
|
+
|
|
6
|
+
const FRAME = 2048
|
|
7
|
+
|
|
8
|
+
export const pitch = (ctx) => {
|
|
9
|
+
const win = new Float32Array(FRAME)
|
|
10
|
+
let fill = 0
|
|
11
|
+
return (inputs, outputs, params) => {
|
|
12
|
+
const x = inputs[0] && inputs[0][0]
|
|
13
|
+
if (!x) return
|
|
14
|
+
for (let i = 0; i < x.length; i++) {
|
|
15
|
+
win[fill++] = x[i]
|
|
16
|
+
if (fill === FRAME) {
|
|
17
|
+
const r = yin(win, { fs: ctx.sampleRate, threshold: params.threshold[0] })
|
|
18
|
+
if (r) {
|
|
19
|
+
ctx.emit('pitch', r.freq)
|
|
20
|
+
ctx.emit('clarity', r.clarity)
|
|
21
|
+
}
|
|
22
|
+
fill = 0
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
pitch.channels = { inputs: 1, outputs: [] }
|
|
28
|
+
pitch.events = { out: { pitch: 'number', clarity: 'number' } }
|
|
29
|
+
pitch.params = {
|
|
30
|
+
threshold: { type: 'number', min: 0.05, max: 0.5, default: 0.1 },
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/pitch-yin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "YIN pitch detection (de Cheveign\u00e9 & Kawahara, 2002)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "yin.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./yin.js",
|
|
10
|
+
"./package.json": "./package.json",
|
|
11
|
+
"./audio-module": "./audio-module.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"yin.js",
|
|
15
|
+
"audio-module.js"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"audio",
|
|
19
|
+
"dsp",
|
|
20
|
+
"pitch",
|
|
21
|
+
"pitch-detection",
|
|
22
|
+
"f0",
|
|
23
|
+
"yin"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/audiojs/pitch.git",
|
|
30
|
+
"directory": "packages/pitch-yin"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/audiojs/pitch/tree/main/packages/pitch-yin",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/audiojs/pitch/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"audio-module": "./audio-module.js"
|
|
43
|
+
}
|
package/yin.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YIN pitch detection (de Cheveigné & Kawahara, 2002).
|
|
3
|
+
* Difference fn → cumulative mean normalized difference → threshold → parabolic interpolation.
|
|
4
|
+
*
|
|
5
|
+
* @param {Float32Array | Float64Array} data - audio samples (single window, eg. 2048 samples)
|
|
6
|
+
* @param {{fs?: number, threshold?: number}} params
|
|
7
|
+
* @returns {{freq: number, clarity: number} | null}
|
|
8
|
+
*/
|
|
9
|
+
export default function yin(data, params) {
|
|
10
|
+
let fs = params?.fs || 44100
|
|
11
|
+
let threshold = params?.threshold ?? 0.15
|
|
12
|
+
let len = data.length
|
|
13
|
+
let half = len >> 1
|
|
14
|
+
|
|
15
|
+
// step 1-2: difference function
|
|
16
|
+
let d = new Float64Array(half)
|
|
17
|
+
for (let tau = 1; tau < half; tau++) {
|
|
18
|
+
let sum = 0
|
|
19
|
+
for (let i = 0; i < half; i++) {
|
|
20
|
+
let diff = data[i] - data[i + tau]
|
|
21
|
+
sum += diff * diff
|
|
22
|
+
}
|
|
23
|
+
d[tau] = sum
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// step 3: cumulative mean normalized difference
|
|
27
|
+
let cmndf = new Float64Array(half)
|
|
28
|
+
cmndf[0] = 1
|
|
29
|
+
let running = 0
|
|
30
|
+
for (let tau = 1; tau < half; tau++) {
|
|
31
|
+
running += d[tau]
|
|
32
|
+
cmndf[tau] = running > 0 ? d[tau] * tau / running : 1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// step 4: absolute threshold — find first dip below threshold
|
|
36
|
+
let tau = 2
|
|
37
|
+
while (tau < half - 1) {
|
|
38
|
+
if (cmndf[tau] < threshold) {
|
|
39
|
+
// find local minimum
|
|
40
|
+
while (tau + 1 < half - 1 && cmndf[tau + 1] < cmndf[tau]) tau++
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
tau++
|
|
44
|
+
}
|
|
45
|
+
if (tau >= half - 1) return null
|
|
46
|
+
|
|
47
|
+
// step 5: parabolic interpolation
|
|
48
|
+
let s0 = cmndf[tau - 1], s1 = cmndf[tau], s2 = cmndf[tau + 1]
|
|
49
|
+
let denom = s0 - 2 * s1 + s2
|
|
50
|
+
let shift = denom !== 0 ? (s0 - s2) / (2 * denom) : 0
|
|
51
|
+
let period = tau + shift
|
|
52
|
+
|
|
53
|
+
return { freq: fs / period, clarity: 1 - s1 }
|
|
54
|
+
}
|