@audio/shift-lpc 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/index.js +106 -0
- package/package.json +42 -0
package/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { bufferedStream, hannWindow, makePitchShift, matchGain, resolveRatio } from '@audio/shift-core'
|
|
2
|
+
import delay from '@audio/shift-delay'
|
|
3
|
+
|
|
4
|
+
// Canonical LPC source-filter pitch shift (residual-excited linear prediction, RELP
|
|
5
|
+
// lineage): per frame, estimate the vocal-tract all-pole filter A(z) by the
|
|
6
|
+
// autocorrelation method (Levinson-Durbin), inverse-filter the signal to its
|
|
7
|
+
// spectrally-flat excitation residual, repitch the residual with the delay-line
|
|
8
|
+
// splicer (@audio/shift-delay — duration-preserving, and its correlation splicing
|
|
9
|
+
// lands on the residual's pitch pulses), and resynthesize through the UNMODIFIED
|
|
10
|
+
// 1/A(z) run continuously with block-switched coefficients — the formant envelope
|
|
11
|
+
// never moves, by construction. The classical speech-processing complement to
|
|
12
|
+
// cepstral envelope preservation (@audio/shift-formant): exact on voiced monophonic
|
|
13
|
+
// material, graceful-but-averaged on polyphony. Degenerate on narrowband/pure tones —
|
|
14
|
+
// there the AR envelope IS the partial, so the filter re-imposes the original pitch;
|
|
15
|
+
// that is the family's defining tradeoff, not a bug. `order` defaults to the classical
|
|
16
|
+
// 2 + sr/1000, capped so pole pairs model the envelope, not individual harmonics.
|
|
17
|
+
|
|
18
|
+
function levinson(r, p, a, tmp) {
|
|
19
|
+
a.fill(0)
|
|
20
|
+
a[0] = 1
|
|
21
|
+
let err = r[0]
|
|
22
|
+
for (let i = 1; i <= p; i++) {
|
|
23
|
+
let acc = r[i]
|
|
24
|
+
for (let j = 1; j < i; j++) acc += a[j] * r[i - j]
|
|
25
|
+
let k = -acc / err
|
|
26
|
+
for (let j = 0; j <= i; j++) tmp[j] = a[j] + k * a[i - j]
|
|
27
|
+
for (let j = 0; j <= i; j++) a[j] = tmp[j]
|
|
28
|
+
err *= 1 - k * k
|
|
29
|
+
if (err < 1e-12) { err = 1e-12; break }
|
|
30
|
+
}
|
|
31
|
+
return err
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function lpcBatch(data, opts) {
|
|
35
|
+
let { ratio, ratioFn } = resolveRatio(opts)
|
|
36
|
+
let n = data.length
|
|
37
|
+
let N = opts?.frameSize ?? 1024
|
|
38
|
+
let hop = N >> 2
|
|
39
|
+
let sr = opts?.sampleRate || 44100
|
|
40
|
+
let p = opts?.order ?? Math.min(N >> 4, Math.round(2 + sr / 1000))
|
|
41
|
+
let out = new Float32Array(n)
|
|
42
|
+
if (!n) return out
|
|
43
|
+
|
|
44
|
+
let win = hannWindow(N)
|
|
45
|
+
let frames = Math.ceil(n / hop)
|
|
46
|
+
let coefs = new Float64Array(frames * (p + 1))
|
|
47
|
+
let xw = new Float64Array(N)
|
|
48
|
+
let r = new Float64Array(p + 1)
|
|
49
|
+
let a = new Float64Array(p + 1)
|
|
50
|
+
let tmp = new Float64Array(p + 1)
|
|
51
|
+
|
|
52
|
+
for (let f = 0; f < frames; f++) {
|
|
53
|
+
// Clamp the fit window inside the signal: a zero-padded tail window fits the fade,
|
|
54
|
+
// whitens poorly, and the hot residual tail then rings the synthesis filter.
|
|
55
|
+
let start = Math.max(0, Math.min(f * hop, n - N))
|
|
56
|
+
for (let u = 0; u < N; u++) {
|
|
57
|
+
let t = start + u
|
|
58
|
+
xw[u] = t < n ? data[t] * win[u] : 0
|
|
59
|
+
}
|
|
60
|
+
for (let k = 0; k <= p; k++) {
|
|
61
|
+
let s = 0
|
|
62
|
+
for (let u = k; u < N; u++) s += xw[u] * xw[u - k]
|
|
63
|
+
r[k] = s
|
|
64
|
+
}
|
|
65
|
+
r[0] *= 1.000001
|
|
66
|
+
if (r[0] < 1e-12) { coefs[f * (p + 1)] = 1; continue }
|
|
67
|
+
levinson(r, p, a, tmp)
|
|
68
|
+
coefs.set(a, f * (p + 1))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Inverse-filter with block-switched coefficients: e = A(z)·x, state continuous.
|
|
72
|
+
let res = new Float32Array(n)
|
|
73
|
+
for (let t = 0; t < n; t++) {
|
|
74
|
+
let base = Math.min(frames - 1, (t / hop) | 0) * (p + 1)
|
|
75
|
+
let e = data[t]
|
|
76
|
+
for (let j = 1; j <= p; j++) {
|
|
77
|
+
let tj = t - j
|
|
78
|
+
if (tj >= 0) e += coefs[base + j] * data[tj]
|
|
79
|
+
}
|
|
80
|
+
res[t] = e
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Repitch the flat residual, then run 1/A(z) continuously over it — no grains, no
|
|
84
|
+
// synthesis transients; each sample uses its own frame's coefficients.
|
|
85
|
+
let shifted = delay(res, { ...opts, ratio: ratioFn || ratio })
|
|
86
|
+
for (let t = 0; t < n; t++) {
|
|
87
|
+
let base = Math.min(frames - 1, (t / hop) | 0) * (p + 1)
|
|
88
|
+
let y = shifted[t]
|
|
89
|
+
for (let j = 1; j <= p; j++) {
|
|
90
|
+
let tj = t - j
|
|
91
|
+
if (tj >= 0) y -= coefs[base + j] * out[tj]
|
|
92
|
+
}
|
|
93
|
+
out[t] = y
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Harmonics moving across the AR response change level with position; a single
|
|
97
|
+
// whole-signal RMS match restores loudness without touching the envelope shape
|
|
98
|
+
// (time-varying gain would ring AM sidebands through every harmonic). The stream
|
|
99
|
+
// form is bufferedStream — whole-signal at flush — so batch and stream stay
|
|
100
|
+
// identical even with a global correction.
|
|
101
|
+
return matchGain(out, data)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let lpcStream = (opts) => bufferedStream(lpcBatch, opts)
|
|
105
|
+
|
|
106
|
+
export default makePitchShift(lpcBatch, lpcStream)
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/shift-lpc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "LPC source-filter pitch shift — repitched excitation residual through the unmodified all-pole vocal-tract filter",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@audio/shift-core": "^1.0.0",
|
|
17
|
+
"@audio/shift-delay": "^1.0.0"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "audiojs",
|
|
24
|
+
"homepage": "https://github.com/audiojs/shift/tree/main/packages/shift-lpc#readme",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/audiojs/shift.git",
|
|
28
|
+
"directory": "packages/shift-lpc"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"audio",
|
|
32
|
+
"pitch-shift",
|
|
33
|
+
"lpc",
|
|
34
|
+
"linear-prediction",
|
|
35
|
+
"formant",
|
|
36
|
+
"source-filter",
|
|
37
|
+
"dsp"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
}
|
|
42
|
+
}
|