Legacy SwiftPM target archived to launcher/_archive-AV-Live-Body/.
New native Xcode app lives at avlivebody-mac/; no swift run path.
startBodyApp now logs + no-ops with FIXME(rewrite-2026-05-18).
Web data-only fully wired contre les 20 feeds :
dashboard.js : 8 nouveaux handlers (gdelt, wikimedia, tides,
moon, atc, pose, mempool, github, rte_eco2mix). Sparklines +
seuils alert/warn/green coherents.
map.js : marker GDELT (purple) en plus des seismes/foudre/
avions/ISS/volcans, TTL 60s comme les autres ephemeres.
control.html : nouvelle section 'Modes visuels (oF / Metal)'
avec 9 boutons (storm/tunnel/plasma/kaleido/voronoi/metaballs/
stars/bars/hands3d). Le handler scene-btn est splitte par
container (#scenes vs #vizmodes) pour ne pas melanger
l'etat actif.
server.js : whitelist /control/viz* + routing par path. Les
/control/viz* partent vers oF (:57123), tout le reste vers
SC (:57121). Nouveau env OF_PORT_OUT.
Audit fixes :
- LaunchMode displayName Data-only : 'SC + oF + feeds, no web'
-> 'SC + Metal viz + feeds + web' (l'ancien libelle datait
d'avant le toggle Web UI dans dataOnly).
- SettingsPanel : toggle Squelette etiquete '(a venir)' tant
que le rendu n'est pas implemente cote MeshRenderer.
Pipeline web data-only complet :
Cote feeds, OSC target :57124 ajoute au profil data-only.toml
pour que le bridge.py diffuse vers : SC (57121) + oF (57123)
+ Web (57124).
Cote web (data_only_viz/web/) :
- server.js : Express :3211 + WS broadcast + osc UDP :57124
parse les paths /data/<feed>/<sub> en JSON et diffuse
- public/dashboard.html : grille de cards live avec sparklines
SVG vanilla (USGS, SWPC Kp/wind/Bz/X-ray, Blitz, OpenSky,
Bluesky, meteo, air, ISS, volcans, social_buzz, grid)
- public/map.html : Leaflet dark fullscreen avec markers
ephemeres (60s TTL) pour seismes/foudre/avions/volcans +
marker ISS persistant qui suit la position
Cote launcher :
- ProcessManager : startDataWeb / stopDataWeb + openDataDashboard
/ openDataMap, port :3211 hardcode, script attendu a
metalVizDir/web/server.js
- MenuBarContent : nouvelle row 'Dashboard data-only' en mode
.dataOnly et .bodyMesh + boutons Dashboard/Carte monde quand
le serveur tourne
- stopAll inclut dataWebProc
Ajoute une ProcessRow AV-Live-Body toujours visible dans le
menubar (sauf en mode bodyMesh ou la ligne est deja affichee).
L'utilisateur peut maintenant lancer la fenetre RealityKit a la
demande depuis n'importe quel mode pour acceder au panel de
reglages visuels (touche S).
Le guard 'useMultiHMR' dans startBodyApp est supprime : sans
worker Multi-HMR la fenetre affichera juste la cam et un fond
vide pour le mesh, mais le panel reste utilisable pour tester
les sliders / toggles.
- bodyMesh keeps SC + data_feeds (audio + sonification) in addition
to Multi-HMR worker headless + AV-Live-Body. Only oscope-of and
the web UI are stopped.
- dataOnly now keeps the web UI (the user wants Hydra/control panel
available even in the Metal-viz-only mode). Only oscope-of is
stopped on transition.
- MenuBar shows Web UI row in both .full and .dataOnly, and a
Data Feeds row in all three modes (with a distinct subtitle in
bodyMesh that flags it as sonification-only).
- ModePicker subtitles updated to reflect the new contract.
A third LaunchMode (.bodyMesh) joins .full and .dataOnly. It runs
the minimal pipeline : Multi-HMR worker in headless mode + the
Swift AV-Live-Body app, nothing else. No SuperCollider, no
oscope-of, no data feeds, no web UI.
applyModeTransition tears down everything else when switching in
and forces useMultiHMR = true so startMetalViz takes the headless
+ AV-Live-Body branch.
MenuBarContent renders a focused two-row layout in this mode :
'Multi-HMR worker' + 'AV-Live-Body'. ModePickerWindow gets a
pink figure.walk card alongside the two existing ones.
ProcessManager gains a 'useMultiHMR' UserDefault and a 'AV-Live-Body'
process pair (startBodyApp / stopBodyApp). When useMultiHMR is on,
startMetalViz appends '--multi-hmr' to the Python args and, 0.8 s
later, launches the SwiftPM AV-Live-Body executable from
launcher/AV-Live-Body so the RealityKit listener is ready when the
TCP sender connects on :57130.
MenuBarContent adds a Switch under Metal Viz (visible only in
data-only mode) plus a row for AV-Live-Body once it is running.
stopAll terminates the body app along with the rest of the stack.
- Implemented Hydra visualizations in `web_realart/public/hydra/app.js` and `index.html`.
- Created WebGL visualizer using Three.js and WebGPU in `web_realart/public/webgl/app.js` and `index.html`.
- Developed main landing page in `web_realart/public/index.html` to showcase different visualizations.
- Set up WebSocket server in `web_realart/server.js` to relay OSC messages to web clients.
- Added health check API endpoint for monitoring server status.
Context: live performance needs immediate, single-key triggers for the
common moves (intro / drop / breakdown / fills / mute a voice / random
tweak), AND mid-set the operator may need to reboot just the Node web
server without touching sclang or the audio engine. The previous
commit added scsynth and sclang reboots but not web.
Approach: a new Live tab dedicated to performance. 30 pads laid out
on a 10x3 grid that mirrors the QWERTY rows physically — top row
(qwertyuiop) = chains, home row (asdfghjkl;) = one-shots, bottom row
(zxcvbnm,./) = mutes + FX. Each pad shows a big label, a small kind
hint and the bound key in a kbd-style badge at the bottom right.
When the Live tab is active a capture-phase keydown listener wins
against the global keymap, so 'r' fires Buildup ×4 instead of
'refresh catalogues' etc. — non-Live tabs keep the global behavior.
Mute pads are stateful : they toggle /control/mute and /control/unmute
based on a local Set, and visually invert (red, 🔇 prefix) when active.
The ⟳ web button writes ~/.av-live-restart-web ; the launcher's
sentinel watcher restarts the Node server without touching sclang.
Changes:
- web/public/control/index.html :
* new tab Live between Transport and Album
* new tabpane #livePads (filled by JS), with header hint listing the
three sections
* three reboot buttons in the topbar (⟳ sc small / ⟳ web small / ⟳ sclang big)
- web/public/control/app.js :
* LIVE_PADS array (30 entries, key/label/sub/kind/arg/voice)
* firePad(pad, padEl) dispatcher : chain / playAll / stopAll / synth
/ fn / tap / ping / unsolo / muteToggle
* initLivePads() builds the 30 buttons, attaches click + capture-phase
keydown listener that only fires when Live tab is active, with
.hit pulse animation feedback
* makeArmedButton binding for #btnRebootWeb sending /control/rebootWeb
- web/public/control/style.css :
* .live-grid 10-col CSS grid (5-col under 1200px, 2-col under 720px)
* .live-pad with hover/active microinteractions and section-coloured
.live-pad-{chain,shot,mute} variants
* .live-pad.muted state (red tint, 🔇 prefix)
* .live-pad-key kbd-style badge bottom-right
- web_bridge.scd :
* /control/rebootWeb : writes ~/.av-live-restart-web sentinel
* /control/triggerSynth <name> : safe Synth(<name>, [\decay 2, \amp 0.5])
* /control/snareFill : ~snareFill.value
* /control/tomFill : ~tomFill.value
* /control/randomTweak : ~randomTweak.value
- launcher/ProcessManager.swift :
* webWantsRestart flag + restartWeb() helper
* web terminationHandler now respawns 0.4s later if wantsRestart
* startSentinelWatcher() now also watches ~/.av-live-restart-web
Impact: switch to the Live tab and trigger any of the 30 commands by
clicking or by tapping the corresponding key on the keyboard. Mute
toggles persist visually. ⟳ web restarts the Node server in ~1 second
without disturbing sclang/scsynth or the WS clients (which auto-
reconnect).
Context: previous commit's REBOOT button only restarted scsynth (the
audio engine), keeping the sclang interpreter alive with all its
loaded modules. The user clarified they want a real 'reboot sclang' :
kill the whole interpreter and reload everything from boot.scd. That
needs cooperation between the web bridge (which can only reach sclang)
and the launcher (which controls the sclang process).
Approach: a sentinel-file pattern lets the bridge signal the launcher
without sharing a port. The bridge handler /control/rebootSclang
writes ~/.av-live-restart-sclang then pkills sclang. The launcher's
new sentinel watcher fires every 1s ; on detection it removes the
sentinel and calls restartSclang(), which sets a wantsRestart flag
and terminates the current process — the existing terminationHandler
sees the flag and respawns sclang 0.6s later (so scsynth can be
reaped). Two distinct buttons in the topbar : ⟳ scsynth (small,
existing /control/rebootServer) and ⟳ sclang (big, new path).
Changes:
- web_bridge.scd : new /control/rebootSclang OSCdef writes the
sentinel file then unixCmd's pkill -TERM -f 'sclang.*boot.scd'
after a 0.5s delay
- launcher/ProcessManager.swift :
* sclangWantsRestart flag
* restartSclang() : sets the flag, calls stopSclang, re-sets the
flag (since stopSclang clears it for normal stops)
* startSentinelWatcher() : 1s Timer that checks for the sentinel
file in /Users/electron, removes it and calls restartSclang() on hit
* maybeRestartSclang() : called from terminationHandler, respawns
if wantsRestart is set, with 0.6s delay so scsynth pkill finishes
- launcher/AVLiveLauncherApp.swift : startSentinelWatcher() invoked
in applicationDidFinishLaunching
- web/public/control/index.html :
* #btnReboot split into #btnRebootScsynth (small ⟳ scsynth) and
#btnRebootSclang (full ⟳ sclang)
- web/public/control/app.js : two makeArmedButton bindings, one per
reboot path
- web/public/control/style.css : .btn.warn.small variant for the
scsynth button
Impact: ⟳ sclang now does a full engine restart (kills sclang,
launcher respawns it, ~albums and all wrappers reload). ⟳ scsynth
keeps the lighter path that just bounces the audio server. Both
follow the same two-click armed pattern as TEARDOWN.
Context: clicking through 23 album cards in the browser UI is the
existing path, but during a live set you often want to jump to an
album without breaking out of the menubar workflow. The launcher had
no way to actually drive sclang — Process spawn / stop only.
Approach: add a tiny OSC 1.0 sender written from scratch in Swift
(no dependency, ~80 LOC of bit twiddling) that ships UDP packets to
sclang's :57121 listener — same address the web bridge uses for
/control/* messages. Discover albums by scanning <avLive>/sound_algo/
tracks/X_<slug>/ subdirectories at popover open, build a horizontal
scroll of bordered buttons (one per album) showing the letter +
human-cased title. A Stepper picks the gap between tracks (0..32s),
a stop icon button next to it sends /control/stopAlbum.
Changes:
- launcher/OSCSender.swift : minimal OSC encoder + UDP transport.
Supports string / int / float args, big-endian int32 + float32, OSC
string null-terminator + 4-byte padding, type tag string starting
with ','. Uses BSD socket() / sendto() — fire and forget, no
reception
- launcher/ProcessManager.swift :
* new osc property (OSCSender bound to 127.0.0.1:57121)
* Album struct (letter, slug, displayTitle)
* discoverAlbums() walks the tracks/ directory looking for
A_xxx / B_xxx / ... single-uppercase-letter prefixed folders,
title-cases the slug for display
* playAlbum(letter, gap) → /control/playAlbum
* playTrack(letter, n) → /control/playTrack
* stopAlbum() → /control/stopAlbum
- launcher/MenuBarContent.swift :
* AlbumLauncher view : header with title + gap Stepper + stop button,
horizontal ScrollView of 64x30 album buttons (letter big +
truncated title small), tooltip 'X — Title' on hover
* popover frame width 360 → 380 to fit the row better
* MonospacedFont uses .system(.caption, design: .monospaced) (macOS
11 compatible) instead of .caption.monospaced() (12+)
Impact: clicking an album button in the menubar fires the same OSC
message the web UI sends — the audio engine starts the album in
sequence with the chosen gap, no browser tab needed. Live sets can
operate purely from the menubar.
Context: launching sclang with 00_load.scd as argument was a no-op —
the file is split into ~32 IDE-friendly Cmd+Enter blocks and sclang's
parser only accepts ONE top-level expression per .load. So nothing
actually ran, the engine never came up on the MBP, and every web tab
that depends on /sync/* (Album, Mélodies, Kicks, Mixer real values,
…) was empty. Same problem hit tracks/_index.scd which has shortcut
blocks at the bottom : 'ERROR: syntax error, unexpected (, expecting
end of file' at line 104, which aborted the load and never populated
~albums.
Approach: a tiny single-block boot.scd that derives ~base from
thisProcess.nowExecutingPath and just .load's live/_load.scd (which
runs the whole engine + tracks + web_bridge chain). The launcher's
default soundAlgoLoadFile picks boot.scd if it exists, falls back to
00_load.scd otherwise. tracks/_index.scd's IDE shortcut blocks at the
bottom are wrapped in /* */ block comment so the file is .load-clean
while staying useful in the IDE (uncomment to use). stopSclang()
also runs pkill scsynth as a safety net so the next boot can grab
:57110 — sclang.terminate() doesn't reliably kill its scsynth child.
Changes:
- sound_algo/boot.scd : new entry point, single block, sets ~base
from disk location and loads live/_load.scd
- sound_algo/tracks/_index.scd : 9 IDE shortcut lines (lines 104-114)
wrapped in /* */ so .load no longer hits 'syntax error unexpected (,
expecting end of file'
- launcher/ProcessManager.swift :
* default soundAlgoLoadFile prefers <avLive>/sound_algo/boot.scd
over <avLive>/sound_algo/00_load.scd when both exist
* stopSclang() runs /usr/bin/pkill -f scsynth after sclang.terminate()
Impact: end-to-end verified on the Big Sur Intel MBP via SSH tunnel.
After launcher start the Album tab shows the 23 expected cards
(Acid Journey, Deep to Hard, …, Dub Techno) with working ▶ Album /
▶ Tracks buttons. scsynth :57110 is alive, web bridge is healthy
(3 WS clients), oscope-of receives /sync/album in its HUD.
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)
Context: the AV-Live web UI (control surface + Hydra canvas
synchronized to audio, served by sound_algo/web/server.js as a Node
Express app on :3000) was the third leg of the stack but the launcher
ignored it. Operator had to remember to 'cd web && npm start' in a
terminal before opening the page.
Approach: add a third managed process to ProcessManager, spawned 0.6s
after oscope-of, then 2.3s later (T+3.5s from launch) call
NSWorkspace.shared.open() on http://localhost:$port/ to bring the
browser tab up. Path defaults are derived the same way as the others —
walk up from the .app bundle, fall back to ~/AV-Live and
~/Documents/Projets/AV-Live, first one with sound_algo/web/server.js
wins. Node binary discovery probes /usr/local/bin/node,
/opt/homebrew/bin/node, /usr/bin/node, picks the first executable
file. Process inherits the parent environment with PATH augmented by
both Homebrew prefixes (Node deps shell out to npm-installed binaries
sometimes) and HTTP_PORT set so the user can change the port from the
launcher without hand-editing server.js.
Changes:
- ProcessManager.swift :
* import AppKit (needed for NSWorkspace browser launch)
* @Published webRunning + nodePath / webServerScript / webPort /
autoOpenBrowser, all persisted in UserDefaults
* startWeb / stopWeb / openBrowser methods, mirroring the
sclang/oscope pair, with PATH and HTTP_PORT env injection
* startAll() now starts web 1.2s after sclang and opens the browser
at +3.5s when autoOpenBrowser is true
* stopAll() terminates web too
- MenuBarContent.swift : new ProcessRow for the Web UI showing the
port in the subtitle, an 'Open in browser' button next to the rows
(disabled until the server is up), Settings sheet gets toggles for
autoOpenBrowser and PathFields for node + server.js
Impact: opening AVLiveLauncher.app on the live MBP now brings up
sclang, oscope-of, the Node web server AND the browser tab in 4 seconds
flat. Toggle off in Paths… for users who prefer to open the URL on
another device. Smoke-tested end-to-end on Big Sur Intel — HTTP 200
on http://localhost:3000/ ~9s after launch.
Context: opening AVLiveLauncher used to drop a menubar icon and wait
for the user to click Start twice. For a live performance laptop you
want the whole stack alive the moment you double-click the .app — sound
engine and visualizer should both be running by the time the menubar
icon appears.
Approach: a new autoStart UserDefault (default true) gates a startAll()
helper that fires sclang immediately and oscope-of 0.6s later (gives
sclang's scsynth boot a head start so its OSC bridge is listening when
oscope-of opens its sender). Settings sheet exposes a toggle to disable
the behavior. Default-path discovery now adapts to the actual location
of the AV-Live tree by walking up from the .app bundle and checking
~/AV-Live and ~/Documents/Projets/AV-Live, instead of hard-coding the
GrosMac path that didn't exist on the deployment MBP (sclang was
silently failing with 'load file not found').
Changes:
- ProcessManager.swift :
* @Published var autoStart, persisted in UserDefaults, defaults true
on first launch
* new startAll() that starts sclang then dispatches startOscope()
after 0.6s
* init() now derives avLive root by trying (in order) the parent
directories of the app bundle (5 levels up), ~/AV-Live, and
~/Documents/Projets/AV-Live — first one whose sound_algo/00_load.scd
exists wins
- AVLiveLauncherApp.swift : after the popover is wired up,
asyncAfter(+0.3s) → processManager.startAll() if autoStart enabled,
so the menubar icon is visible before subprocess output starts
- MenuBarContent.swift : Settings sheet gets an Auto-start toggle at
the top, above the Paths section
Impact: double-clicking AVLiveLauncher.app on the live MBP now boots
the whole stack in ~1 second. The toggle is reachable from Paths…
in the popover for users who want the old behavior. Smoke-tested on
the Intel Big Sur MBP — sclang + scsynth + oscope-of all alive 6s
after open.
Context: the launcher .app needs (a) a real icon visible in Dock /
Mission Control / Finder when its log window is open, and (b) a binary
that runs on both Intel MBPs (the current target on the 192.168.0.189
machine) and Apple Silicon. Initial build was arm64-only.
Approach: SVG icon rendered via librsvg into a multi-size .iconset,
packed into AppIcon.icns by iconutil (no Xcode asset catalog needed).
build.sh now compiles for both arm64 and x86_64 separately and lipos
them into a fat binary before bundling. The icon is regenerated only
when the SVG is newer than the existing .icns. Also switched the
default oscope-of binary path to look inside the .app bundle that
oF Release produces, with a walk-up-to-bin/ heuristic for the cwd.
Changes:
- Resources/icon.svg : 1024x1024 design — orange electron with
asymmetric googly eyes, sticking-out tongue, three rotated chaotic
orbital ellipses (cyan / purple / pink), radiating yellow lightning
bolts and scattered sparks on a dark purple radial gradient with
Big-Sur rounded-rect mask
- Resources/make_icon.sh : rsvg-convert at 16/32/128/256/512 px @1x
and @2x, then iconutil -c icns
- Resources/Info.plist : CFBundleIconFile = AppIcon
- build.sh : iterate ARCHS (default 'arm64 x86_64'), build each, lipo
-create them, regen icon if SVG is newer, copy AppIcon.icns into
Contents/Resources
- ProcessManager.swift :
* default oscopePath now prefers
.../bin/oscope-of.app/Contents/MacOS/oscope-of (the bundle that
openFrameworks produces) with fallback to bare binary
* new oscopeProjectRoot(from:) walks up the path to find the parent
of 'bin/', so cwd is the oF project root regardless of bundle vs
bare binary, letting oF locate bin/data/
- .gitignore : exclude generated AppIcon.iconset and AppIcon.icns
Impact: ./build.sh now produces a universal .app that launches on both
arm64 and Intel macs (smoke-tested on Big Sur Intel MBP). The icon
appears in the Dock when the log window is open, and oscope-of starts
correctly regardless of how it was built.
Context: AV-Live needs a single-click way to boot its two heavy
components (the SuperCollider sound engine and the openFrameworks
oscope-of visualizer) together, with their logs aggregated in one
place. Running them by hand from terminal during a live set is fragile.
Approach: Swift Package Manager + a small build.sh that wraps the
release binary into a real .app bundle (Info.plist with LSUIElement=true
to keep it out of the Dock). The UI uses NSStatusItem + NSPopover so
it works back to macOS 11 (Big Sur) — MenuBarExtra would require 13+
which the current target MBP can't run.
Changes:
- launcher/Package.swift : SwiftPM manifest, single executable target,
macOS 11 minimum
- AVLiveLauncherApp.swift : @main App + AppDelegate that owns the
status item, popover and lazily-created log window
- ProcessManager.swift : ObservableObject spawning sclang and oscope-of
via Foundation Process, capturing stdout/stderr through Pipe +
readabilityHandler, ring-buffered log array (2000 lines), termination
handlers that flip @Published flags back. Paths persisted in
UserDefaults with sane defaults
- MenuBarContent.swift : popover view with two ProcessRow toggles,
Logs / Paths / Quit, Settings sheet using NSOpenPanel
- LogView.swift : separate window with filter, auto-scroll, color-coded
source column, monospaced font. #available guard for textSelection
- Info.plist : bundle id cc.saillant.AVLiveLauncher, GPL-3 copyright,
LSUIElement true, min macOS 11
- build.sh : swift build -c release, copies binary + Info.plist into
AVLiveLauncher.app, ad-hoc codesigns
- .gitignore + README
Impact: 'cd launcher && ./build.sh && open build/AVLiveLauncher.app'
gives a working .app that brings up the AV stack from the menubar.
Smoke-tested on arm64 — Intel MBP needs a local build or a future
universal -arch x86_64 flag in build.sh.