/** * Web Worker: renders the phase-rotated sound B from the source A, and the * curves and spectrograms shown in the app. One message in, one message out. */ import { applyFilter, bandCenters, groupDelay, sigmaF, spectrogram, type Spectrogram } from './bank' import { maxGroupDelaySec, phaseMap, wrapAngle } from './phasemap' import { peak, rms } from './sources' import type { Curve, RenderRequest, RenderResult, SpectrogramData } from './types' /** Sample an array defined on FFT bins onto `points` evenly spaced frequencies in [0, fmax]. */ function sampleCurve(values: ArrayLike, binHz: number, fmax: number, points: number): Curve { const f = new Float32Array(points) const y = new Float32Array(points) for (let i = 0; i < points; i++) { const fi = (fmax * i) / (points - 1) const b = Math.min(values.length - 1, Math.round(fi / binHz)) f[i] = fi y[i] = values[b] } return { f, y } } function toDb(s: Spectrogram, ref: number): SpectrogramData { const db = new Float32Array(s.data.length) for (let i = 0; i < db.length; i++) db[i] = 20 * Math.log10(Math.max(s.data[i], 1e-12) / ref) return { db, rows: s.rows, cols: s.cols, durationSec: s.durationSec, fmax: s.fmax } } function render(req: RenderRequest): RenderResult { const t0 = performance.now() const { samples: x, fs, fmax } = req const sigma = req.sigmaMs / 1000 const centers = bandCenters(fs, sigma) const df = sigmaF(sigma) const phi = phaseMap(req.map, centers, fs) const padSec = 8 * sigma + maxGroupDelaySec(phi, df) + 0.02 const { y, g } = applyFilter(x, fs, sigma, phi, centers, padSec) // shared gain: target RMS from A, but never let either sound clip const pk = Math.max(peak(x), peak(y)) const r = rms(x) const gain = pk > 0 && r > 0 ? Math.min(0.9 / pk, 0.2 / r) : 0 let d = 0 let e = 0 for (let i = 0; i < x.length; i++) { d += (y[i] - x[i]) ** 2 e += x[i] * x[i] } const relDiff = e > 0 ? Math.sqrt(d / e) : 0 const nb = g.re.length const magDb = new Float64Array(nb) for (let i = 0; i < nb; i++) magDb[i] = 20 * Math.log10(Math.max(Math.hypot(g.re[i], g.im[i]), 1e-9)) const tau = groupDelay(g) const tauMs = new Float64Array(nb) for (let i = 0; i < nb; i++) tauMs[i] = tau[i] * 1000 const gdCurve = sampleCurve(tauMs, g.binHz, fmax, req.curvePoints) let maxGd = 0 let maxMagDev = 0 const lastBin = Math.min(nb - 1, Math.round(fmax / g.binHz)) for (let i = 0; i <= lastBin; i++) { maxGd = Math.max(maxGd, Math.abs(tauMs[i])) maxMagDev = Math.max(maxMagDev, Math.abs(magDb[i])) } // the map itself, wrapped, at the band centers up to fmax (subsampled if there are many bands) const kMax = Math.min(centers.length - 1, Math.floor(fmax / df)) const step = Math.max(1, Math.ceil((kMax + 1) / req.curvePoints)) const mapF: number[] = [] const mapY: number[] = [] for (let k = 0; k <= kMax; k += step) { mapF.push(centers[k]) mapY.push(wrapAngle(phi[k])) } const sA = spectrogram(x, fs, sigma, fmax, req.maxCols, req.maxRows) const sB = spectrogram(y, fs, sigma, fmax, req.maxCols, req.maxRows) let ref = 1e-12 for (let i = 0; i < sA.data.length; i++) if (sA.data[i] > ref) ref = sA.data[i] for (let i = 0; i < sB.data.length; i++) if (sB.data[i] > ref) ref = sB.data[i] return { id: req.id, y, gain, relDiff, bands: centers.length, sigmaF: df, map: { f: Float32Array.from(mapF), y: Float32Array.from(mapY) }, magDb: sampleCurve(magDb, g.binHz, fmax, req.curvePoints), groupDelayMs: gdCurve, maxGroupDelayMs: maxGd, maxMagDeviationDb: maxMagDev, specA: toDb(sA, ref), specB: toDb(sB, ref), elapsedMs: performance.now() - t0, } } self.onmessage = (e: MessageEvent) => { const res = render(e.data) const transfer = [res.y.buffer, res.specA.db.buffer, res.specB.db.buffer] ;(self as unknown as Worker).postMessage(res, transfer) }