80 Commits

Author SHA1 Message Date
L'électron rare 041c6b42be chore(viz): add per-stage perf timers 2026-05-13 13:22:21 +02:00
L'électron rare c2777fc6f9 feat(av-live-body): live settings panel
Adds RenderSettings ObservableObject (cam opacity, bg brightness,
mesh metallic/roughness/visible, 3 light intensities, FOV, panel
visibility) and a SettingsPanel SwiftUI overlay with sliders and
toggles for each.

ContentView wraps BodyView in a ZStack with a corner button + an
on-receive keyboard handler ('S') that toggles the panel. BodyView
applies every setting in updateNSView : preview opacity, container
bg, ARView FOV, the three DirectionalLight intensities, and
MeshRenderer.applyMaterialSettings rebuilds SimpleMaterial with
the new metallic/roughness on each call.

BodyView also tightens the layer chain that was visually ambiguous :
arView.layer.isOpaque = false (clear background actually honored by
the compositor) and preview.zPosition = -100 so the cam stays
strictly behind the AR layer.
2026-05-13 13:13:57 +02:00
L'électron rare d266a98ef2 perf(av-live-body): drop stale frames on backlog 2026-05-13 13:09:27 +02:00
L'électron rare 8294c56486 fix(av-live-body): cap TCP buffer at 8 MiB 2026-05-13 13:06:09 +02:00
L'électron rare 67dc83b34e feat(av-live-body): cam overlay + 3-pt lighting
BodyView keeps the AVCaptureVideoPreviewLayer behind the ARView
but drops its opacity to 0.35 so the SMPL-X mesh stays the
dominant visual subject while the webcam frame provides spatial
context.

The ARView gets a three-light rig (key 4000 lm warm from upper
right, fill 1500 lm cool from upper left, rim 2000 lm white from
behind) so the per-vertex normals computed in MeshRenderer give
visible shading instead of flat color blobs.

Also captures two follow-up plans : body-mesh-perf-safety and
smplx-test-coverage, both stay as docs only for now.
2026-05-13 12:59:12 +02:00
L'électron rare 352eab18cc feat(launcher): refine bodyMesh + dataOnly modes
- 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.
2026-05-13 12:56:01 +02:00
L'électron rare c49f171334 feat(launcher): add 'Body Mesh' launch mode
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.
2026-05-13 12:51:58 +02:00
L'électron rare b6badea216 fix(av-live-body): per-vertex normals for shading
The mesh was rendered as flat color blobs because LowLevelMesh
had only a position attribute, leaving SimpleMaterial (which is
lit) without normals to compute shading. Each face ends up with
identical lighting, no depth cue.

createLowLevelMesh now declares a second vertex buffer for
normals (semantic .normal, bufferIndex 1, same float3 stride).
updateMeshVertices recomputes per-vertex normals each frame by
accumulating triangle normals and normalising, then writes the
buffer in place. Cost is O(faces + verts) per person : ~63 k
cross products for 10 475 verts / 20 908 triangles, comfortably
under a millisecond on M5.

Result : the SMPL-X mesh now has visible relief instead of a
chromatic silhouette.
2026-05-13 12:44:42 +02:00
L'électron rare e5e133a26b fix(av-live-body): coord conversion + winding flip
Three coupled changes so the SMPL-X mesh actually shows up in the
RealityKit ARView when fed from Multi-HMR :

- Multi-HMR camera frame is y-down z-forward, RealityKit is
  y-up z-back. We flip v.y and v.z in updatePersons, leaving
  the entity at the origin since v3d is already absolute camera
  coordinates (Python sender ships them post-translation).
- Flipping y and z reverses chirality, so the front-facing
  triangles become back-facing and backface culling hides the
  whole mesh. We swap each triangle's i1/i2 in loadFaces.
- A DirectionalLight is added in BodyView so SimpleMaterial,
  which is lit, doesn't render the mesh as a black silhouette.

Logged once per pid when an entity is first created : the
bounding box of the converted vertices (helps spot scale or
sign issues in the future).
2026-05-13 12:43:13 +02:00
L'électron rare 052d1d0927 feat(layout): single-window mode + headless py
User wants a single visible window. data_only_viz now skips
NSWindow / Metal / HUD entirely when --multi-hmr is set, keeping
only the OSC listener and the Multi-HMR worker that streams
vertices to AV-Live-Body. AV-Live-Body keeps the camera preview
behind the SMPL-X mesh and stays the sole UI surface.

Tradeoff: the generative Metal scenes (storm, tunnel, plasma...)
and the multi-person skeleton overlay drawn by data_only_viz are
not visible in multi-hmr mode. They remain available by running
the launcher without the Multi-HMR toggle.
2026-05-13 12:18:59 +02:00
L'électron rare a69d096d15 fix(camera): strict built-in filter both sides
Both the Python AVCapture path and the Swift CameraPreviewLayer
now enforce the same two-step filter :
 1. deviceType == .builtInWideAngleCamera
 2. localizedName must NOT contain 'iPhone', 'GSM', 'Desk View',
    or 'Continuity'

The second clause matters because under some macOS configurations
(Continuity Camera promoted to widescreen, iPhone mirroring),
externally tethered phone cameras can advertise themselves with
the same primary deviceType.

Swift side also logs via NSLog so the picked device shows up in
Console.app even when stdout is buffered.
2026-05-13 12:01:14 +02:00
L'électron rare 5eefe7baa3 feat(av-live-body): webcam preview behind mesh
Adds CameraPreviewLayer that runs its own AVCaptureSession on the
built-in webcam and exposes the AVCaptureVideoPreviewLayer.

BodyView now layers : an NSView with the preview layer as backing
+ an ARView on top with transparent background. The result is the
live webcam visible behind the SMPL-X meshes — useful for debug
and for the performative angle the user wants.

macOS lets multiple processes open the same camera simultaneously
(each with its own AVCaptureSession), so this coexists fine with
the Python Multi-HMR worker that consumes the camera too.
2026-05-13 11:57:59 +02:00
L'électron rare 500abb937f perf(av-live-body): use LowLevelMesh for verts
Each frame the Python sender pushes ~125 KB of vertices per person.
The previous MeshRenderer rebuilt the MeshResource from scratch
every frame, costing a copy + driver upload for each entity.

LowLevelMesh (RealityKit, macOS 15+) keeps a persistent GPU vertex
buffer that we mutate in place via withUnsafeMutableBytes. The
faces buffer is written once at entity creation, and we tag each
ModelEntity with a PidComponent so updateMeshVertices can look up
its LowLevelMesh without touching the entities-by-pid dictionary
from the render path.

Package.swift bumped to swift-tools 6.0 / macOS 15 for the
LowLevelMesh.parts.replaceAll API ; the target stays in Swift 5
language mode so the existing closures don't need Sendable
annotations.
2026-05-13 11:23:35 +02:00
L'électron rare 7378b879fa feat(launcher): Multi-HMR toggle + body spawn
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.
2026-05-13 11:20:46 +02:00
L'électron rare d0c29d9a7f feat(av-live-body): RealityKit + TCP listener app
Adds a standalone SwiftPM executable that renders SMPL-X meshes
in a RealityKit ARView and ingests vertices from the Python TCP
sender on :57130.

Components :
- AVLiveBodyApp + ContentView (SwiftUI App entry, @main)
- BodyView (NSViewRepresentable wrapper around ARView with a
  fixed perspective camera 2 m back)
- MeshRenderer (@MainActor ObservableObject ; reads
  smplx_faces.bin from Bundle.module ; maintains a ModelEntity
  per pid ; rebuilds MeshResource each frame, immutable)
- OSCServer (Network.framework NWListener ; parses the SMPX
  binary frame protocol and emits SMPLXPersonData)

Resources are bundled inside Sources/AVLiveBody/Resources/
because SwiftPM rejects '..' traversal in resource paths.

Smoke test : 'nc -zv 127.0.0.1 57130' succeeds after launch.
2026-05-13 11:11:46 +02:00
L'électron rare 0825b35463 docs: add nested CLAUDE.md tree
Root CLAUDE.md (overview, where-to-look, conventions) plus
nested files for data_only_viz, oscope-of, launcher.
sound_algo already had its own nested tree.

Sized for the ~150-instruction budget: root 43 lines,
each nested 43-52 lines, telegraphic style.
2026-05-13 10:56:55 +02:00
L'électron rare 6932de5e74 feat(smplx): extract face topology to binary
User provided SMPLX_NEUTRAL.npz locally so we can generate the real
faces file. Adds the extraction script and the produced binary
(20908 triangles x 3 indices x int32 LE = 250896 bytes) consumed
by the upcoming AV-Live-Body RealityKit target.
2026-05-13 10:48:59 +02:00
L'électron rare eb8afd5bac nlf: extract SMPL face topology to binary 2026-05-13 10:11:04 +02:00
L'électron rare fac0f7e055 feat(launcher): data-only mode + mode picker
Add ModePickerWindow.swift for live vs data-only.
Update MenuBarContent and ProcessManager for modes.
Bump Info.plist version, update app entry point.
2026-05-13 09:33:47 +02:00
L'électron rare 02de5302c9 feat: add realart web application with Hydra, WebGL, and OSC data feeds
- 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.
2026-05-11 07:21:04 +02:00
L'électron rare 3e82d81ffc feat(web): Live tab — 30 pads + reboot web
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).
2026-05-07 18:03:53 +02:00
L'électron rare b608a50c38 feat(reboot): split scsynth vs sclang reboot
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.
2026-05-07 17:56:36 +02:00
L'électron rare 26ae5aae8f feat(launcher): album quick-launcher
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.
2026-05-07 17:46:29 +02:00
L'électron rare fa5c9bb36d fix: sclang auto-boot via boot.scd
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.
2026-05-07 17:38:19 +02:00
L'électron rare 7191f2eb2e 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)
2026-05-07 17:01:51 +02:00
L'électron rare 81450b74db feat(launcher): manage web UI server too
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.
2026-05-07 16:53:57 +02:00
L'électron rare 2aadc15a70 feat(launcher): auto-start sclang + oscope-of
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.
2026-05-07 16:27:03 +02:00
L'électron rare bf286705d4 fix(launcher): show in Dock as well as menubar
LSUIElement=true hid the app from the Dock entirely, which made it
invisible to Cmd-Tab and to anyone who'd rather keep the menubar
clean. Switched to LSUIElement=false so the app shows up in both
places — the menubar status item is still the primary entry point,
the Dock icon is just there for discoverability and Cmd-Tab.
2026-05-07 16:20:18 +02:00
L'électron rare 7dfff1b7fa feat(launcher): icon + universal build
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.
2026-05-07 12:16:13 +02:00
L'électron rare 8dab7236d4 feat(launcher): SwiftUI menubar app
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.
2026-05-07 12:04:59 +02:00