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