@audio/denoise-repair 0.1.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/README.md +5 -0
- package/package.json +33 -0
- package/repair.js +80 -0
package/README.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/denoise-repair",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Spectral repair — interpolate short time×frequency gaps and holes (De-Slop; iZotope RX class)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"keywords": [
|
|
8
|
+
"audio",
|
|
9
|
+
"dsp",
|
|
10
|
+
"restoration",
|
|
11
|
+
"spectral-repair"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"main": "repair.js",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./repair.js",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"repair.js"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@audio/denoise-core": "^0.1.0",
|
|
28
|
+
"fourier-transform": "^2.3.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/repair.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Spectral repair (iZotope RX class) — replace time×frequency holes by interpolating
|
|
2
|
+
// the spectrogram across each region: log-magnitude interpolated between the last clean
|
|
3
|
+
// frame before and first clean frame after, phase advanced coherently from the leading
|
|
4
|
+
// context (phase-vocoder style). Full-band regions repair dropouts/gaps; band-limited
|
|
5
|
+
// regions repair chirps/beeps/holes without touching surrounding content.
|
|
6
|
+
|
|
7
|
+
import { fft } from 'fourier-transform'
|
|
8
|
+
import { stftBatch, hannWindow } from '@audio/denoise-core'
|
|
9
|
+
|
|
10
|
+
// analyze one frame's half-spectrum at pos (mag + phase copies)
|
|
11
|
+
function analyze (data, pos, win, half) {
|
|
12
|
+
let N = win.length
|
|
13
|
+
let f = new Float64Array(N)
|
|
14
|
+
for (let i = 0; i < N; i++) f[i] = (data[pos + i] || 0) * win[i]
|
|
15
|
+
let [re, im] = fft(f)
|
|
16
|
+
let mag = new Float64Array(half + 1), phase = new Float64Array(half + 1)
|
|
17
|
+
for (let k = 0; k <= half; k++) {
|
|
18
|
+
mag[k] = Math.sqrt(re[k] * re[k] + im[k] * im[k])
|
|
19
|
+
phase[k] = Math.atan2(im[k], re[k])
|
|
20
|
+
}
|
|
21
|
+
return { mag, phase }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Float32Array} data — mono PCM
|
|
26
|
+
* @param {object} opts — {
|
|
27
|
+
* regions: [{ at, duration, from = 0, to = fs/2 }] — seconds / Hz, required
|
|
28
|
+
* frameSize = 2048, hopSize = 512, fs = 44100
|
|
29
|
+
* }
|
|
30
|
+
* @returns {Float32Array} repaired copy
|
|
31
|
+
*/
|
|
32
|
+
export default function repair (data, opts = {}) {
|
|
33
|
+
let regions = opts.regions
|
|
34
|
+
if (!regions?.length) throw new RangeError('repair: opts.regions is required')
|
|
35
|
+
let N = opts.frameSize ?? 2048
|
|
36
|
+
let hop = opts.hopSize ?? (N >> 2)
|
|
37
|
+
let fs = opts.fs ?? 44100
|
|
38
|
+
let half = N >> 1
|
|
39
|
+
let win = hannWindow(N)
|
|
40
|
+
let binHz = fs / N
|
|
41
|
+
|
|
42
|
+
// per region: frame span touching the gap + clean context frames fully outside it
|
|
43
|
+
let regs = regions.map(r => {
|
|
44
|
+
let a = Math.round(r.at * fs), b = Math.round((r.at + r.duration) * fs)
|
|
45
|
+
let fPre = Math.max(0, Math.floor((a - N) / hop)) // last frame fully before
|
|
46
|
+
let fPost = Math.ceil(b / hop) // first frame fully after
|
|
47
|
+
let pre = analyze(data, fPre * hop, win, half)
|
|
48
|
+
let prePre = analyze(data, Math.max(0, fPre - 1) * hop, win, half)
|
|
49
|
+
let post = fPost * hop + N <= data.length ? analyze(data, fPost * hop, win, half) : pre
|
|
50
|
+
// measured per-hop phase advance (phase vocoder): handles off-bin tones exactly
|
|
51
|
+
let adv = new Float64Array(half + 1)
|
|
52
|
+
for (let k = 0; k <= half; k++) {
|
|
53
|
+
let expected = 2 * Math.PI * hop * k / N
|
|
54
|
+
let d = pre.phase[k] - prePre.phase[k] - expected
|
|
55
|
+
d -= 2 * Math.PI * Math.round(d / (2 * Math.PI))
|
|
56
|
+
adv[k] = expected + (fPre > 0 ? d : 0)
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
f0: fPre + 1, f1: fPost - 1, pre, post, adv,
|
|
60
|
+
b0: Math.max(0, Math.round((r.from ?? 0) / binHz)),
|
|
61
|
+
b1: Math.min(half, Math.round((r.to ?? fs / 2) / binHz)),
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
let idx = -1
|
|
66
|
+
return stftBatch(data, (mag, phase, state) => {
|
|
67
|
+
idx++
|
|
68
|
+
for (let r of regs) {
|
|
69
|
+
if (idx < r.f0 || idx > r.f1) continue
|
|
70
|
+
let t = (idx - r.f0 + 1) / (r.f1 - r.f0 + 2) // interp position in the hole
|
|
71
|
+
for (let k = r.b0; k <= r.b1; k++) {
|
|
72
|
+
// log-magnitude interpolation between clean context frames
|
|
73
|
+
mag[k] = Math.exp((1 - t) * Math.log(r.pre.mag[k] + 1e-12) + t * Math.log(r.post.mag[k] + 1e-12))
|
|
74
|
+
// coherent phase: pre-context phase advanced by the measured per-hop rotation
|
|
75
|
+
phase[k] = r.pre.phase[k] + (idx - r.f0 + 1) * r.adv[k]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { mag, phase }
|
|
79
|
+
}, { frameSize: N, hopSize: hop, fs })
|
|
80
|
+
}
|