mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-19 03:54:23 +02:00
@@ -0,0 +1,122 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user