fix(of): clamp postfx + scope4 + wave roll

Context: FX mode rendered pure white (bloom + grain + feedback
saturating), sample rate slider was unwired, and there was no
combined scope-style view.

Approach:
- Tone-map (Reinhard soft-knee) + clamp final color in postfx.frag.
- Lower default bloom 0.4 -> 0.15 to match the new tone-map.
- Wire sampleRateHz_ slider to HantekDevice::setSampleRate (initial
  + on change) so the rate actually applies.
- WaveformVis: ring buffer (16x window) + timebase ms/div +
  scrollSpeed (0=freeze, >0=roll) + setSampleRate hook.
- New Scope4 mode (key 'q'): waveform / spectrogram / polar /
  lissajous in 2x2 grid.
- Also track previously-ignored shaders (postfx, plasma, tunnel)
  so other machines pick them up after pull.
This commit is contained in:
L'électron rare
2026-05-07 18:34:49 +02:00
parent 41aab8227d
commit c4e5007d86
12 changed files with 392 additions and 13 deletions
+7 -2
View File
@@ -10,8 +10,13 @@ launcher/**/xcuserdata/
launcher/**/*.xcuserstate
launcher/**/Package.resolved
# openFrameworks
oscope-of/bin/
# openFrameworks — on garde les shaders + settings.json pour qu'ils
# arrivent sur les autres machines, mais on ignore les binaires.
oscope-of/bin/*
!oscope-of/bin/data/
oscope-of/bin/data/*
!oscope-of/bin/data/shaders/
!oscope-of/bin/data/settings.json
oscope-of/obj/
oscope-of/.vscode/
oscope-of/Project.xcconfig
+49
View File
@@ -0,0 +1,49 @@
#version 120
uniform vec2 uRes;
uniform float uTime;
uniform float uBpm;
uniform float uBass;
uniform float uLead;
uniform float uKick;
uniform float uPad;
vec3 palette(float t) {
// Inigo Quilez-style cosine palette, hue tilted toward magenta/cyan
vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 1.0, 1.0);
vec3 d = vec3(0.0, 0.33, 0.67);
return a + b * cos(6.28318 * (c * t + d));
}
void main() {
vec2 p = (gl_FragCoord.xy / uRes) * 2.0 - 1.0;
p.x *= uRes.x / uRes.y;
float t = uTime * (0.4 + uBpm * 0.005);
float turb = 1.0 + uLead * 1.8 + uKick * 1.0;
float v = 0.0;
v += sin(p.x * 4.0 * turb + t * 1.6);
v += sin(p.y * 5.0 * turb - t * 1.3);
v += sin((p.x + p.y) * 3.0 * turb + t * 2.1);
float r = length(p);
v += sin(r * 8.0 - t * 2.0 + uBass * 6.0);
v *= 0.25;
// Bass swells push the hue
float hue = v + t * 0.15 + uBass * 0.3;
vec3 col = palette(hue);
// Pad amplitude → glow toward white
col = mix(col, vec3(1.0), uPad * 0.4 * smoothstep(0.0, 1.5, abs(v)));
// Kick → darker contrast pulse
col *= mix(1.0, 1.6, uKick);
// Soft radial vignette
col *= smoothstep(1.5, 0.2, r);
gl_FragColor = vec4(col, 1.0);
}
+5
View File
@@ -0,0 +1,5 @@
#version 120
uniform mat4 modelViewProjectionMatrix;
void main() {
gl_Position = modelViewProjectionMatrix * gl_Vertex;
}
+163
View File
@@ -0,0 +1,163 @@
#version 120
#extension GL_ARB_texture_rectangle : enable
// Composite post-processing pass. Each effect is gated by an intensity
// uniform — set to 0 to skip its branch. Order matters: spatial /
// kaleido first (work in UV), then color / chromatic / glitch.
uniform sampler2DRect uScene;
uniform sampler2DRect uPrev;
uniform vec2 uRes;
uniform float uTime;
uniform float uChroma;
uniform float uBloom;
uniform float uRgbShift;
uniform float uSat;
uniform float uScan;
uniform float uVignette;
uniform float uGrain;
uniform float uPixelate;
uniform float uKaleido;
uniform float uFeedback;
uniform float uFbZoom;
uniform float uFbRot;
uniform float uGlitch;
uniform float uGlitchProb;
varying vec2 vTex;
const float TAU = 6.2831853;
float rand(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
vec2 rot2(vec2 v, float a) {
float c = cos(a), s = sin(a);
return vec2(c*v.x - s*v.y, s*v.x + c*v.y);
}
vec3 hueRotate(vec3 col, float a) {
// Approximate hue rotation in YIQ space — fast and visually OK
float c1 = cos(a), s1 = sin(a);
mat3 m = mat3(
0.299 + 0.701*c1 + 0.168*s1, 0.587 - 0.587*c1 + 0.330*s1, 0.114 - 0.114*c1 - 0.497*s1,
0.299 - 0.299*c1 - 0.328*s1, 0.587 + 0.413*c1 + 0.035*s1, 0.114 - 0.114*c1 + 0.292*s1,
0.299 - 0.300*c1 + 1.250*s1, 0.587 - 0.588*c1 - 1.050*s1, 0.114 + 0.886*c1 - 0.203*s1
);
return clamp(m * col, 0.0, 1.5);
}
void main() {
vec2 px = gl_FragCoord.xy;
vec2 uv = px;
vec2 c = (px / uRes) - 0.5;
// 1. Pixelate (snap to block centers)
if (uPixelate > 0.001) {
float ps = mix(1.0, 64.0, uPixelate);
uv = floor(uv / ps) * ps + ps * 0.5;
c = (uv / uRes) - 0.5;
}
// 2. Kaleidoscope radial mirror
if (uKaleido > 0.001) {
float n = floor(mix(2.0, 12.0, uKaleido));
float ang = atan(c.y, c.x);
float seg = TAU / n;
ang = abs(mod(ang + seg*0.5, seg) - seg*0.5);
float r = length(c);
c = vec2(cos(ang), sin(ang)) * r;
uv = (c + 0.5) * uRes;
}
// 3. Glitch — block displacement on horizontal slices
vec2 glUv = uv;
if (uGlitch > 0.001) {
float blockY = floor(uv.y / 14.0);
float t = floor(uTime * 24.0);
float r = rand(vec2(blockY, t));
float prob = mix(0.0, uGlitchProb * 2.5, uGlitch);
if (r < prob) {
float shift = (rand(vec2(blockY, t + 1.0)) - 0.5) * 240.0 * uGlitch;
glUv.x += shift;
}
}
glUv = clamp(glUv, vec2(0.0), uRes - 1.0);
// 4. Chromatic aberration : RGB channels sampled at radial offsets
vec2 dir = c * uChroma * 14.0;
vec3 col;
col.r = texture2DRect(uScene, clamp(glUv + dir, vec2(0.0), uRes - 1.0)).r;
col.g = texture2DRect(uScene, glUv).g;
col.b = texture2DRect(uScene, clamp(glUv - dir, vec2(0.0), uRes - 1.0)).b;
// 5. Glitch channel swap
if (uGlitch > 0.55) {
float swap = step(0.5, rand(vec2(floor(uTime * 12.0), 1.0)));
col = mix(col, col.brg, swap);
}
// 6. Bloom — cheap 5x5 bright-pass average added back
if (uBloom > 0.001) {
vec3 bl = vec3(0.0);
float w = 4.0 + uBloom * 12.0;
for (int i = -2; i <= 2; i++) {
for (int j = -2; j <= 2; j++) {
vec3 s = texture2DRect(uScene,
clamp(uv + vec2(i, j) * w, vec2(0.0), uRes - 1.0)).rgb;
float lum = max(max(s.r, s.g), s.b);
bl += s * smoothstep(0.45, 1.0, lum);
}
}
bl /= 25.0;
col += bl * uBloom * 1.6;
}
// 7. Hue rotation
if (uRgbShift > 0.001) {
col = hueRotate(col, uRgbShift * TAU);
}
// 8. Saturation
{
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uSat);
}
// 9. Feedback (zoom + rotate previous frame)
if (uFeedback > 0.001) {
vec2 fc = uv - uRes * 0.5;
fc /= uFbZoom;
fc = rot2(fc, uFbRot);
fc += uRes * 0.5;
fc = clamp(fc, vec2(0.0), uRes - 1.0);
vec3 prev = texture2DRect(uPrev, fc).rgb * 0.96;
col = max(col, prev * uFeedback * 1.5);
}
// 10. Scanlines
if (uScan > 0.001) {
float sl = sin(px.y * 3.14159) * 0.5 + 0.5;
col *= mix(1.0, 0.45 + sl * 0.55, uScan);
}
// 11. Film grain
if (uGrain > 0.001) {
float g = (rand(px + uTime * vec2(13.0, 7.0)) - 0.5) * uGrain * 0.6;
col += vec3(g);
}
// 12. Vignette
if (uVignette > 0.001) {
float v = smoothstep(0.95, 0.35, length(c) * 1.4);
col *= mix(1.0, v, uVignette);
}
// Final tone-map / clamp — without this, bloom + grain + feedback
// saturate to pure white very quickly on bright visualizers.
col = col / (col + vec3(0.6)); // Reinhard-ish soft knee
col = clamp(col, 0.0, 1.0);
gl_FragColor = vec4(col, 1.0);
}
+9
View File
@@ -0,0 +1,9 @@
#version 120
// openFrameworks injects modelViewProjectionMatrix and the standard
// gl_Vertex / gl_MultiTexCoord0 inputs in compatibility mode.
uniform mat4 modelViewProjectionMatrix;
varying vec2 vTex;
void main() {
vTex = gl_MultiTexCoord0.xy;
gl_Position = modelViewProjectionMatrix * gl_Vertex;
}
+65
View File
@@ -0,0 +1,65 @@
#version 120
// Cylindrical pseudo-3D tunnel. Closed-form mapping from screen UV to
// (angle, depth) — no real raymarching needed for an infinite straight
// tunnel. Twist is added per-depth slice based on lead amplitude.
uniform vec2 uRes;
uniform float uTime;
uniform float uTravel;
uniform float uBpm;
uniform float uKick;
uniform float uBass;
uniform float uLead;
uniform float uPad;
vec3 palette(float t) {
return 0.5 + 0.5 * cos(6.28318 * (vec3(1.0) * t + vec3(0.0, 0.33, 0.67)));
}
void main() {
vec2 p = (gl_FragCoord.xy / uRes) * 2.0 - 1.0;
p.x *= uRes.x / uRes.y;
// Polar coords -> tunnel space
float r = length(p);
float a = atan(p.y, p.x);
if (r < 0.001) r = 0.001;
// depth z = 1/r so r→0 is far. Travel scrolls slices.
float z = 1.0 / r + uTravel;
// Twist: deeper = more rotation, modulated by lead
a += z * (0.15 + uLead * 0.6);
// Slice index for repeating bands
float slice = floor(z * 2.0);
float frac = fract(z * 2.0);
// Brick / ring pattern — checker on (slice, angle)
float ang = a / 6.28318 * 16.0;
float wall = mod(floor(ang) + slice, 2.0);
// Base color cycles along depth + slight bass push
vec3 col = palette(slice * 0.07 + uTime * 0.05 + uBass * 0.2);
// Bright "lamps" every 4 slices
float lampMask = step(0.92, fract(slice / 4.0));
float lamp = (1.0 - smoothstep(0.0, 0.4, abs(frac - 0.5))) * lampMask;
col += vec3(1.0, 0.9, 0.7) * lamp * (0.6 + uKick * 1.5);
// Wall vs floor stripes alternate brightness
col *= mix(0.45, 1.0, wall);
// Add edges between bricks (subtle dark line)
float edge = smoothstep(0.0, 0.05, abs(frac - 0.5));
col *= mix(0.7, 1.0, edge);
// Distance fog : pixels close to center (far away) fade to background
float fog = smoothstep(0.0, 0.85, r);
col = mix(vec3(0.0, 0.0, 0.05 + uPad * 0.15), col, fog);
// Outer ring vignette
col *= smoothstep(2.0, 0.4, length(p));
gl_FragColor = vec4(col, 1.0);
}
+3
View File
@@ -0,0 +1,3 @@
#version 120
uniform mat4 modelViewProjectionMatrix;
void main() { gl_Position = modelViewProjectionMatrix * gl_Vertex; }
+1 -1
View File
@@ -14,7 +14,7 @@ public:
struct Params {
// Color
float chroma = 0.0f; // chromatic aberration radius
float bloom = 0.4f; // bright-pass blur strength
float bloom = 0.15f; // bright-pass blur strength (was 0.4 — saturated)
float rgbShift = 0.0f; // hue rotation 0..1 = 0..2π
float saturation = 1.0f; // 0..2 (1 = neutral)
+26
View File
@@ -46,6 +46,7 @@ const char* modeName(ofApp::Mode m) {
case ofApp::Mode::Kaleido: return "Kaleido";
case ofApp::Mode::Tunnel: return "Tunnel 3D";
case ofApp::Mode::Mesh: return "Mesh 3D";
case ofApp::Mode::Scope4: return "Scope 4-up";
case ofApp::Mode::Hybrid: return "Hybrid";
}
return "?";
@@ -100,6 +101,10 @@ void ofApp::setup() {
gui_.add(gainCh2_.setup("CH2 V/div", 1.0f, 0.25f, 5.0f));
gui_.add(trailFade_.setup("Trail fade", 0.07f, 0.0f, 1.0f));
gui_.add(sampleRateHz_.setup("Sample rate", 8000000, 1000000, 48000000));
gui_.add(timeMsPerDiv_.setup("Time ms/div", 5.0f, 0.05f, 200.0f));
gui_.add(scrollSpeed_.setup("Scroll", 0.0f, 0.0f, 1.0f));
lastSampleRateApplied_ = sampleRateHz_;
scope_.setSampleRate(static_cast<uint32_t>(lastSampleRateApplied_));
// PostFx panel — placed to the right of the main GUI
fxGui_.setup("post-fx", "fx-settings.xml", 230, 10);
@@ -145,6 +150,8 @@ void ofApp::loadSettings() {
else if (m == "kaleido") mode_ = Mode::Kaleido;
else if (m == "tunnel") mode_ = Mode::Tunnel;
else if (m == "mesh") mode_ = Mode::Mesh;
else if (m == "scope4" ||
m == "quad") mode_ = Mode::Scope4;
else mode_ = Mode::Hybrid;
}
@@ -183,6 +190,15 @@ void ofApp::applyOscFx() {
void ofApp::update() {
osc_.update();
// Re-apply sample rate to the scope when the slider changes.
if (sampleRateHz_ != lastSampleRateApplied_) {
lastSampleRateApplied_ = sampleRateHz_;
scope_.setSampleRate(static_cast<uint32_t>(lastSampleRateApplied_));
}
// Push live timebase + scroll into WaveformVis.
waveform_->setTimeMsPerDiv(timeMsPerDiv_);
waveform_->setScrollSpeed(scrollSpeed_);
waveform_->setSampleRate(static_cast<float>(lastSampleRateApplied_));
scope_.ring().readLatest(ch1_, ch2_, static_cast<std::size_t>(bufferSize_));
oscope::VisFrame frame{ch1_, ch2_, osc_};
@@ -241,6 +257,15 @@ void ofApp::drawMode(Mode m, int x, int y, int w, int h) {
case Mode::Kaleido: kaleido_->draw(x, y, w, h); break;
case Mode::Tunnel: tunnel_->draw(x, y, w, h); break;
case Mode::Mesh: mesh_->draw(x, y, w, h); break;
case Mode::Scope4: {
const int hw = w / 2;
const int hh = h / 2;
waveform_->draw (x, y, hw, hh);
spectro_->draw (x + hw, y, hw, hh);
polar_->draw (x, y + hh, hw, hh);
lissajous_->draw(x + hw, y + hh, hw, hh);
break;
}
case Mode::Hybrid: drawHybrid(w, h); break;
}
}
@@ -364,6 +389,7 @@ void ofApp::keyPressed(int key) {
case '9': mode_ = Mode::Kaleido; break;
case '0': mode_ = Mode::Tunnel; break;
case '-': mode_ = Mode::Mesh; break;
case 'q': mode_ = Mode::Scope4; break;
case 'f':
fullscreen_ = !fullscreen_;
ofSetFullscreen(fullscreen_);
+4
View File
@@ -32,6 +32,7 @@ public:
Lissajous, Spectrogram, Reactive, Waveform,
Polar, Plasma, Particles, Kaleido,
Tunnel, Mesh,
Scope4, // waveform + spectrogram + polar pulsing + lissajous
Hybrid
};
@@ -76,6 +77,9 @@ private:
ofxFloatSlider gainCh1_, gainCh2_;
ofxFloatSlider trailFade_;
ofxIntSlider sampleRateHz_;
ofxFloatSlider timeMsPerDiv_;
ofxFloatSlider scrollSpeed_;
int lastSampleRateApplied_ = 0;
ofxLabel scopeStatusLabel_;
ofxLabel oscStatusLabel_;
ofxLabel modeLabel_;
+40 -10
View File
@@ -9,6 +9,11 @@ void WaveformVis::setup(int w, int h) {
h_ = h;
trace1_.assign(syntheticSize_, 0.0f);
trace2_.assign(syntheticSize_, 0.0f);
// Ring d'historique : ~16x la fenêtre pour pouvoir scroller en arrière
// sans trou même avec un timebase lent.
ring1_.assign(syntheticSize_ * 16, 0.0f);
ring2_.assign(syntheticSize_ * 16, 0.0f);
ringHead_ = 0;
}
void WaveformVis::update(const VisFrame& frame) {
@@ -22,16 +27,38 @@ void WaveformVis::update(const VisFrame& frame) {
}
beatPhase_ *= 0.92f;
// Si Hantek a fourni des samples, on les copie tels quels
// Si Hantek a fourni des samples, on alimente le ring d'historique
if (frame.ch1.size() > 16) {
std::size_t n = std::min(frame.ch1.size(), trace1_.size());
const std::size_t cap = ring1_.size();
const std::size_t n = std::min(frame.ch1.size(), cap);
for (std::size_t i = 0; i < n; ++i) {
trace1_[i] = frame.ch1[i];
trace2_[i] = (i < frame.ch2.size()) ? frame.ch2[i] : 0.0f;
ring1_[ringHead_] = frame.ch1[i];
ring2_[ringHead_] = (i < frame.ch2.size()) ? frame.ch2[i] : 0.0f;
ringHead_ = (ringHead_ + 1) % cap;
}
if (n < trace1_.size()) {
std::fill(trace1_.begin() + n, trace1_.end(), 0.0f);
std::fill(trace2_.begin() + n, trace2_.end(), 0.0f);
// Mode FREEZE — scrollSpeed_ ~ 0 : on n'actualise pas la fenêtre
// affichée, le trace gèle tel qu'il était (utile pour analyser).
if (scrollSpeed_ < 0.01f) return;
// Largeur de la fenêtre = timebase × divisions, convertie en samples.
// (sampleRate * (ms/div × divX) / 1000)
const float winSec = (timeMsPerDiv_ * 0.001f) * divX_;
std::size_t winSamples = static_cast<std::size_t>(
std::max(64.0f, std::min(static_cast<float>(cap),
winSec * sampleRateHz_)));
winSamples = std::min(winSamples, trace1_.size());
// Position de lecture : tail = head - winSamples (mode freeze) ou
// tail décalé pour un effet de scroll continu.
const std::size_t off = (cap + ringHead_ - winSamples) % cap;
for (std::size_t i = 0; i < trace1_.size(); ++i) {
const float t = static_cast<float>(i) /
static_cast<float>(trace1_.size() - 1);
const std::size_t k = static_cast<std::size_t>(t * (winSamples - 1));
const std::size_t idx = (off + k) % cap;
trace1_[i] = ring1_[idx];
trace2_[i] = ring2_[idx];
}
return;
}
@@ -119,11 +146,14 @@ void WaveformVis::draw(int x, int y, int w, int h) {
ofSetColor(160, 220, 180);
const float vdiv1 = 0.5f;
const float vdiv2 = 0.5f;
const float tdiv = 5.0f; // ms/div approximatif
ofDrawBitmapString("CH1 " + ofToString(vdiv1, 2) + " V/div", x + 10, y + 16);
ofDrawBitmapString("CH2 " + ofToString(vdiv2, 2) + " V/div", x + 10, y + 32);
ofDrawBitmapString("Time " + ofToString(tdiv, 1) + " ms/div", x + 10, y + 48);
ofDrawBitmapString("Trig auto", x + 10, y + 64);
ofDrawBitmapString("Time " + ofToString(timeMsPerDiv_, 2) + " ms/div",
x + 10, y + 48);
ofDrawBitmapString(scrollSpeed_ < 0.01f ? "FROZEN" : "ROLL",
x + 10, y + 64);
ofDrawBitmapString("Sr " + ofToString(sampleRateHz_ * 1e-6f, 1) + " MS/s",
x + 10, y + 80);
ofPopMatrix();
ofPopStyle();
+20
View File
@@ -7,6 +7,9 @@
#include "Visualizer.h"
#include "ofMain.h"
#include <algorithm>
#include <vector>
namespace oscope {
class WaveformVis : public Visualizer {
@@ -15,16 +18,33 @@ public:
void update(const VisFrame& frame) override;
void draw(int x, int y, int w, int h) override;
// Live tweaks (driven by GUI sliders / OSC).
void setTimeMsPerDiv(float v) { timeMsPerDiv_ = std::max(0.05f, v); }
void setScrollSpeed(float v) { scrollSpeed_ = std::max(0.0f, std::min(1.0f, v)); }
void setSampleRate(float hz) { if (hz > 1.0f) sampleRateHz_ = hz; }
private:
int w_ = 0, h_ = 0;
float phase_ = 0.0f;
float beatPhase_ = 0.0f;
int prevBeat_ = 0;
// trace1_/trace2_ : fenêtre actuelle affichée (taille fixe)
std::vector<float> trace1_;
std::vector<float> trace2_;
// Ring d'historique pour le scrolling (plus long, on lit la queue)
std::vector<float> ring1_;
std::vector<float> ring2_;
std::size_t ringHead_ = 0;
int syntheticSize_ = 2048;
float divX_ = 8.0f; // grid divisions
float divY_ = 8.0f;
// Time base — combien de millisecondes représente une division horizontale.
float timeMsPerDiv_ = 5.0f;
// 0 = freeze (capture par trame), 1 = scroll continu (oscilloscope rolling).
float scrollSpeed_ = 0.0f;
// Hz — utilisé pour convertir ms/div en samples/div.
float sampleRateHz_ = 8'000'000.0f;
};
} // namespace oscope