@audio/pitch-cepstrum 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/cepstrum.js +64 -0
- package/package.json +43 -0
package/cepstrum.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { fft, ifft } from 'fourier-transform'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cepstrum pitch detection (Noll, 1967).
|
|
5
|
+
* Real cepstrum c(τ) = IFFT(log |FFT(x)|). A peak at quefrency τ
|
|
6
|
+
* corresponds to period τ in the time domain.
|
|
7
|
+
*
|
|
8
|
+
* @param {Float32Array | Float64Array} data - window, length must be a power of 2
|
|
9
|
+
* @param {{fs?: number, minFreq?: number, maxFreq?: number, threshold?: number}} [params]
|
|
10
|
+
* @returns {{freq: number, clarity: number} | null}
|
|
11
|
+
*/
|
|
12
|
+
export default function cepstrum(data, params) {
|
|
13
|
+
let fs = params?.fs || 44100
|
|
14
|
+
let minFreq = params?.minFreq ?? 50
|
|
15
|
+
let maxFreq = params?.maxFreq ?? 2000
|
|
16
|
+
let threshold = params?.threshold ?? 0.3
|
|
17
|
+
let N = data.length
|
|
18
|
+
if ((N & (N - 1)) !== 0) throw new Error('cepstrum: window length must be a power of 2')
|
|
19
|
+
|
|
20
|
+
// FFT → half-complex; take log magnitude; IFFT → real cepstrum
|
|
21
|
+
let [re, im] = fft(data)
|
|
22
|
+
let half = re.length // N/2 + 1
|
|
23
|
+
let logMag = new Float64Array(half)
|
|
24
|
+
let eps = 1e-12
|
|
25
|
+
for (let i = 0; i < half; i++) {
|
|
26
|
+
logMag[i] = Math.log(Math.hypot(re[i], im[i]) + eps)
|
|
27
|
+
}
|
|
28
|
+
// real spectrum → pass as re with zero imaginary
|
|
29
|
+
let zeros = new Float64Array(half)
|
|
30
|
+
let cep = ifft(logMag, zeros)
|
|
31
|
+
|
|
32
|
+
let tauMin = Math.max(2, Math.floor(fs / maxFreq))
|
|
33
|
+
let tauMax = Math.min((N >> 1) - 1, Math.ceil(fs / minFreq))
|
|
34
|
+
if (tauMax <= tauMin + 1) return null
|
|
35
|
+
|
|
36
|
+
// find the largest peak within valid quefrency range
|
|
37
|
+
let peak = tauMin, peakVal = -Infinity
|
|
38
|
+
for (let tau = tauMin; tau <= tauMax; tau++) {
|
|
39
|
+
if (cep[tau] > peakVal) { peakVal = cep[tau]; peak = tau }
|
|
40
|
+
}
|
|
41
|
+
if (peak <= tauMin || peak >= tauMax) return null
|
|
42
|
+
|
|
43
|
+
// parabolic interpolation
|
|
44
|
+
let s0 = cep[peak - 1], s1 = cep[peak], s2 = cep[peak + 1]
|
|
45
|
+
let denom = s0 - 2 * s1 + s2
|
|
46
|
+
let shift = denom !== 0 ? (s0 - s2) / (2 * denom) : 0
|
|
47
|
+
let period = peak + shift
|
|
48
|
+
|
|
49
|
+
// clarity: peak-to-second-local-max ratio. For periodic signals the
|
|
50
|
+
// quefrency peak is much taller than any competing local maximum; for
|
|
51
|
+
// noise, several local maxima have comparable height.
|
|
52
|
+
let second = -Infinity
|
|
53
|
+
for (let tau = tauMin + 1; tau < tauMax; tau++) {
|
|
54
|
+
if (Math.abs(tau - peak) < 3) continue
|
|
55
|
+
if (cep[tau] > cep[tau - 1] && cep[tau] > cep[tau + 1] && cep[tau] > second) {
|
|
56
|
+
second = cep[tau]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
let clarity = peakVal > 0 && second > 0 ? 1 - second / peakVal : (peakVal > 0 ? 1 : 0)
|
|
60
|
+
clarity = Math.max(0, Math.min(1, clarity))
|
|
61
|
+
|
|
62
|
+
if (clarity < threshold) return null
|
|
63
|
+
return { freq: fs / period, clarity }
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/pitch-cepstrum",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cepstrum pitch detection (Noll, 1967)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "cepstrum.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./cepstrum.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"cepstrum.js"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"fourier-transform": "^2.2.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"audio",
|
|
20
|
+
"dsp",
|
|
21
|
+
"pitch",
|
|
22
|
+
"pitch-detection",
|
|
23
|
+
"f0",
|
|
24
|
+
"cepstrum"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/audiojs/pitch.git",
|
|
31
|
+
"directory": "packages/pitch-cepstrum"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/audiojs/pitch/tree/main/packages/pitch-cepstrum",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/audiojs/pitch/issues"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|