import type { ScopePopoutSessionState } from '../types/popout' import type { SpectrumAnalyzerDataSource } from '../renderer/visualizers/SpectrumAnalyzer' type SpectrumStereoChunk = { left: Float32Array; right: Float32Array } /** * `SpectrumAnalyzerDataSource` for the plugin webview. * * In Electron this source drains raw sample queues from the AudioRouter. In the * plugin the DSP runs in C++, so there are no raw samples to drain here — the * pending-sample getters return empty. This source's only job is to report the * session state (sample rate + whether the host is feeding us frames) so the * visualizer maps frequencies correctly and runs its render loop. * * Mirrors the seam used by ScopePopoutDataSource so the visualizer is unchanged. */ export class PluginWebViewDataSource implements SpectrumAnalyzerDataSource { private sessionState: ScopePopoutSessionState = { sessionId: 1, sampleRate: 48000, channelCount: 2, capturing: false, backendKind: null, } private readonly listeners = new Set<(state: ScopePopoutSessionState) => void>() // The DSP runs in C++ and pushes finished magnitudes (no raw samples flow // through here). But SpectrumAnalyzer only refreshes its heatmap buffer when // it sees "new samples arrived" (a non-empty pending queue). We therefore // hand it a reusable sentinel chunk each frame to signal a fresh frame. Its // length saturates `nativeBufferedSamples` so heatmap smoothing applies — the // values are unused (the shim's pushSamples is a no-op; magnitudes come from // fillMagnitudes). Sized to the max FFT so any fftSize saturates in one frame. private readonly sentinel = new Float32Array(16384) private readonly sentinelStereo: SpectrumStereoChunk = { left: this.sentinel, right: this.sentinel, } getPendingSpectrumSamples(): Float32Array[] { return this.sessionState.capturing ? [this.sentinel] : [] } getPendingSpectrumStereoSamples(): SpectrumStereoChunk[] { return this.sessionState.capturing ? [this.sentinelStereo] : [] } // Oscilloscope: same sentinel trick — the DSP runs in C++ and pushes finished // display windows; this just advances the visualizer's warmup/"new data" gate. getPendingOscilloscopeSamples(): Float32Array[] { return this.sessionState.capturing ? [this.sentinel] : [] } // Spectrogram: needs a sentinel so the visualizer calls the analyzer's process() // each frame (it only does so per pending chunk) to drain the C++ column queue. getPendingSpectrogramSamples(): Float32Array[] { return this.sessionState.capturing ? [this.sentinel] : [] } // Waveform: same sentinel trick — the visualizer calls processMono/processStereo // per pending chunk, which drains the C++-pushed column summaries from the bridge. getPendingWaveformSamples(): Float32Array[] { return this.sessionState.capturing ? [this.sentinel] : [] } getPendingWaveformStereoSamples(): SpectrumStereoChunk[] { return this.sessionState.capturing ? [this.sentinelStereo] : [] } // VU + loudness meters read the C++-pushed snapshot from the bridge analyzer // every frame, so there are no raw samples to drain here (no sentinel needed). getPendingVUMeterSamples(): Array<{ left: Float32Array; right: Float32Array }> { return [] } getPendingLUFSMeterSamples(): Array<{ left: Float32Array; right: Float32Array }> { return [] } // Vectorscope reads the C++-pushed point cloud via fillPoints/getMultibandPoints // each frame — no raw samples drain here, and no sentinel is needed. getPendingVectorscopeSamples(): Array<{ left: Float32Array; right: Float32Array }> { return [] } getSampleRate(): number { return this.sessionState.sampleRate } isPlaying(): boolean { return this.sessionState.capturing } subscribeToSessionChanges(listener: (state: ScopePopoutSessionState) => void): () => void { this.listeners.add(listener) listener(this.sessionState) return () => { this.listeners.delete(listener) } } /** Called by the bridge when a host frame arrives. */ setSampleRate(sampleRate: number): void { if (sampleRate > 0 && sampleRate !== this.sessionState.sampleRate) { this.updateSession({ sampleRate, sessionId: this.sessionState.sessionId + 1 }) } } setPlaying(playing: boolean): void { if (playing !== this.sessionState.capturing) { this.updateSession({ capturing: playing }) } } private updateSession(partial: Partial): void { this.sessionState = { ...this.sessionState, ...partial } for (const listener of this.listeners) { listener(this.sessionState) } } }