/ proof-of-concept / phase-perception-2
Sign in
proof-of-concept / phase-perception-2
phase-perception-2 / src / worker.ts
105 lines · 3.8 KBBlameHistoryRaw
1/**
2 * Web Worker: renders the phase-rotated sound B from the source A, and the
3 * curves and spectrograms shown in the app. One message in, one message out.
4 */
5import { applyFilter, bandCenters, groupDelay, sigmaF, spectrogram, type Spectrogram } from './bank'
6import { maxGroupDelaySec, phaseMap, wrapAngle } from './phasemap'
7import { peak, rms } from './sources'
8import type { Curve, RenderRequest, RenderResult, SpectrogramData } from './types'
10/** Sample an array defined on FFT bins onto `points` evenly spaced frequencies in [0, fmax]. */
11function sampleCurve(values: ArrayLike<number>, binHz: number, fmax: number, points: number): Curve {
12 const f = new Float32Array(points)
13 const y = new Float32Array(points)
14 for (let i = 0; i < points; i++) {
15 const fi = (fmax * i) / (points - 1)
16 const b = Math.min(values.length - 1, Math.round(fi / binHz))
17 f[i] = fi
18 y[i] = values[b]
19 }
20 return { f, y }
23function toDb(s: Spectrogram, ref: number): SpectrogramData {
24 const db = new Float32Array(s.data.length)
25 for (let i = 0; i < db.length; i++) db[i] = 20 * Math.log10(Math.max(s.data[i], 1e-12) / ref)
26 return { db, rows: s.rows, cols: s.cols, durationSec: s.durationSec, fmax: s.fmax }
29function render(req: RenderRequest): RenderResult {
30 const t0 = performance.now()
31 const { samples: x, fs, fmax } = req
32 const sigma = req.sigmaMs / 1000
33 const centers = bandCenters(fs, sigma)
34 const df = sigmaF(sigma)
35 const phi = phaseMap(req.map, centers, fs)
36 const padSec = 8 * sigma + maxGroupDelaySec(phi, df) + 0.02
37 const { y, g } = applyFilter(x, fs, sigma, phi, centers, padSec)
39 // shared gain: target RMS from A, but never let either sound clip
40 const pk = Math.max(peak(x), peak(y))
41 const r = rms(x)
42 const gain = pk > 0 && r > 0 ? Math.min(0.9 / pk, 0.2 / r) : 0
44 let d = 0
45 let e = 0
46 for (let i = 0; i < x.length; i++) {
47 d += (y[i] - x[i]) ** 2
48 e += x[i] * x[i]
49 }
50 const relDiff = e > 0 ? Math.sqrt(d / e) : 0
52 const nb = g.re.length
53 const magDb = new Float64Array(nb)
54 for (let i = 0; i < nb; i++) magDb[i] = 20 * Math.log10(Math.max(Math.hypot(g.re[i], g.im[i]), 1e-9))
55 const tau = groupDelay(g)
56 const tauMs = new Float64Array(nb)
57 for (let i = 0; i < nb; i++) tauMs[i] = tau[i] * 1000
58 const gdCurve = sampleCurve(tauMs, g.binHz, fmax, req.curvePoints)
59 let maxGd = 0
60 let maxMagDev = 0
61 const lastBin = Math.min(nb - 1, Math.round(fmax / g.binHz))
62 for (let i = 0; i <= lastBin; i++) {
63 maxGd = Math.max(maxGd, Math.abs(tauMs[i]))
64 maxMagDev = Math.max(maxMagDev, Math.abs(magDb[i]))
65 }
67 // the map itself, wrapped, at the band centers up to fmax (subsampled if there are many bands)
68 const kMax = Math.min(centers.length - 1, Math.floor(fmax / df))
69 const step = Math.max(1, Math.ceil((kMax + 1) / req.curvePoints))
70 const mapF: number[] = []
71 const mapY: number[] = []
72 for (let k = 0; k <= kMax; k += step) {
73 mapF.push(centers[k])
74 mapY.push(wrapAngle(phi[k]))
75 }
77 const sA = spectrogram(x, fs, sigma, fmax, req.maxCols, req.maxRows)
78 const sB = spectrogram(y, fs, sigma, fmax, req.maxCols, req.maxRows)
79 let ref = 1e-12
80 for (let i = 0; i < sA.data.length; i++) if (sA.data[i] > ref) ref = sA.data[i]
81 for (let i = 0; i < sB.data.length; i++) if (sB.data[i] > ref) ref = sB.data[i]
83 return {
84 id: req.id,
85 y,
86 gain,
87 relDiff,
88 bands: centers.length,
89 sigmaF: df,
90 map: { f: Float32Array.from(mapF), y: Float32Array.from(mapY) },
91 magDb: sampleCurve(magDb, g.binHz, fmax, req.curvePoints),
92 groupDelayMs: gdCurve,
93 maxGroupDelayMs: maxGd,
94 maxMagDeviationDb: maxMagDev,
95 specA: toDb(sA, ref),
96 specB: toDb(sB, ref),
97 elapsedMs: performance.now() - t0,
98 }
101self.onmessage = (e: MessageEvent<RenderRequest>) => {
102 const res = render(e.data)
103 const transfer = [res.y.buffer, res.specA.db.buffer, res.specB.db.buffer]
104 ;(self as unknown as Worker).postMessage(res, transfer)
↑↓ move↵openescclose