mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 05:10:51 +02:00
spectrogram VST
This commit is contained in:
@@ -61,6 +61,7 @@ function(add_prism_scope TARGET PRODUCT PLUGIN_CODE SCOPE_DEFINE)
|
||||
${PRISM_NATIVE_DIR}/lufsmeter.cpp
|
||||
${PRISM_NATIVE_DIR}/vectorscope.cpp
|
||||
${PRISM_NATIVE_DIR}/multiband.cpp
|
||||
${PRISM_NATIVE_DIR}/spectrogram.cpp
|
||||
${PRISM_NATIVE_DIR}/dsp_utils.cpp)
|
||||
|
||||
target_include_directories(${TARGET} PRIVATE Source ${PRISM_NATIVE_DIR})
|
||||
@@ -101,3 +102,4 @@ add_prism_scope(PrismOscilloscope "Prism Oscilloscope" Posc "PRISM_SCOPE_OSCILLO
|
||||
add_prism_scope(PrismVUMeter "Prism VU Meter" Pvum "PRISM_SCOPE_VUMETER=1")
|
||||
add_prism_scope(PrismLUFSMeter "Prism Loudness Meter" Pluf "PRISM_SCOPE_LUFSMETER=1")
|
||||
add_prism_scope(PrismVectorscope "Prism Vectorscope" Pvct "PRISM_SCOPE_VECTORSCOPE=1")
|
||||
add_prism_scope(PrismSpectrogram "Prism Spectrogram" Pspg "PRISM_SCOPE_SPECTROGRAM=1")
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "VUMeterEngine.h"
|
||||
#include "LUFSMeterEngine.h"
|
||||
#include "VectorscopeEngine.h"
|
||||
#include "SpectrogramEngine.h"
|
||||
#include <cstring>
|
||||
|
||||
#if ! PRISM_USE_DEV_SERVER
|
||||
@@ -21,7 +22,9 @@ namespace
|
||||
|
||||
std::unique_ptr<ScopeEngine> makeEngine()
|
||||
{
|
||||
#if defined(PRISM_SCOPE_VECTORSCOPE) && PRISM_SCOPE_VECTORSCOPE
|
||||
#if defined(PRISM_SCOPE_SPECTROGRAM) && PRISM_SCOPE_SPECTROGRAM
|
||||
return std::make_unique<SpectrogramEngine>();
|
||||
#elif defined(PRISM_SCOPE_VECTORSCOPE) && PRISM_SCOPE_VECTORSCOPE
|
||||
return std::make_unique<VectorscopeEngine>();
|
||||
#elif defined(PRISM_SCOPE_LUFSMETER) && PRISM_SCOPE_LUFSMETER
|
||||
return std::make_unique<LUFSMeterEngine>();
|
||||
@@ -77,7 +80,8 @@ namespace
|
||||
.withNativeIntegrationEnabled()
|
||||
.withInitialisationData("prismScope", juce::String(scopeId))
|
||||
.withEventListener("prismConfig", [&editor](juce::var v) { editor.onPrismConfig(std::move(v)); })
|
||||
.withEventListener("prismReady", [&editor](juce::var) { editor.onPrismReady(); });
|
||||
.withEventListener("prismReady", [&editor](juce::var) { editor.onPrismReady(); })
|
||||
.withEventListener("prismSpectrogramConfig", [&editor](juce::var v) { editor.onScopeNativeConfig(std::move(v)); });
|
||||
#if ! PRISM_USE_DEV_SERVER
|
||||
options = options.withResourceProvider([](const auto& url) { return provideResource(url); });
|
||||
#endif
|
||||
@@ -134,6 +138,11 @@ void PrismSpectrumEditor::onPrismReady()
|
||||
sendAppDefaults();
|
||||
}
|
||||
|
||||
void PrismSpectrumEditor::onScopeNativeConfig(juce::var payload)
|
||||
{
|
||||
engine->configureNative(payload);
|
||||
}
|
||||
|
||||
void PrismSpectrumEditor::pushRestoreSettings()
|
||||
{
|
||||
auto* obj = new juce::DynamicObject();
|
||||
|
||||
@@ -27,6 +27,9 @@ public:
|
||||
void onPrismConfig(juce::var payload);
|
||||
void onPrismReady();
|
||||
|
||||
// Scope-specific native config (e.g. the spectrogram's canvas-derived rowCount).
|
||||
void onScopeNativeConfig(juce::var payload);
|
||||
|
||||
// Push the processor's saved settings to the UI (used on ready + on host
|
||||
// state restore, to cover either ordering).
|
||||
void pushRestoreSettings();
|
||||
|
||||
@@ -24,6 +24,13 @@ public:
|
||||
/** Apply scope settings (the JS settings object) to the DSP. */
|
||||
virtual void configure(const juce::var& settings) = 0;
|
||||
|
||||
/**
|
||||
* Apply a scope-specific native config pushed from the UI on the
|
||||
* "prismSpectrogramConfig" event. Default no-op; only scopes whose DSP needs
|
||||
* canvas-derived parameters (e.g. the spectrogram's rowCount) override this.
|
||||
*/
|
||||
virtual void configureNative(const juce::var&) {}
|
||||
|
||||
/** Feed audio (called off the realtime thread). numSamples may be 0. */
|
||||
virtual void process(const float* left, const float* right, int numSamples) = 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
|
||||
#include "ScopeEngine.h"
|
||||
#include "spectrogram.h" // reused, unmodified, from native/src
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
/**
|
||||
* Spectrogram engine. The reused Visualizer::SpectrogramAnalyzer runs in C++ and
|
||||
* produces finished display+heat columns. Unlike the other scopes, its DSP needs
|
||||
* the canvas-derived rowCount (and fft/freq/db/scale/orientation), which only the
|
||||
* webview knows — the UI pushes the full native config via "prismSpectrogramConfig",
|
||||
* routed here through configureNative(). process() mixes to mono and runs the DSP;
|
||||
* buildFrame() emits the columns produced since the last frame (base64) tagged with
|
||||
* rowCount, so the bridge can match them to the config the UI currently expects.
|
||||
* configure() (scope settings) is a no-op — every DSP parameter arrives in the
|
||||
* native config. The host sample rate (from setSampleRate) overrides whatever the
|
||||
* UI believed, so frequency mapping is always correct.
|
||||
*/
|
||||
class SpectrogramEngine : public ScopeEngine
|
||||
{
|
||||
public:
|
||||
const char* scopeId() const override { return "spectrogram"; }
|
||||
juce::Identifier frameEventId() const override { return frameId; }
|
||||
|
||||
void setSampleRate(double sampleRate) override
|
||||
{
|
||||
if (sampleRate > 0.0 && (float) sampleRate != config.sampleRate)
|
||||
{
|
||||
config.sampleRate = (float) sampleRate;
|
||||
if (hasConfig)
|
||||
spectro.configure(config);
|
||||
}
|
||||
}
|
||||
|
||||
void configure(const juce::var&) override {}
|
||||
|
||||
void configureNative(const juce::var& opts) override
|
||||
{
|
||||
if (! opts.isObject())
|
||||
return;
|
||||
|
||||
config.fftSize = (size_t) std::max(0, (int) opts.getProperty("fftSize", 4096));
|
||||
config.rowCount = (size_t) std::max(0, (int) opts.getProperty("rowCount", 0));
|
||||
config.minFrequency = (float) opts.getProperty("minFrequency", 20.0);
|
||||
config.maxFrequency = (float) opts.getProperty("maxFrequency", 20000.0);
|
||||
config.minDecibels = (float) opts.getProperty("minDecibels", -90.0);
|
||||
config.maxDecibels = (float) opts.getProperty("maxDecibels", -12.0);
|
||||
config.scrollSpeed = (float) opts.getProperty("scrollSpeed", 2.0);
|
||||
config.contrast = (float) opts.getProperty("contrast", 1.0);
|
||||
config.tiltDbPerOctave = (float) opts.getProperty("tiltDbPerOctave", 4.0);
|
||||
config.clarityMode = opts.getProperty("clarityMode", "sharper").toString().toStdString();
|
||||
config.scaleMode = opts.getProperty("scaleMode", "log").toString().toStdString();
|
||||
config.orientation = opts.getProperty("orientation", "horizontal").toString().toStdString();
|
||||
|
||||
// Host rate (from setSampleRate) is authoritative; the UI may still hold a
|
||||
// stale default before it has seen a frame. Only fall back to the UI value
|
||||
// if we have not yet learned the host rate.
|
||||
if (config.sampleRate <= 0.0f)
|
||||
config.sampleRate = (float) opts.getProperty("sampleRate", 48000.0);
|
||||
|
||||
hasConfig = config.rowCount > 0 && config.fftSize > 0;
|
||||
if (hasConfig)
|
||||
spectro.configure(config);
|
||||
|
||||
// The config changed: drop any half-built column batch so the next frame
|
||||
// starts clean at the new rowCount.
|
||||
pendingDisplay.clear();
|
||||
pendingHeat.clear();
|
||||
pendingColumns = 0;
|
||||
}
|
||||
|
||||
void process(const float* left, const float* right, int numSamples) override
|
||||
{
|
||||
if (! hasConfig || numSamples <= 0)
|
||||
return;
|
||||
if ((int) mono.size() < numSamples)
|
||||
mono.resize((size_t) numSamples);
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
mono[(size_t) i] = 0.5f * (left[i] + right[i]);
|
||||
|
||||
auto result = spectro.process(mono.data(), (size_t) numSamples);
|
||||
if (result.columnCount > 0 && result.rowCount == config.rowCount)
|
||||
{
|
||||
pendingDisplay.insert(pendingDisplay.end(), result.display.begin(), result.display.end());
|
||||
pendingHeat.insert(pendingHeat.end(), result.heat.begin(), result.heat.end());
|
||||
pendingColumns += result.columnCount;
|
||||
}
|
||||
}
|
||||
|
||||
juce::var buildFrame(double sampleRate) override
|
||||
{
|
||||
auto* obj = new juce::DynamicObject();
|
||||
obj->setProperty("sampleRate", sampleRate);
|
||||
obj->setProperty("rowCount", (int) config.rowCount);
|
||||
obj->setProperty("columnCount", (int) pendingColumns);
|
||||
if (pendingColumns > 0)
|
||||
{
|
||||
obj->setProperty("display", juce::Base64::toBase64(pendingDisplay.data(), pendingDisplay.size() * sizeof(float)));
|
||||
obj->setProperty("heat", juce::Base64::toBase64(pendingHeat.data(), pendingHeat.size() * sizeof(float)));
|
||||
}
|
||||
else
|
||||
{
|
||||
obj->setProperty("display", juce::String());
|
||||
obj->setProperty("heat", juce::String());
|
||||
}
|
||||
pendingDisplay.clear();
|
||||
pendingHeat.clear();
|
||||
pendingColumns = 0;
|
||||
return juce::var(obj);
|
||||
}
|
||||
|
||||
private:
|
||||
const juce::Identifier frameId { "spectrogramFrame" };
|
||||
Visualizer::SpectrogramAnalyzer spectro;
|
||||
Visualizer::SpectrogramConfig config;
|
||||
bool hasConfig = false;
|
||||
std::vector<float> mono;
|
||||
std::vector<float> pendingDisplay, pendingHeat;
|
||||
size_t pendingColumns = 0;
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { SpectrogramNativeAnalyzer, SpectrogramNativeOptions, SpectrogramNativeResult } from '../renderer/audio/native'
|
||||
import { emitToHost } from './juceBridge'
|
||||
|
||||
const EMPTY = new Float32Array(0)
|
||||
|
||||
/**
|
||||
* Drop-in `SpectrogramNativeAnalyzer` for the plugin webview.
|
||||
*
|
||||
* The spectrogram DSP runs in the C++ plugin, but its output depends on the
|
||||
* canvas-derived `rowCount` that only the UI knows. So this shim works in two
|
||||
* directions: `configure()` forwards the full native config to C++ (event
|
||||
* "prismSpectrogramConfig"), and the host streams finished display+heat columns
|
||||
* back which the bridge enqueues via `pushFrame()`. `process()` ignores its audio
|
||||
* argument (no samples flow through the webview) and returns all columns queued
|
||||
* since the last call, concatenated into one result.
|
||||
*
|
||||
* Columns are only kept while their rowCount matches the rowCount the UI last
|
||||
* asked for — on a resize the UI reconfigures, we drop the stale queue, and the
|
||||
* C++ side catches up within a frame or two (a brief gap, never a mismatch).
|
||||
*/
|
||||
export class BridgeSpectrogramAnalyzer implements SpectrogramNativeAnalyzer {
|
||||
private expectedRowCount = 0
|
||||
private queuedDisplay: Float32Array[] = []
|
||||
private queuedHeat: Float32Array[] = []
|
||||
private queuedColumns = 0
|
||||
|
||||
configure(options: SpectrogramNativeOptions): void {
|
||||
if (options.rowCount !== this.expectedRowCount) {
|
||||
this.expectedRowCount = options.rowCount
|
||||
this.clearQueue()
|
||||
}
|
||||
emitToHost('prismSpectrogramConfig', options)
|
||||
}
|
||||
|
||||
/** Called by the bridge when the host emits a spectrogram frame. */
|
||||
pushFrame(display: Float32Array, heat: Float32Array, columnCount: number, rowCount: number): void {
|
||||
if (rowCount !== this.expectedRowCount || columnCount <= 0) return
|
||||
if (display.length < columnCount * rowCount || heat.length < columnCount * rowCount) return
|
||||
this.queuedDisplay.push(display)
|
||||
this.queuedHeat.push(heat)
|
||||
this.queuedColumns += columnCount
|
||||
}
|
||||
|
||||
/** The rowCount the UI last asked for (0 until first configure). */
|
||||
getExpectedRowCount(): number {
|
||||
return this.expectedRowCount
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
process(_audioData: Float32Array): SpectrogramNativeResult {
|
||||
const rowCount = this.expectedRowCount
|
||||
if (this.queuedColumns === 0 || rowCount <= 0) {
|
||||
return { display: EMPTY, heat: EMPTY, columnCount: 0, rowCount }
|
||||
}
|
||||
|
||||
const total = this.queuedColumns * rowCount
|
||||
const display = new Float32Array(total)
|
||||
const heat = new Float32Array(total)
|
||||
let offset = 0
|
||||
for (let i = 0; i < this.queuedDisplay.length; i += 1) {
|
||||
display.set(this.queuedDisplay[i], offset)
|
||||
heat.set(this.queuedHeat[i], offset)
|
||||
offset += this.queuedDisplay[i].length
|
||||
}
|
||||
|
||||
const columnCount = this.queuedColumns
|
||||
this.clearQueue()
|
||||
return { display, heat, columnCount, rowCount }
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.clearQueue()
|
||||
}
|
||||
|
||||
private clearQueue(): void {
|
||||
this.queuedDisplay = []
|
||||
this.queuedHeat = []
|
||||
this.queuedColumns = 0
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,12 @@ export class PluginWebViewDataSource implements SpectrumAnalyzerDataSource {
|
||||
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] : []
|
||||
}
|
||||
|
||||
// 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 }> {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useEffect, useRef, type JSX } from 'react'
|
||||
import { Spectrogram } from '../renderer/visualizers/Spectrogram'
|
||||
import type { ScopeSettings } from '../types/settings'
|
||||
import type { ResolvedSpectrogramTheme } from '../types/theme'
|
||||
import type { BridgeSpectrogramAnalyzer } from './BridgeSpectrogramAnalyzer'
|
||||
import type { PluginWebViewDataSource } from './PluginWebViewDataSource'
|
||||
import { spectrogramSettingsToOptions } from './spectrogramOptions'
|
||||
|
||||
interface SpectrogramScopeProps {
|
||||
dataSource: PluginWebViewDataSource
|
||||
nativeAnalyzer: BridgeSpectrogramAnalyzer
|
||||
settings: ScopeSettings['spectrogram']
|
||||
theme: ResolvedSpectrogramTheme
|
||||
}
|
||||
|
||||
export default function SpectrogramScope({
|
||||
dataSource,
|
||||
nativeAnalyzer,
|
||||
settings,
|
||||
theme,
|
||||
}: SpectrogramScopeProps): JSX.Element {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const vizRef = useRef<Spectrogram | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
const canvas = canvasRef.current
|
||||
if (!container || !canvas) return
|
||||
|
||||
const viz = new Spectrogram(canvas, {
|
||||
...spectrogramSettingsToOptions(settings, theme),
|
||||
dataSource,
|
||||
nativeAnalyzer,
|
||||
})
|
||||
vizRef.current = viz
|
||||
|
||||
const applySize = (): void => {
|
||||
const rect = container.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const pixelWidth = Math.max(1, Math.floor(rect.width * dpr))
|
||||
const pixelHeight = Math.max(1, Math.floor(rect.height * dpr))
|
||||
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
|
||||
canvas.width = pixelWidth
|
||||
canvas.height = pixelHeight
|
||||
viz.resize()
|
||||
}
|
||||
}
|
||||
|
||||
applySize()
|
||||
viz.start()
|
||||
const observer = new ResizeObserver(applySize)
|
||||
observer.observe(container)
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
viz.dispose()
|
||||
vizRef.current = null
|
||||
}
|
||||
// settings/theme applied via setOptions below.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataSource, nativeAnalyzer])
|
||||
|
||||
useEffect(() => {
|
||||
vizRef.current?.setOptions(spectrogramSettingsToOptions(settings, theme))
|
||||
}, [settings, theme])
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="spectrum-scope">
|
||||
<canvas ref={canvasRef} className="spectrum-scope__canvas" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -442,6 +442,99 @@ export function connectVectorscopeBridge(handlers: VectorscopeBridgeHandlers): (
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spectrogram frames (event "spectrogramFrame"): the new display+heat columns
|
||||
// produced since the last frame, base64-encoded, tagged with rowCount/columnCount.
|
||||
|
||||
export interface SpectrogramFrame {
|
||||
sampleRate: number
|
||||
display: Float32Array
|
||||
heat: Float32Array
|
||||
columnCount: number
|
||||
rowCount: number
|
||||
}
|
||||
|
||||
interface SpectrogramFramePayload {
|
||||
sampleRate?: number
|
||||
display?: string
|
||||
heat?: string
|
||||
columnCount?: number
|
||||
rowCount?: number
|
||||
}
|
||||
|
||||
function decodeSpectrogramFrame(payload: unknown): SpectrogramFrame | null {
|
||||
if (typeof payload !== 'object' || payload === null) return null
|
||||
const { sampleRate, display, heat, columnCount, rowCount } = payload as SpectrogramFramePayload
|
||||
return {
|
||||
sampleRate: typeof sampleRate === 'number' && sampleRate > 0 ? sampleRate : 48000,
|
||||
display: typeof display === 'string' ? base64ToFloat32Array(display) : new Float32Array(0),
|
||||
heat: typeof heat === 'string' ? base64ToFloat32Array(heat) : new Float32Array(0),
|
||||
columnCount: typeof columnCount === 'number' ? columnCount : 0,
|
||||
rowCount: typeof rowCount === 'number' ? rowCount : 0,
|
||||
}
|
||||
}
|
||||
|
||||
export interface SpectrogramBridgeHandlers {
|
||||
onFrame: (frame: SpectrogramFrame) => void
|
||||
onConnected?: (usingMock: boolean) => void
|
||||
/** Lets the dev mock size its columns to the canvas-derived rowCount. */
|
||||
getRowCount?: () => number
|
||||
}
|
||||
|
||||
export function connectSpectrogramBridge(handlers: SpectrogramBridgeHandlers): () => void {
|
||||
let disposed = false
|
||||
let listenerId: number | null = null
|
||||
let mockRaf: number | null = null
|
||||
|
||||
const startMock = (): void => {
|
||||
handlers.onConnected?.(true)
|
||||
console.warn('[prism-plugin] no JUCE host — using synthetic spectrogram (browser dev mode)')
|
||||
let phase = 0
|
||||
const tick = (): void => {
|
||||
if (disposed) return
|
||||
phase += 0.08
|
||||
const rowCount = handlers.getRowCount?.() ?? 0
|
||||
if (rowCount > 0) {
|
||||
const columnCount = 2
|
||||
const display = new Float32Array(rowCount * columnCount)
|
||||
const heat = new Float32Array(rowCount * columnCount)
|
||||
for (let c = 0; c < columnCount; c += 1) {
|
||||
for (let r = 0; r < rowCount; r += 1) {
|
||||
const t = r / rowCount
|
||||
const band = Math.exp(-Math.pow((t - (0.3 + 0.2 * Math.sin(phase))) * 6, 2))
|
||||
const v = Math.min(1, band + Math.random() * 0.15)
|
||||
display[c * rowCount + r] = v
|
||||
heat[c * rowCount + r] = v
|
||||
}
|
||||
}
|
||||
handlers.onFrame({ sampleRate: 48000, display, heat, columnCount, rowCount })
|
||||
}
|
||||
mockRaf = requestAnimationFrame(tick)
|
||||
}
|
||||
mockRaf = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
void ensureBackend().then((backend) => {
|
||||
if (disposed) return
|
||||
if (backend) {
|
||||
listenerId = backend.addEventListener('spectrogramFrame', (payload) => {
|
||||
const frame = decodeSpectrogramFrame(payload)
|
||||
if (frame) handlers.onFrame(frame)
|
||||
})
|
||||
handlers.onConnected?.(false)
|
||||
console.log('[prism-plugin] connected to JUCE host (spectrogram)')
|
||||
} else {
|
||||
startMock()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
disposed = true
|
||||
if (mockRaf !== null) cancelAnimationFrame(mockRaf)
|
||||
if (listenerId !== null) window.__JUCE__?.backend?.removeEventListener?.(listenerId)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Oscilloscope frames (event "oscilloscopeFrame": { sampleRate, samples, pitch }).
|
||||
|
||||
|
||||
+28
-1
@@ -8,13 +8,15 @@ import OscilloscopeScope from './OscilloscopeScope'
|
||||
import VUMeterScope from './VUMeterScope'
|
||||
import LUFSMeterScope from './LUFSMeterScope'
|
||||
import VectorscopeScope from './VectorscopeScope'
|
||||
import SpectrogramScope from './SpectrogramScope'
|
||||
import { BridgeSpectrumAnalyzer } from './BridgeSpectrumAnalyzer'
|
||||
import { BridgeOscilloscopeAnalyzer } from './BridgeOscilloscopeAnalyzer'
|
||||
import { BridgeVUMeterAnalyzer } from './BridgeVUMeterAnalyzer'
|
||||
import { BridgeLUFSMeterAnalyzer } from './BridgeLUFSMeterAnalyzer'
|
||||
import { BridgeVectorscopeAnalyzer } from './BridgeVectorscopeAnalyzer'
|
||||
import { BridgeSpectrogramAnalyzer } from './BridgeSpectrogramAnalyzer'
|
||||
import { PluginWebViewDataSource } from './PluginWebViewDataSource'
|
||||
import { connectOscilloscopeBridge, connectSpectrumBridge, connectVUMeterBridge, connectLUFSMeterBridge, connectVectorscopeBridge } from './juceBridge'
|
||||
import { connectOscilloscopeBridge, connectSpectrumBridge, connectVUMeterBridge, connectLUFSMeterBridge, connectVectorscopeBridge, connectSpectrogramBridge } from './juceBridge'
|
||||
|
||||
// The C++ plugin tells us which scope it is via JUCE initialisation data.
|
||||
// JUCE stores each value as an array (e.g. prismScope = ["oscilloscope"]).
|
||||
@@ -29,6 +31,31 @@ function getScopeKind(): string {
|
||||
const dataSource = new PluginWebViewDataSource()
|
||||
|
||||
function buildApp(): JSX.Element {
|
||||
if (getScopeKind() === 'spectrogram') {
|
||||
const analyzer = new BridgeSpectrogramAnalyzer()
|
||||
connectSpectrogramBridge({
|
||||
onFrame: (frame) => {
|
||||
analyzer.pushFrame(frame.display, frame.heat, frame.columnCount, frame.rowCount)
|
||||
dataSource.setSampleRate(frame.sampleRate)
|
||||
dataSource.setPlaying(true)
|
||||
},
|
||||
getRowCount: () => analyzer.getExpectedRowCount(),
|
||||
})
|
||||
return (
|
||||
<ScopeApp
|
||||
kind="spectrogram"
|
||||
renderScope={(settings, theme) => (
|
||||
<SpectrogramScope
|
||||
settings={settings}
|
||||
theme={theme.spectrogram}
|
||||
dataSource={dataSource}
|
||||
nativeAnalyzer={analyzer}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (getScopeKind() === 'vectorscope') {
|
||||
const analyzer = new BridgeVectorscopeAnalyzer()
|
||||
connectVectorscopeBridge({
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ScopeSettings } from '../types/settings'
|
||||
import type { ResolvedSpectrogramTheme } from '../types/theme'
|
||||
import type { SpectrogramOptions } from '../renderer/visualizers/Spectrogram'
|
||||
|
||||
/**
|
||||
* Map Prism's spectrogram settings + resolved theme to Spectrogram options.
|
||||
* Mirrors the `spectrogram` case of `scopeSettingsToOptions` in ScopeModule.tsx.
|
||||
*/
|
||||
export function spectrogramSettingsToOptions(
|
||||
settings: ScopeSettings['spectrogram'],
|
||||
theme: ResolvedSpectrogramTheme,
|
||||
): SpectrogramOptions {
|
||||
return {
|
||||
lineColor: theme.mono,
|
||||
heatColors: theme.heatColors,
|
||||
backgroundColor: theme.background,
|
||||
fftSize: settings.fftSize,
|
||||
tiltDbPerOctave: settings.tiltDbPerOctave,
|
||||
scrollSpeed: settings.scrollSpeed,
|
||||
contrast: settings.contrast,
|
||||
clarityMode: settings.clarityMode,
|
||||
scaleMode: settings.scaleMode,
|
||||
orientation: settings.orientation,
|
||||
colorScheme: settings.colorScheme,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user