mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 05:10:51 +02:00
123 lines
5.2 KiB
C++
123 lines
5.2 KiB
C++
#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; }
|
|
PreferredSize preferredSize() const override { return { 700, 340, 360, 200 }; }
|
|
|
|
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;
|
|
};
|