From 7191f2eb2e1c48324d4d8feb414f55e7b95c94f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Thu, 7 May 2026 17:01:51 +0200 Subject: [PATCH] feat: HUD album/track + Control/Hydra deeplinks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: the user noticed the auto-launched browser tab landed on the landing page (just two cards saying 'Control' / 'Hydra') instead of the control surface with all 11 tabs (Transport, Album, Mixer, Steps, Acid, Harmony, Kicks, FX, Sends, LFO, Tricks). And while sclang was emitting /sync/album, /sync/melody, /sync/synthdef, oscope-of received them but never showed them — the HUD only had FPS + mode. Approach: launcher's auto-open now goes straight to /control/, with a second 'Hydra' button in the popover for the visual canvas page. In oscope-of, drawHud() gets a top-left info box that reads OscClient state and prints ALBUM / MELODY / SYNTH / BPM / BEAT lines plus a beat-pulse dot driven by kick amplitude. Box is drawn semi-transparent so it survives the post-fx chain without blocking the visualizers. Changes: - launcher/ProcessManager.swift : * autoOpenBrowser now opens /control/ instead of / * new openHydra() method for the second deeplink - launcher/MenuBarContent.swift : the single 'Open in browser' button becomes two — 'Control' (slider icon) and 'Hydra' (waveform icon), both disabled until the web server is running - oscope-of/ofApp.cpp drawHud() : adds top-left info box with the five lines from osc_.album()/melody()/synthdef()/bpm()/beat(), semi-transparent black backdrop with a dim cyan border, beat dot in the top-right corner of the box that pulses with kick amp. Empty-string fields are skipped so the box is small at startup before sclang has emitted anything Impact: the operator now sees the live state of the sound_algo engine overlaid on the visualizer at all times, without switching to the browser. The browser tab opens directly on the working surface. Smoke-tested end-to-end on the Big Sur Intel MBP. TODO from this session's question — not done yet, follow-up commits : - SwiftUI menubar submenu listing albums and tracks - Album sequencer / playlist mode - MIDI keyboard / pad mapping to ~kk / ~mm / ~ff / ~cc / ~p - Hantek 6022BL USB driver wiring (firmware + entitlements) --- .../AVLiveLauncher/MenuBarContent.swift | 6 ++- .../AVLiveLauncher/ProcessManager.swift | 10 ++++- oscope-of/src/ofApp.cpp | 43 +++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/launcher/Sources/AVLiveLauncher/MenuBarContent.swift b/launcher/Sources/AVLiveLauncher/MenuBarContent.swift index 8f3163a..8ed7efa 100644 --- a/launcher/Sources/AVLiveLauncher/MenuBarContent.swift +++ b/launcher/Sources/AVLiveLauncher/MenuBarContent.swift @@ -38,7 +38,11 @@ struct MenuBarContent: View { HStack { Button(action: processManager.openBrowser) { - Label("Open in browser", systemImage: "safari") + Label("Control", systemImage: "slider.horizontal.3") + } + .disabled(!processManager.webRunning) + Button(action: processManager.openHydra) { + Label("Hydra", systemImage: "waveform") } .disabled(!processManager.webRunning) Spacer() diff --git a/launcher/Sources/AVLiveLauncher/ProcessManager.swift b/launcher/Sources/AVLiveLauncher/ProcessManager.swift index 6ab8219..9ca969c 100644 --- a/launcher/Sources/AVLiveLauncher/ProcessManager.swift +++ b/launcher/Sources/AVLiveLauncher/ProcessManager.swift @@ -114,7 +114,7 @@ final class ProcessManager: ObservableObject { if autoOpenBrowser { DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { [weak self] in guard let self = self else { return } - if let url = URL(string: "http://localhost:\(self.webPort)/") { + if let url = URL(string: "http://localhost:\(self.webPort)/control/") { NSWorkspace.shared.open(url) } } @@ -167,7 +167,13 @@ final class ProcessManager: ObservableObject { } func openBrowser() { - if let url = URL(string: "http://localhost:\(webPort)/") { + if let url = URL(string: "http://localhost:\(webPort)/control/") { + NSWorkspace.shared.open(url) + } + } + + func openHydra() { + if let url = URL(string: "http://localhost:\(webPort)/hydra/") { NSWorkspace.shared.open(url) } } diff --git a/oscope-of/src/ofApp.cpp b/oscope-of/src/ofApp.cpp index 5380a89..e5d789b 100644 --- a/oscope-of/src/ofApp.cpp +++ b/oscope-of/src/ofApp.cpp @@ -291,12 +291,55 @@ void ofApp::draw() { void ofApp::drawHud() { ofPushStyle(); + + // Bottom-left : FPS + mode ofSetColor(180, 220, 200, 220); ofDrawBitmapString("FPS: " + ofToString(ofGetFrameRate(), 1), 12, ofGetHeight() - 28); ofDrawBitmapString(std::string("Mode: ") + modeName(mode_) + (postFxEnabled_ ? " [fx ON]" : " [fx OFF]"), 12, ofGetHeight() - 12); + + // Top-left : sound_algo state from /sync/* (album, melody, synthdef, + // bpm, beat). Boxed background so it stays readable over any visual. + const std::string& album = osc_.album(); + const std::string& melody = osc_.melody(); + const std::string& synthdef = osc_.synthdef(); + const float bpm = osc_.bpm(); + const int beat = osc_.beat(); + const float kick = osc_.amp("kick"); + + std::vector lines; + if (!album.empty()) lines.push_back("ALBUM " + album); + if (!melody.empty()) lines.push_back("MELODY " + melody); + if (!synthdef.empty()) lines.push_back("SYNTH " + synthdef); + lines.push_back("BPM " + ofToString(bpm, 1) + + " BEAT " + ofToString(beat)); + + if (!lines.empty()) { + const int padding = 8; + const int lineH = 14; + const int boxW = 280; + const int boxH = static_cast(lines.size()) * lineH + padding * 2; + ofSetColor(0, 0, 0, 150); + ofDrawRectangle(12, 12, boxW, boxH); + ofSetColor(140, 200, 255, 60); + ofNoFill(); + ofDrawRectangle(12, 12, boxW, boxH); + ofFill(); + for (std::size_t i = 0; i < lines.size(); ++i) { + ofSetColor(220, 230, 240, 230); + ofDrawBitmapString(lines[i], + 12 + padding, + 12 + padding + 11 + static_cast(i) * lineH); + } + // Beat indicator dot pulses on each beat (uses kick amp as proxy) + const float pulse = std::min(1.0f, kick * 2.0f); + ofSetColor(255, 180, 80, static_cast(120 + pulse * 135.0f)); + ofDrawCircle(12 + boxW - padding - 6, 12 + padding + 6, + 3.0f + pulse * 4.0f); + } + ofPopStyle(); }