@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.
Files changed (3) hide show
  1. package/audio-module.js +31 -0
  2. package/package.json +43 -0
  3. package/yin.js +54 -0
@@ -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
+ }