@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.
Files changed (3) hide show
  1. package/README.md +5 -0
  2. package/package.json +33 -0
  3. package/repair.js +80 -0
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @audio/denoise-repair
2
+
3
+ > Spectral repair — interpolate short time×frequency gaps and holes (De-Slop; iZotope RX class)
4
+
5
+ Planned — not implemented yet. See the umbrella README for status and sources.
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
+ }