import type { SpectrumNativeAnalyzer } from '../renderer/audio/native' const FFT_SILENCE_DB = -100 /** * A drop-in `SpectrumNativeAnalyzer` for the plugin webview. * * In the Electron app, `SpectrumAnalyzer` pushes raw samples into the N-API DSP * addon and reads magnitudes back. There is no N-API addon inside a webview, so * here the DSP runs in the C++ plugin instead: it computes magnitudes off the * realtime thread and pushes them over the JUCE bridge. This shim simply caches * the latest pushed magnitudes and serves them through the same interface * `SpectrumAnalyzer` already consumes — so the visualizer needs no changes. * * `pushSamples` / `pushStereoSamples` are intentional no-ops: audio never flows * through the webview. */ export class BridgeSpectrumAnalyzer implements SpectrumNativeAnalyzer { private fftSize = 2048 private sampleRate = 48000 private magnitudes: Float32Array private sideMagnitudes: Float32Array constructor(fftSize = 2048) { this.fftSize = fftSize this.magnitudes = new Float32Array(fftSize / 2).fill(FFT_SILENCE_DB) this.sideMagnitudes = new Float32Array(fftSize / 2).fill(FFT_SILENCE_DB) } /** Called by the bridge whenever the host emits a new frame. */ setMagnitudes(magnitudes: Float32Array, side?: Float32Array): void { if (magnitudes.length !== this.magnitudes.length) { this.magnitudes = new Float32Array(magnitudes.length) } this.magnitudes.set(magnitudes) if (side && side.length > 0) { if (side.length !== this.sideMagnitudes.length) { this.sideMagnitudes = new Float32Array(side.length) } this.sideMagnitudes.set(side) } } isAvailable(): boolean { return true } setFFTSize(size: number): void { if (size > 0 && size !== this.fftSize) { this.fftSize = size this.magnitudes = new Float32Array(size / 2).fill(FFT_SILENCE_DB) this.sideMagnitudes = new Float32Array(size / 2).fill(FFT_SILENCE_DB) } } getFFTSize(): number { return this.fftSize } setSampleRate(sampleRate: number): void { this.sampleRate = sampleRate } // Smoothing is applied in the C++ DSP; nothing to do on this side. setSmoothing(_smoothing: number): void {} pushSamples(_audioData: Float32Array): void {} pushStereoSamples(_leftChannel: Float32Array, _rightChannel: Float32Array): void {} fillMagnitudes(output: Float32Array): number { const count = Math.min(output.length, this.magnitudes.length) if (count > 0) { output.set(this.magnitudes.subarray(0, count), 0) } return count } // The C++ side sends already-smoothed magnitudes; serve them for the raw // request too (the heatmap path re-smooths from this in the visualizer). fillRawMagnitudes(output: Float32Array): number { return this.fillMagnitudes(output) } fillSideMagnitudes(output: Float32Array): number { const count = Math.min(output.length, this.sideMagnitudes.length) if (count > 0) { output.set(this.sideMagnitudes.subarray(0, count), 0) } return count } getMagnitudes(): Float32Array { return this.magnitudes } getRawMagnitudes(): Float32Array { return this.magnitudes } getSideMagnitudes(): Float32Array { return this.sideMagnitudes } process(_audioData: Float32Array): Float32Array { return this.magnitudes } binToFrequency(bin: number): number { return (bin * this.sampleRate) / this.fftSize } reset(): void { this.magnitudes.fill(FFT_SILENCE_DB) this.sideMagnitudes.fill(FFT_SILENCE_DB) } }