Files
prism/src/plugin-ui/PluginWebViewDataSource.ts
T
Boof2015 63ef5ffa0f Merge pull request #10 from Boof2015/VSTs
Add cross platform VSTs
2026-06-03 10:21:21 -04:00

123 lines
4.6 KiB
TypeScript

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<ScopePopoutSessionState>): void {
this.sessionState = { ...this.sessionState, ...partial }
for (const listener of this.listeners) {
listener(this.sessionState)
}
}
}