feat(of): scope4 tunnel center + spectro ring

Layout retravaillé pour le mode Scope4 :

- Tunnel fullscreen en fond, focal central. Ses uniformes sont
  pilotés par les fréquences :
  - uTileZ (densité Z) : bass loud → grosses tuiles
  - uTileX (angulaire) : lead loud → tuiles fines
  - uDirection : signe -1..+1 lerp lente vers (lead-bass), inverse
    sens de défilement et twist
- Spectrogramme circulaire colorisé centré : anneau de barres
  FFT, hue bleu (basses) → rouge (aigus), magnitude = longueur
  radiale. Mapping log pour étaler les basses.
- 3 satellites (Waveform, Polar, Lissajous) en orbite autour du
  centre, position tournante (vitesse BPM-driven). Chaque
  satellite a son label live.
- Label "TUNNEL" central avec BPM + balance LF/HF.
This commit is contained in:
L'électron rare
2026-05-07 19:34:13 +02:00
parent 3a28da823e
commit 1b8d9b89d0
6 changed files with 124 additions and 39 deletions
+14 -7
View File
@@ -14,6 +14,13 @@ uniform float uKick;
uniform float uBass;
uniform float uLead;
uniform float uPad;
// Pilotage par fréquence :
// uTileZ — bass loud → grosses tuiles axe Z (valeur basse, défaut 2)
// uTileX — lead loud → tuiles fines axe angulaire (valeur haute, défaut 16)
// uDirection — signe du défilement / twist (-1..+1)
uniform float uTileZ;
uniform float uTileX;
uniform float uDirection;
vec3 palette(float t) {
return 0.5 + 0.5 * cos(6.28318 * (vec3(1.0) * t + vec3(0.0, 0.33, 0.67)));
@@ -30,15 +37,15 @@ void main() {
// 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);
// Twist: deeper = more rotation, modulated by lead, signé par direction
a += z * (0.15 + uLead * 0.6) * uDirection;
// Slice index for repeating bands
float slice = floor(z * 2.0);
float frac = fract(z * 2.0);
// Slice index — densité Z pilotée par bass (uTileZ).
float slice = floor(z * uTileZ);
float frac = fract(z * uTileZ);
// Brick / ring pattern — checker on (slice, angle)
float ang = a / 6.28318 * 16.0;
// Brick / ring pattern — densité angulaire pilotée par lead (uTileX).
float ang = a / 6.28318 * uTileX;
float wall = mod(floor(ang) + slice, 2.0);
// Base color cycles along depth + slight bass push
+39 -29
View File
@@ -314,41 +314,51 @@ void ofApp::drawPanelLabel(int x, int y, const char* title,
}
void ofApp::drawScope4(int W, int H) {
// Lissajous fullscreen en fond — additif pour ne pas masquer.
ofEnableBlendMode(OF_BLENDMODE_ADD);
ofPushStyle();
ofSetColor(255, 255, 255, 110);
lissajous_->draw(0, 0, W, H);
ofPopStyle();
ofDisableBlendMode();
// 1) Tunnel fullscreen en fond — focal central, fréquences pilotent
// tile size / direction / vitesse (cf. TunnelVis).
tunnel_->draw(0, 0, W, H);
// Bandeau haut : Waveform à gauche, Spectrogramme à droite (1/3 hauteur)
const int topH = H / 3;
const int hw = W / 2;
waveform_->draw(0, 0, hw, topH);
spectro_->draw (hw, 0, hw, topH);
// 2) Spectrogramme circulaire colorisé par fréquence, centré.
// Anneau de barres FFT, hue bleu→rouge, magnitude = longueur radiale.
const float cx = W * 0.5f;
const float cy = H * 0.5f;
const float ringInner = std::min(W, H) * 0.18f;
const float ringOuter = std::min(W, H) * 0.32f;
spectro_->drawCircular(cx, cy, ringInner, ringOuter);
// Polar central, grand — couvre les 2/3 inférieurs centrés.
const int polarW = static_cast<int>(W * 0.62f);
const int polarH = static_cast<int>((H - topH) * 0.95f);
const int polarX = (W - polarW) / 2;
const int polarY = topH + ((H - topH) - polarH) / 2;
polar_->draw(polarX, polarY, polarW, polarH);
// Labels live par panneau (coin haut-gauche de chaque zone).
// 3) Trois satellites en orbite autour du tunnel — leur position
// tourne lentement en suivant le BPM. Chacun garde son rendu
// interne (pas de rotation du contenu, juste de la position).
const float bpm = osc_.bpm();
const float orbit = ofGetElapsedTimef() * (0.04f + bpm * 0.0003f);
const float orbitR = std::min(W, H) * 0.40f;
const int satW = static_cast<int>(W * 0.24f);
const int satH = static_cast<int>(H * 0.20f);
struct Satellite { Visualizer* vis; const char* tag; std::string metric; };
const float kick = osc_.amp("kick");
const float lead = osc_.amp("lead");
const float bass = osc_.amp("bass");
drawPanelLabel(8, 6, "WAVE ",
ofToString(static_cast<float>(timeMsPerDiv_), 2) + " ms/div " +
ofToString(lastSampleRateApplied_ * 1e-6f, 1) + " MS/s");
drawPanelLabel(hw + 8, 6, "SPECTRO",
"lead " + ofToString(lead, 2) + " bass " + ofToString(bass, 2));
drawPanelLabel(polarX + 8, polarY + 6, "POLAR ",
ofToString(bpm, 1) + " bpm kick " + ofToString(kick, 2));
drawPanelLabel(8, H - 28, "LISSA ",
"trail blend ADD");
Satellite sats[3] = {
{waveform_.get(), "WAVE ",
ofToString(static_cast<float>(timeMsPerDiv_), 2) + " ms"},
{polar_.get(), "POLAR ",
ofToString(bpm, 0) + " bpm k" + ofToString(kick, 1)},
{lissajous_.get(), "LISSA ",
"L" + ofToString(lead, 1) + " B" + ofToString(bass, 1)},
};
for (int i = 0; i < 3; ++i) {
const float a = orbit + i * (TWO_PI / 3.0f);
const int sx = static_cast<int>(cx + std::cos(a) * orbitR - satW * 0.5f);
const int sy = static_cast<int>(cy + std::sin(a) * orbitR - satH * 0.5f);
sats[i].vis->draw(sx, sy, satW, satH);
drawPanelLabel(sx + 6, sy + 6, sats[i].tag, sats[i].metric);
}
// 4) Label central : tunnel infos
drawPanelLabel(static_cast<int>(cx) - 80, 6, "TUNNEL",
ofToString(bpm, 1) + " bpm dir " +
ofToString((osc_.amp("lead") - osc_.amp("bass")), 2));
}
void ofApp::draw() {
@@ -68,4 +68,50 @@ void SpectrogramVis::draw(int x, int y, int w, int h) {
ofPopStyle();
}
void SpectrogramVis::drawCircular(int cx, int cy, float innerR, float outerR) {
if (mag_.empty()) return;
ofPushStyle();
ofPushMatrix();
ofTranslate(cx, cy);
// Anneau central — chaque bin FFT occupe un secteur angulaire,
// longueur radiale = magnitude, hue = bin (basses bleu → aigus rouge).
const std::size_t bins = mag_.size();
// On limite au quart bas (~Nyquist/4) car la moitié haute est très
// sparse et brouille le rendu.
const std::size_t shown = bins / 4;
const float step = TWO_PI / static_cast<float>(shown);
ofSetLineWidth(2);
for (std::size_t i = 0; i < shown; ++i) {
const float a = i * step;
// Mapping log : on remap i sur le spectre log pour étaler les basses.
const float t = std::pow(static_cast<float>(i) / shown, 0.55f);
const std::size_t bin = static_cast<std::size_t>(t * (bins - 1));
const float magdb = 20.0f * std::log10(std::max(mag_[bin], 1e-6f)) + 60.0f;
const float v = ofClamp(magdb / 60.0f, 0.0f, 1.0f);
// Hue colorisée par fréquence : 220 (bleu) → 0 (rouge) en hue HSB
const float hue = 220.0f - 220.0f * t;
ofColor col;
col.setHsb(hue, 200.0f, 80.0f + 175.0f * v);
col.a = static_cast<unsigned char>(120 + 135 * v);
ofSetColor(col);
const float r0 = innerR;
const float r1 = innerR + (outerR - innerR) * v;
const float ca = std::cos(a);
const float sa = std::sin(a);
ofDrawLine(ca * r0, sa * r0, ca * r1, sa * r1);
}
// Anneau interne discret pour cadrer
ofNoFill();
ofSetLineWidth(1);
ofSetColor(60, 100, 140, 120);
ofDrawCircle(0, 0, innerR);
ofPopMatrix();
ofPopStyle();
}
} // namespace oscope
@@ -15,6 +15,9 @@ public:
void setup(int w, int h) override;
void update(const VisFrame& frame) override;
void draw(int x, int y, int w, int h) override;
/// Render circulaire : anneau de barres FFT centré, couleur par bande
/// (bleu = basses, vert = mediums, rouge = aigus). Utilisé en Scope4.
void drawCircular(int cx, int cy, float innerR, float outerR);
private:
ofFbo fbo_;
+19 -1
View File
@@ -12,7 +12,17 @@ void TunnelVis::update(const VisFrame& frame) {
bass_ = frame.osc.amp("bass");
lead_ = frame.osc.amp("lead");
pad_ = frame.osc.amp("pad");
travel_ += static_cast<float>(ofGetLastFrameTime()) * (0.6f + bpm_ * 0.012f + kick_ * 1.5f);
// Direction : lerp lente vers +1 si la balance HF (lead) > LF (bass),
// -1 sinon. Le signe contrôle le sens de défilement et le sens du twist.
const float dirTarget = (lead_ - bass_) > 0.05f ? 1.0f
: (bass_ - lead_) > 0.05f ? -1.0f
: direction_;
direction_ += (dirTarget - direction_) * 0.04f;
// Vitesse : base BPM + boost sur kick, signée par direction_.
const float speed = (0.6f + bpm_ * 0.012f + kick_ * 1.5f) * direction_;
travel_ += static_cast<float>(ofGetLastFrameTime()) * speed;
}
void TunnelVis::draw(int x, int y, int w, int h) {
@@ -34,6 +44,14 @@ void TunnelVis::draw(int x, int y, int w, int h) {
shader_.setUniform1f("uBass", bass_);
shader_.setUniform1f("uLead", lead_);
shader_.setUniform1f("uPad", pad_);
// Taille de tile pilotée par les fréquences :
// - axe Z (profondeur) : bass loud → grosses tuiles (uTileZ bas)
// - axe angulaire : lead loud → tuiles fines (uTileX haut)
const float tileZ = ofLerp(3.0f, 0.5f, std::min(1.0f, bass_));
const float tileX = ofLerp(8.0f, 32.0f, std::min(1.0f, lead_));
shader_.setUniform1f("uTileZ", tileZ);
shader_.setUniform1f("uTileX", tileX);
shader_.setUniform1f("uDirection", direction_);
ofDrawRectangle(x, y, w, h);
shader_.end();
}
+3 -2
View File
@@ -17,8 +17,9 @@ public:
void reloadShaders() override;
private:
int w_ = 0, h_ = 0;
float travel_ = 0.0f;
int w_ = 0, h_ = 0;
float travel_ = 0.0f;
float direction_ = 1.0f; // smooth -1..+1 (lead-bass driven)
float bpm_ = 120.0f, kick_ = 0.0f, bass_ = 0.0f, lead_ = 0.0f, pad_ = 0.0f;
ofShader shader_;
};