Get sosci fully working with RGB

pull/325/head
James H Ball 2025-09-13 17:30:31 +01:00
rodzic e6969845dc
commit f49a29407c
9 zmienionych plików z 190 dodań i 64 usunięć

Wyświetl plik

@ -132,6 +132,7 @@ public:
std::function<void()> haltRecording;
std::atomic<bool> forceDisableBrightnessInput = false;
std::atomic<bool> forceDisableRgbInput = false;
// shouldn't be accessed by audio thread, but needs to persist when GUI is closed
// so should only be accessed by message thread
@ -163,6 +164,15 @@ public:
protected:
bool brightnessEnabled = false;
bool rgbEnabled = false;
// Expose flags to GUI thread safely
public:
bool isBrightnessEnabled() const { return brightnessEnabled; }
bool isRgbEnabled() const { return rgbEnabled; }
bool getForceDisableBrightnessInput() const { return forceDisableBrightnessInput.load(); }
bool getForceDisableRgbInput() const { return forceDisableRgbInput.load(); }
protected:
std::vector<osci::BooleanParameter*> booleanParameters;
std::vector<osci::FloatParameter*> floatParameters;

Wyświetl plik

@ -1,7 +1,7 @@
#include "SosciPluginProcessor.h"
#include "SosciPluginEditor.h"
SosciAudioProcessor::SosciAudioProcessor() : CommonAudioProcessor(BusesProperties().withInput("Input", juce::AudioChannelSet::namedChannelSet(4), true).withOutput("Output", juce::AudioChannelSet::stereo(), true)) {
SosciAudioProcessor::SosciAudioProcessor() : CommonAudioProcessor(BusesProperties().withInput("Input", juce::AudioChannelSet::namedChannelSet(5), true).withOutput("Output", juce::AudioChannelSet::stereo(), true)) {
// demo audio file on standalone only
if (juce::JUCEApplicationBase::isStandaloneApp()) {
std::unique_ptr<juce::InputStream> stream = std::make_unique<juce::MemoryInputStream>(BinaryData::sosci_flac, BinaryData::sosci_flacSize, false);
@ -36,23 +36,41 @@ void SosciAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::M
} else {
float x = input.getNumChannels() > 0 ? inputArray[0][sample] : 0.0f;
float y = input.getNumChannels() > 1 ? inputArray[1][sample] : 0.0f;
float brightness = 1.0f;
float zAsBrightnessOrR = 1.0f;
if (input.getNumChannels() > 2 && !forceDisableBrightnessInput) {
float brightnessChannel = inputArray[2][sample];
// Only enable brightness if we actually receive a signal on the brightness channel
if (!brightnessEnabled && brightnessChannel > EPSILON) {
brightnessEnabled = true;
float zChan = inputArray[2][sample];
if (!brightnessEnabled && zChan > EPSILON) brightnessEnabled = true;
if (brightnessEnabled) zAsBrightnessOrR = zChan;
}
// RGB detection: if channels 3 or 4 present and not forced off, treat as RGB mode.
float gChan = 0.0f, bChan = 0.0f;
bool haveG = input.getNumChannels() > 3;
bool haveB = input.getNumChannels() > 4;
if (!forceDisableRgbInput && (haveG || haveB)) {
float gIn = haveG ? inputArray[3][sample] : 0.0f;
float bIn = haveB ? inputArray[4][sample] : 0.0f;
// Enable RGB only when we actually receive signal
if (!rgbEnabled && (std::abs(gIn) > EPSILON || std::abs(bIn) > EPSILON)) {
rgbEnabled = true;
}
if (brightnessEnabled) {
brightness = brightnessChannel;
if (rgbEnabled) {
gChan = gIn;
bChan = bIn;
}
}
point = { x, y, brightness };
// Build point:
// - In RGB mode: use z as R, and channels 3/4 as G/B
// - Otherwise: use z as brightness and leave RGB at defaults
if (rgbEnabled && !forceDisableRgbInput) {
point = osci::Point(x, y, 1.0f, zAsBrightnessOrR, gChan, bChan);
} else {
point = osci::Point(x, y, zAsBrightnessOrR);
}
}
// no negative brightness
point.z = juce::jlimit(0.0, 1.0, point.z);
// Clamp brightness
point.z = juce::jlimit(0.0, 1.0, point.z);
for (auto& effect : permanentEffects) {
point = effect->apply(sample, point);

Wyświetl plik

@ -24,7 +24,7 @@ public:
readHead += buffer.size();
}
return osci::Point(input.x, buffer[readHead].y, input.z);
return osci::Point(input.x, buffer[readHead].y, input.z, input.r, input.g, input.b);
}
std::shared_ptr<osci::Effect> build() const override {

Wyświetl plik

@ -106,6 +106,18 @@ void SosciMainMenuBarModel::resetMenuItems() {
addMenuItem(3, "Force Disable Brightness Input", [&]() {
processor.forceDisableBrightnessInput = !processor.forceDisableBrightnessInput;
if (processor.forceDisableBrightnessInput) {
// Disabling brightness should also disable RGB
processor.forceDisableRgbInput = true;
}
menuItemsChanged();
});
addMenuItem(3, "Force Disable RGB Input", [&]() {
processor.forceDisableRgbInput = !processor.forceDisableRgbInput;
if (!processor.forceDisableRgbInput) {
// Enabling RGB implies brightness is allowed too
processor.forceDisableBrightnessInput = false;
}
menuItemsChanged();
});

Wyświetl plik

@ -12,6 +12,7 @@ uniform vec2 uScale;
uniform float uFishEye;
uniform sampler2D uScreen; // still sampled for focus/gain texturing, but we'll reduce its influence on colour
uniform float uLineHueShift; // 0..1 hue shift for the beam colour
uniform float uUseVertexColor; // 1.0 to use per-vertex RGB, 0.0 to use hue-only
varying float vSize;
varying vec4 uvl;
varying vec2 vTexCoord;
@ -51,8 +52,14 @@ void main() {
float len = uvl.z;
vec2 xy = uvl.xy;
float brightness;
// Apply hue shift immediately to the incoming colour, before any further colour operations
vec3 baseColor = hueShift(vColor, uLineHueShift);
// Determine base color: either per-vertex RGB with hue shift, or hue-only using a fixed seed color
vec3 baseColor;
if (uUseVertexColor > 0.5) {
baseColor = hueShift(vColor, uLineHueShift);
} else {
// Start from a seed color and rotate hue; using a non-primary seed to cover spectrum nicely
baseColor = hueShift(vec3(1.0, 0.7, 0.2), uLineHueShift);
}
baseColor = clamp(baseColor, 0.0, 1.0);
float sigma = vSize/5.0;

Wyświetl plik

@ -110,6 +110,7 @@ VisualiserComponent::VisualiserComponent(
preRenderCallback = [this] {
if (!record.getToggleState()) {
updateRenderModeFromProcessor();
setResolution(this->recordingSettings.getResolution());
setFrameRate(this->recordingSettings.getFrameRate());
}
@ -518,6 +519,26 @@ void VisualiserComponent::childUpdated() {
}
}
void VisualiserComponent::updateRenderModeFromProcessor() {
DBG(juce::String((int) getRenderMode()));
// Called on message thread
if (!visualiserOnly) {
setRenderMode(RenderMode::XY);
return;
}
// Determine based on whether brightness and RGB are enabled and not force-disabled
bool brightnessAllowed = !audioProcessor.getForceDisableBrightnessInput();
bool rgbAllowed = !audioProcessor.getForceDisableRgbInput();
// Prefer RGB if we have 4th/5th channels effectively
if (rgbAllowed && audioProcessor.isRgbEnabled()) {
setRenderMode(RenderMode::XYRGB);
} else if (brightnessAllowed && audioProcessor.isBrightnessEnabled()) {
setRenderMode(RenderMode::XYZ);
} else {
setRenderMode(RenderMode::XY);
}
}
#if OSCI_PREMIUM
void VisualiserComponent::initialiseSharedTexture() {
Texture renderTexture = getRenderTexture();

Wyświetl plik

@ -57,6 +57,7 @@ public:
bool keyPressed(const juce::KeyPress& key) override;
void setRecording(bool recording);
void childUpdated();
void updateRenderModeFromProcessor();
VisualiserComponent* parent = nullptr;
VisualiserComponent* child = nullptr;
@ -93,6 +94,7 @@ private:
int lastMouseX = 0;
int lastMouseY = 0;
int timerId = 0;
int renderModeTimerId = 0;
bool hideButtonRow = false;
bool fullScreen = false;
std::function<void(FullScreenMode)> fullScreenCallback;

Wyświetl plik

@ -50,6 +50,7 @@ void VisualiserRenderer::runTask(const std::vector<osci::Point> &points) {
xSamples.clear();
ySamples.clear();
zSamples.clear();
rSamples.clear();
gSamples.clear();
bSamples.clear();
@ -71,6 +72,7 @@ void VisualiserRenderer::runTask(const std::vector<osci::Point> &points) {
static double testPhase = 0.0;
auto mode = renderMode.load();
if (parameters.isSweepEnabled()) {
double sweepIncrement = getSweepIncrement();
long samplesPerSweep = sampleRate * parameters.getSweepSeconds();
@ -96,11 +98,14 @@ void VisualiserRenderer::runTask(const std::vector<osci::Point> &points) {
xSamples.push_back(sweepPoint.x);
ySamples.push_back(sweepPoint.y);
// Unconditional test pattern colour (ignore incoming point colour)
rSamples.push_back(0.0);
gSamples.push_back(1.0);
bSamples.push_back(0.0);
if (mode == RenderMode::XYZ) {
zSamples.push_back(sweepPoint.z);
} else if (mode == RenderMode::XYRGB) {
// colour provided (if not, Z will mirror into r by upstream logic)
rSamples.push_back((float) sweepPoint.r);
gSamples.push_back((float) sweepPoint.g);
bSamples.push_back((float) sweepPoint.b);
}
sampleCount++;
}
@ -118,11 +123,13 @@ void VisualiserRenderer::runTask(const std::vector<osci::Point> &points) {
xSamples.push_back(point.x);
ySamples.push_back(point.y);
// Unconditional test pattern colour (ignore incoming point colour)
rSamples.push_back(1.0);
gSamples.push_back(0.0);
bSamples.push_back(0.0);
if (mode == RenderMode::XYZ) {
zSamples.push_back(point.z);
} else if (mode == RenderMode::XYRGB) {
rSamples.push_back((float) point.r);
gSamples.push_back((float) point.g);
bSamples.push_back((float) point.b);
}
}
}
@ -133,9 +140,12 @@ void VisualiserRenderer::runTask(const std::vector<osci::Point> &points) {
smoothedXSamples.resize(newResampledSize);
smoothedYSamples.resize(newResampledSize);
smoothedRSamples.resize(newResampledSize);
smoothedGSamples.resize(newResampledSize);
smoothedBSamples.resize(newResampledSize);
if (mode == RenderMode::XYZ) smoothedZSamples.resize(newResampledSize);
if (mode == RenderMode::XYRGB) {
smoothedRSamples.resize(newResampledSize);
smoothedGSamples.resize(newResampledSize);
smoothedBSamples.resize(newResampledSize);
}
if (parameters.isSweepEnabled()) {
// interpolate between sweep values to avoid any artifacts from quickly going from one sweep to the next
@ -154,19 +164,16 @@ void VisualiserRenderer::runTask(const std::vector<osci::Point> &points) {
}
}
} else {
xResampler.process(xSamples.data(), smoothedXSamples.data(), xSamples.size());
xResampler.process(xSamples.data(), smoothedXSamples.data(), (int) xSamples.size());
}
yResampler.process(ySamples.data(), smoothedYSamples.data(), (int) ySamples.size());
if (mode == RenderMode::XYZ) {
if (!zSamples.empty()) zResampler.process(zSamples.data(), smoothedZSamples.data(), (int) zSamples.size());
} else if (mode == RenderMode::XYRGB) {
if (!rSamples.empty()) rResampler.process(rSamples.data(), smoothedRSamples.data(), (int) rSamples.size());
if (!gSamples.empty()) gResampler.process(gSamples.data(), smoothedGSamples.data(), (int) gSamples.size());
if (!bSamples.empty()) bResampler.process(bSamples.data(), smoothedBSamples.data(), (int) bSamples.size());
}
yResampler.process(ySamples.data(), smoothedYSamples.data(), ySamples.size());
// Legacy: replicate intensity from point.z across RGB until upstream provides distinct channels
// When color data available, rSamples/gSamples/bSamples will hold independent values
// Colour channels: independent resamplers to avoid cross-channel state pollution that caused
// subtle colour discontinuities when a single shared zResampler was reused sequentially.
// If residual banding persists, consider:
// * adding slight pre-blur (lowpass) to each colour before upsampling
// * or using a shared phase accumulator & polyphase kernel to guarantee alignment.
rResampler.process(rSamples.data(), smoothedRSamples.data(), rSamples.size());
gResampler.process(gSamples.data(), smoothedGSamples.data(), gSamples.size());
bResampler.process(bSamples.data(), smoothedBSamples.data(), bSamples.size());
}
}
@ -183,6 +190,7 @@ int VisualiserRenderer::prepareTask(double sampleRate, int bufferSize) {
this->sampleRate = sampleRate;
xResampler.prepare(sampleRate, RESAMPLE_RATIO);
yResampler.prepare(sampleRate, RESAMPLE_RATIO);
zResampler.prepare(sampleRate, RESAMPLE_RATIO);
rResampler.prepare(sampleRate, RESAMPLE_RATIO);
gResampler.prepare(sampleRate, RESAMPLE_RATIO);
bResampler.prepare(sampleRate, RESAMPLE_RATIO);
@ -528,7 +536,12 @@ void VisualiserRenderer::drawLineTexture(const std::vector<float> &xPoints, cons
activateTargetTexture(lineTexture);
fade();
drawLine(xPoints, yPoints, rPoints, gPoints, bPoints);
const std::vector<float>* brightness = nullptr;
auto mode = renderMode.load();
if (mode == RenderMode::XYZ) {
brightness = parameters.getUpsamplingEnabled() ? &smoothedZSamples : &zSamples;
}
drawLine(xPoints, yPoints, brightness, rPoints, gPoints, bPoints, renderMode.load());
glBindTexture(GL_TEXTURE_2D, targetTexture.value().id);
}
@ -653,33 +666,45 @@ void VisualiserRenderer::setNormalBlending() {
}
void VisualiserRenderer::drawLine(const std::vector<float> &xPoints, const std::vector<float> &yPoints,
const std::vector<float> &rPoints, const std::vector<float> &gPoints, const std::vector<float> &bPoints) {
const std::vector<float> *brightnessPoints,
const std::vector<float> &rPoints, const std::vector<float> &gPoints, const std::vector<float> &bPoints,
RenderMode mode) {
using namespace juce::gl;
setAdditiveBlending();
int nPoints = xPoints.size();
int nPoints = (int) xPoints.size();
// Without this, there's an access violation that seems to occur only on some systems
std::vector<float> positionData(nPoints * 12);
std::vector<float> colorData(nPoints * 12);
std::vector<float> colorData;
if (mode == RenderMode::XYRGB) colorData.resize(nPoints * 12);
for (int i = 0; i < nPoints; ++i) {
int p = i * 12;
float x = xPoints[i];
float y = yPoints[i];
float r = rPoints[i];
float g = gPoints[i];
float b = bPoints[i];
// Use max channel as base beam intensity but scale to compensate for multi-channel energy
float brightness = std::max(std::max(r, g), b);
float brightness = 1.0f;
if (mode == RenderMode::XYZ) {
if (brightnessPoints != nullptr && i < (int) brightnessPoints->size()) brightness = (*brightnessPoints)[i];
} else if (mode == RenderMode::XYRGB) {
float r = rPoints[i];
float g = gPoints[i];
float b = bPoints[i];
brightness = std::max(r, std::max(g, b));
}
for (int k = 0; k < 4; ++k) {
positionData[p + 3 * k] = x;
positionData[p + 3 * k + 1] = y;
positionData[p + 3 * k + 2] = brightness;
colorData[p + 3 * k] = r;
colorData[p + 3 * k + 1] = g;
colorData[p + 3 * k + 2] = b;
if (mode == RenderMode::XYRGB) {
float r = rPoints[i];
float g = gPoints[i];
float b = bPoints[i];
colorData[p + 3 * k] = r;
colorData[p + 3 * k + 1] = g;
colorData[p + 3 * k + 2] = b;
}
}
}
@ -687,9 +712,11 @@ void VisualiserRenderer::drawLine(const std::vector<float> &xPoints, const std::
glBufferData(GL_ARRAY_BUFFER, positionData.size() * sizeof(float), positionData.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, colorBuffer);
glBufferData(GL_ARRAY_BUFFER, colorData.size() * sizeof(float), colorData.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
if (mode == RenderMode::XYRGB) {
glBindBuffer(GL_ARRAY_BUFFER, colorBuffer);
glBufferData(GL_ARRAY_BUFFER, colorData.size() * sizeof(float), colorData.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
lineShader->use();
GLint aStartLoc = glGetAttribLocation(lineShader->getProgramID(), "aStart");
@ -700,16 +727,20 @@ void VisualiserRenderer::drawLine(const std::vector<float> &xPoints, const std::
glEnableVertexAttribArray(aStartLoc);
glEnableVertexAttribArray(aEndLoc);
glEnableVertexAttribArray(aStartColorLoc);
glEnableVertexAttribArray(aEndColorLoc);
if (mode == RenderMode::XYRGB) {
if (aStartColorLoc >= 0) glEnableVertexAttribArray(aStartColorLoc);
if (aEndColorLoc >= 0) glEnableVertexAttribArray(aEndColorLoc);
}
glEnableVertexAttribArray(aIdxLoc);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glVertexAttribPointer(aStartLoc, 3, GL_FLOAT, GL_FALSE, 0, 0);
glVertexAttribPointer(aEndLoc, 3, GL_FLOAT, GL_FALSE, 0, (void *)(12 * sizeof(float)));
glBindBuffer(GL_ARRAY_BUFFER, colorBuffer);
glVertexAttribPointer(aStartColorLoc, 3, GL_FLOAT, GL_FALSE, 0, 0);
glVertexAttribPointer(aEndColorLoc, 3, GL_FLOAT, GL_FALSE, 0, (void *)(12 * sizeof(float)));
if (mode == RenderMode::XYRGB) {
glBindBuffer(GL_ARRAY_BUFFER, colorBuffer);
if (aStartColorLoc >= 0) glVertexAttribPointer(aStartColorLoc, 3, GL_FLOAT, GL_FALSE, 0, 0);
if (aEndColorLoc >= 0) glVertexAttribPointer(aEndColorLoc, 3, GL_FLOAT, GL_FALSE, 0, (void *)(12 * sizeof(float)));
}
glBindBuffer(GL_ARRAY_BUFFER, quadIndexBuffer);
glVertexAttribPointer(aIdxLoc, 1, GL_FLOAT, GL_FALSE, 0, 0);
@ -720,6 +751,7 @@ void VisualiserRenderer::drawLine(const std::vector<float> &xPoints, const std::
lineShader->setUniform("uGain", 450.0f / 512.0f);
lineShader->setUniform("uInvert", 1.0f);
lineShader->setUniform("uLineHueShift", (GLfloat)(parameters.getHue() / 360.0));
lineShader->setUniform("uUseVertexColor", mode == RenderMode::XYRGB ? 1.0f : 0.0f);
float intensity = parameters.getIntensity() * (41000.0f / sampleRate);
if (parameters.getUpsamplingEnabled()) {
@ -746,8 +778,10 @@ void VisualiserRenderer::drawLine(const std::vector<float> &xPoints, const std::
glDisableVertexAttribArray(aStartLoc);
glDisableVertexAttribArray(aEndLoc);
glDisableVertexAttribArray(aStartColorLoc);
glDisableVertexAttribArray(aEndColorLoc);
if (mode == RenderMode::XYRGB) {
if (aStartColorLoc >= 0) glDisableVertexAttribArray(aStartColorLoc);
if (aEndColorLoc >= 0) glDisableVertexAttribArray(aEndColorLoc);
}
glDisableVertexAttribArray(aIdxLoc);
}
@ -1036,7 +1070,14 @@ void VisualiserRenderer::renderScope(const std::vector<float> &xPoints, const st
renderScale = (float)openGLContext.getRenderingScale();
drawLineTexture(xPoints, yPoints, rPoints, gPoints, bPoints);
// Provide dummy colour buffers for non-RGB modes to avoid allocations
static std::vector<float> empty;
auto mode = renderMode.load();
if (mode != RenderMode::XYRGB) {
drawLineTexture(xPoints, yPoints, empty, empty, empty);
} else {
drawLineTexture(xPoints, yPoints, rPoints, gPoints, bPoints);
}
checkGLErrors(__FILE__, __LINE__);
drawCRT();
checkGLErrors(__FILE__, __LINE__);

Wyświetl plik

@ -15,6 +15,11 @@ struct Texture {
class VisualiserWindow;
class VisualiserRenderer : public juce::Component, public osci::AudioBackgroundThread, public juce::OpenGLRenderer, public juce::AsyncUpdater {
public:
enum class RenderMode : int {
XY = 1,
XYZ = 2,
XYRGB = 3,
};
VisualiserRenderer(
VisualiserParameters &parameters,
osci::AudioBackgroundThreadManager &threadManager,
@ -34,6 +39,9 @@ public:
void openGLContextClosing() override;
void setResolution(int width);
void setFrameRate(double frameRate);
// Render mode can be changed from the message thread at any time
void setRenderMode(RenderMode mode) { renderMode.store(mode); }
RenderMode getRenderMode() const { return renderMode.load(); }
int getRenderWidth() const { return renderTexture.width; }
int getRenderHeight() const { return renderTexture.height; }
@ -82,11 +90,14 @@ private: juce::Rectangle<int> viewportArea;
// XYRGB sample buffers (currently RGB derived from legacy Z brightness until full pipeline provides color)
std::vector<float> xSamples{2};
std::vector<float> ySamples{2};
// brightness (Z) channel used only in XYZ mode
std::vector<float> zSamples{2};
std::vector<float> rSamples{2};
std::vector<float> gSamples{2};
std::vector<float> bSamples{2};
std::vector<float> smoothedXSamples;
std::vector<float> smoothedYSamples;
std::vector<float> smoothedZSamples;
std::vector<float> smoothedRSamples;
std::vector<float> smoothedGSamples;
std::vector<float> smoothedBSamples;
@ -155,10 +166,12 @@ private: juce::Rectangle<int> viewportArea;
double oldSampleRate = -1;
chowdsp::ResamplingTypes::LanczosResampler<2048, 8> xResampler;
chowdsp::ResamplingTypes::LanczosResampler<2048, 8> yResampler;
chowdsp::ResamplingTypes::LanczosResampler<2048, 8> zResampler;
// Dedicated colour channel resamplers to maintain independent filter state per channel
chowdsp::ResamplingTypes::LanczosResampler<2048, 8> rResampler;
chowdsp::ResamplingTypes::LanczosResampler<2048, 8> gResampler;
chowdsp::ResamplingTypes::LanczosResampler<2048, 8> bResampler;
std::atomic<RenderMode> renderMode { RenderMode::XYRGB };
void setOffsetAndScale(juce::OpenGLShaderProgram* shader);
Texture makeTexture(int width, int height, GLuint textureID = 0);
@ -173,7 +186,9 @@ private: juce::Rectangle<int> viewportArea;
void setAdditiveBlending();
void setNormalBlending();
void drawLine(const std::vector<float>& xPoints, const std::vector<float>& yPoints,
const std::vector<float>& rPoints, const std::vector<float>& gPoints, const std::vector<float>& bPoints);
const std::vector<float>* brightnessPoints, // optional, used in XY/XYZ
const std::vector<float>& rPoints, const std::vector<float>& gPoints, const std::vector<float>& bPoints,
RenderMode mode);
void fade();
void drawCRT();
void checkGLErrors(juce::String file, int line);