mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-16 08:10:40 +02:00
63ef5ffa0f
Add cross platform VSTs
123 lines
4.6 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
}
|