feat: HUD album/track + Control/Hydra deeplinks

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)
This commit is contained in:
L'électron rare
2026-05-07 17:01:51 +02:00
parent 81450b74db
commit 7191f2eb2e
3 changed files with 56 additions and 3 deletions
@@ -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()
@@ -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)
}
}
+43
View File
@@ -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<std::string> 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<int>(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<int>(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<int>(120 + pulse * 135.0f));
ofDrawCircle(12 + boxW - padding - 6, 12 + padding + 6,
3.0f + pulse * 4.0f);
}
ofPopStyle();
}