Skip to content

Commit 3c525d0

Browse files
authored
Merge pull request #31 from gantasmo/feat/library-stems-midi-first-class
Boot intro: theDAW.gltf in the cymatics chrome material, smoothed (welded + averaged normals) and faced front by countering the gltf's baked 45deg turn, forming in over ~7s and holding until the backend is ready "by GANTASMO" drawn as electric filament arcs in the electric-wave palette, in the bundled Orbitron techno face, dimmed so bloom does not blow it out flat fallback gated behind a grace period so it no longer flashes in before the 3D, and the screen now hands off on real cinematic completion Startup: open the browser the instant Vite's port is listening (socket poll) instead of the broken "Local:" stdout match that fell through to a 10s timer Also carries earlier pending host fixes: questcast relay recycle on stale state, VJ view tweaks, vite proxy-noise logger, quieter launcher console.
2 parents 29a2702 + 84197b4 commit 3c525d0

20 files changed

Lines changed: 1488 additions & 121 deletions

backend/_devstack.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import os
2020
import shutil
21+
import socket
2122
import subprocess
2223
import sys
2324
import threading
@@ -89,8 +90,6 @@ def _pump(tag: str, proc: subprocess.Popen) -> None:
8990
return
9091
for line in proc.stdout:
9192
_emit(tag, line)
92-
if tag == "frontend" and not _browser_opened.is_set() and "Local:" in line:
93-
_open_browser()
9493

9594

9695
def _open_browser() -> None:
@@ -103,6 +102,23 @@ def _open_browser() -> None:
103102
pass
104103

105104

105+
def _minimize_console() -> None:
106+
"""Drop the launcher console out of sight once the app window is up — the
107+
user should only ever see theDAW, not the log stream. The console keeps
108+
running (logs land there, restorable from the taskbar). Set
109+
``theDAW_KEEP_CONSOLE=1`` to keep it in front (debugging the stack)."""
110+
if not IS_WINDOWS or os.environ.get("theDAW_KEEP_CONSOLE"):
111+
return
112+
try:
113+
import ctypes
114+
115+
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
116+
if hwnd:
117+
ctypes.windll.user32.ShowWindow(hwnd, 6) # SW_MINIMIZE
118+
except Exception:
119+
pass
120+
121+
106122
def _kill_tree(proc: subprocess.Popen) -> None:
107123
if proc.poll() is not None:
108124
return
@@ -141,13 +157,35 @@ def _run_backend(children: list) -> None:
141157
return
142158

143159

144-
def _browser_fallback() -> None:
145-
time.sleep(10)
160+
def _port_open(host: str, port: int) -> bool:
161+
try:
162+
with socket.create_connection((host, port), timeout=0.25):
163+
return True
164+
except OSError:
165+
return False
166+
167+
168+
def _wait_then_open_browser() -> None:
169+
"""Open the browser the instant Vite is actually accepting connections on
170+
5173, instead of parsing its (buffered, colored) stdout for a "Local:" line
171+
that rarely matches and left the launch waiting on a 10s timer. Falls back to
172+
opening anyway after a long wait so the launch never hangs."""
173+
deadline = time.time() + 60.0
174+
while not _shutdown.is_set() and time.time() < deadline:
175+
if _port_open("127.0.0.1", 5173):
176+
_open_browser()
177+
return
178+
time.sleep(0.2)
146179
if not _shutdown.is_set():
147180
_open_browser()
148181

149182

150183
def main() -> int:
184+
# Drop the launcher console immediately so the user sees only the app, never
185+
# the log stream. It keeps running (restorable from the taskbar);
186+
# theDAW_KEEP_CONSOLE=1 keeps it in front for debugging.
187+
_minimize_console()
188+
151189
if not _enable_ansi():
152190
for key in COLORS:
153191
COLORS[key] = ""
@@ -174,7 +212,7 @@ def main() -> int:
174212
else:
175213
_emit("stack", "localtunnel not installed — public link skipped")
176214

177-
threading.Thread(target=_browser_fallback, daemon=True).start()
215+
threading.Thread(target=_wait_then_open_browser, daemon=True).start()
178216

179217
# Backend supervisor on its own thread so Ctrl-C lands in main().
180218
backend = threading.Thread(target=_run_backend, args=(children,), daemon=True)

backend/modules/questcast/sidecar.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def status(self) -> dict[str, Any]:
106106
def _ensure_adb_server(self) -> Optional[str]:
107107
adb = _adb_path()
108108
if not adb:
109-
return "adb not found — install Android platform-tools or set theDAW_ADB / theDAW_QUESTCAST_ADB"
109+
return "adb not found. Install Android platform-tools or set theDAW_ADB / theDAW_QUESTCAST_ADB"
110110
try:
111111
subprocess.run([adb, "start-server"], capture_output=True, timeout=20)
112112
except (subprocess.TimeoutExpired, OSError) as e:
@@ -125,7 +125,7 @@ def _ensure_bootstrap(self) -> Optional[str]:
125125
npm = _npm_cmd()
126126
if not npm:
127127
return "npm not found — cannot bootstrap the questcast sidecar deps"
128-
log.info("questcast: bootstrapping Node deps (one-time)")
128+
log.info("questcast: bootstrapping Node deps (one-time)...")
129129
try:
130130
result = subprocess.run(
131131
[npm, "install"],
@@ -209,8 +209,23 @@ def _read_stdout(self, proc: subprocess.Popen) -> None:
209209
def start(self, device_serial: Optional[str] = None) -> dict[str, Any]:
210210
with self._lock:
211211
if self.running:
212-
self._record("start() ignored — relay already running")
213-
return self.status()
212+
state = self._status.get("state")
213+
if state in ("ready", "starting"):
214+
self._record("start() ignored, relay already running")
215+
return self.status()
216+
# Process is alive but the stream is dead (Quest slept / display
217+
# off -> video-ended) or errored. Recycle it so a fresh scrcpy
218+
# stream comes up, instead of leaving a zombie relay with no
219+
# video (which forced a manual toggle/refresh before).
220+
self._record(f"start() recycling stale relay (state={state})")
221+
proc = self._proc
222+
self._proc = None
223+
if proc is not None and proc.poll() is None:
224+
proc.terminate()
225+
try:
226+
proc.wait(timeout=5)
227+
except subprocess.TimeoutExpired:
228+
proc.kill()
214229

215230
self._record(
216231
f"start() requested (serial={device_serial or 'first device'})"
@@ -232,7 +247,7 @@ def start(self, device_serial: Optional[str] = None) -> dict[str, Any]:
232247
return {"ok": False, "error": err}
233248
self._record("adb start-server ok")
234249

235-
self._record("ensuring node deps + scrcpy server (bootstrap)")
250+
self._record("ensuring node deps + scrcpy server (bootstrap)...")
236251
err = self._ensure_bootstrap()
237252
if err:
238253
self._record(f"ABORT: bootstrap: {err}")
@@ -293,7 +308,7 @@ def stop(self) -> dict[str, Any]:
293308
proc = self._proc
294309
self._proc = None
295310
self._status = {"state": "stopped"}
296-
self._record("stop() requested terminating relay")
311+
self._record("stop() requested, terminating relay")
297312
if proc is not None and proc.poll() is None:
298313
proc.terminate()
299314
try:

docs/plans/2026-06-13-global-layout-vj-library-optimizations-plan.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,3 +546,53 @@ Open items elsewhere (not this plan): PR #19 awaiting review/visual sign-off; au
546546
Landed (VJ repo, needs live eyes; theDAW repo where noted): Quest streaming FIXED (SPS/PPS prepend) + auto-reconnect + visibility-gated decode; Quest source/preview in right panel (reuses stream, killed host double-decode); Quest view crop FULL-SBS/L-16:9/R-16:9; auto-default source→Quest when adb device present; Cymatics as a VJ source (4 modes); Resolume clip-grid + collapsible panels + top level-scope + full-height right panel + native export folder dialog; clipboard fix; basic-pitch `pkg_resources` (setuptools<81, pinned), stems sidecar torch realign (2.11.0+cpu set) + onnxruntime for audio-separator, librosa.core.audio shim. Watch-link: plan `docs/plans/2026-06-14-vj-watch-link-broadcast-plan.md`, Phase 1 backend `backend/modules/broadcast/` DONE; VJ GO-LIVE broadcaster next.
547547

548548
Queued VJ UI punchlist (memory `project_vj_ui_punchlist`, NOT started): (1) banks=rows + wheel/ctrl-wheel scroll, clear add-bank; (2) kill verbose "Awaiting Data Core" empty-state → "media not found. click or drop here to add"; (3) "SOURCE // MATRIX" → "SOURCE"; (4) "SELECT & IMPORT VIDEO CLIPS" → "Import Media" + filetype-list tooltip, DOCUMENT supported vs unsupported codecs + why (ProRes/.mkv/alpha-HEVC etc. — verify before writing); (5) drag Library media → VJ banks/anywhere (cross-origin, via send-to-VJ bus); (6) footer play/pause reflects ACTUAL active playback (library/anywhere), overlaps G13.
549+
550+
### 9a. Import Media — accepted vs unsupported formats (item 4 done)
551+
552+
The VJ ingest path (`fileRouter.ts` `VJ_FILE_ACCEPT = 'video/*,audio/*,image/*'`) hands media to a plain `<video>`/`<audio>`/`<img>` element, so what plays is whatever the host browser (Chromium in the embedded webview) can natively decode. The Import Media button now carries a `title` tooltip (`IMPORT_MEDIA_TOOLTIP` in `VJControls.tsx`) listing the working set; this is the rationale behind that list.
553+
554+
- **Video (supported):** MP4 / M4V with H.264 or H.265/HEVC, WebM with VP8 / VP9 / AV1, MOV with H.264. Transparent overlays only via VP8/VP9-alpha **WebM**.
555+
- **Audio (supported):** MP3, WAV, FLAC, AAC / M4A, OGG (Vorbis), Opus.
556+
- **Images (supported):** PNG, JPG/JPEG, WebP, GIF (incl. animated), AVIF, BMP.
557+
- **NOT supported, and why:**
558+
- **ProRes / DNxHD `.mov`** — pro intermediate codecs; no browser decoder. Transcode to H.264 MP4 first.
559+
- **alpha-HEVC `.mp4`** (Apple-style transparent HEVC) — Chromium will not decode the alpha plane in an MP4 container; the clip plays opaque or fails. Use VP9-alpha WebM for transparency.
560+
- **`.mkv` / `.avi` / `.flv` containers** — Chromium has no Matroska/AVI/FLV demuxer even when the inside codec is fine. Re-wrap to MP4/WebM (lossless `-c copy` is enough when the codec is supported).
561+
- **Raw / uncompressed (YUV, image sequences as a "video")** — no container/decoder path; import a PNG sequence as images or encode to a supported codec.
562+
- Note: H.265/HEVC playback depends on OS/GPU support being present in the Chromium build; on a machine without HEVC it silently fails where H.264 would work — H.264 MP4 is the safe interchange format.
563+
564+
## 10. Session 2026-06-14b: boot cinematic, LBR8 robustness, bank buttons + right-panel reorg plan
565+
566+
All UNCOMMITTED across host (theDAW) + VJ app (GANTASMO-LIVE-VJ) + one backend file. Needs the user's live eyes/ears + relaunch (HMR off; vite.config + splash + python changes need a fresh start).
567+
568+
### Landed this session (to verify live)
569+
- BOOT CINEMATIC rebuilt to the LOCKED spec (see memory project_boot_animation_spec). `frontend/src/components/layout/LiquidChromeTitle.tsx`: dark purple steel background; theDAW.gltf in the cymatics chrome (MeshStandardMaterial metalness 0.99 / roughness 0.008 + EXR env + cymatics light rig) assembling from scattered vertices; "by GANTASMO" formed from the electric-wave-audio-visualizer electricity (cyan->magenta additive points + UnrealBloom + crackle); forms over ~7s after assets load; holds until backend ready; NEVER says loading. LoadingScreen.tsx is now just this cinematic (no ferro screen, no orb, no loading text). index.html has an instant dark-purple splash (no text). main.tsx removes it on mount. App.tsx cinematicDone gate = 9000ms (so the ~7s plays even when backend binds in ~1s). Assets copied into host: frontend/public/{theDAW.gltf,theDAW.svg,piz_compressed.exr}, frontend/src/cymatics/*. BootCinematic.tsx (ferro bg) is now UNUSED (left in tree).
570+
- TERMINAL HIDDEN: backend/_devstack.py minimizes the console at stack start (theDAW_KEEP_CONSOLE=1 to keep it); theDAW.bat trimmed of banner + verbose echoes; vite.config customLogger drops the proxy ECONNREFUSED flood.
571+
- PROFESSIONAL ERROR/LOADING STATES (VJ app): killed "Optics Offline / SYS::ERR / physical video buffer / mainboard"; suppressed the play()/pause() AbortError so it no longer triggers an overlay (useMedia.isBenignPlayInterruption); "Loading OMEGA Engine" -> "Loading..."; VJView sidecar wall -> auto-retry "Loading..." then a compact "The VJ engine didn't start." + Retry.
572+
- LBR8 rename: the QUEST source toggle is now "LBR8"; the verbose ADB paragraph is a compact "LBR8" with the exact tooltip "stream 16x9 or SBS3D from Meta Quest via USB or wireless ADB without Link or MQDH (must have USB Debugging enabled)".
573+
- LBR8 ROBUSTNESS (the real fix): backend/modules/questcast/sidecar.py start() now RECYCLES a stale relay (Node alive but scrcpy video-ended on sleep) instead of "already running" no-op. Frontend useQuestCast.ts has a watchdog (no frames 5s while visible -> recover) + a visibilitychange wake handler -> auto recovery, no refresh/toggle. Auto-default-to-LBR8 (App.tsx) now retries ~6x/1.8s since adb lags after boot.
574+
- BANK AXIS BUTTONS (ClipGrid.tsx): grow/shrink moved to bare +/- on the axes (columns +/- on the right edge, banks +/- along the bottom), small + high-contrast, no labels/counts. Default gridRows = 1 (one bank). Removed the wrong "N rows" label + the glyph scroll-hint. Trailing auto-empty bank removed.
575+
- EMOJI/GLYPH PURGE: removed checkmarks, arrows, ellipses, multiplication signs, and emdashes from useQuestCast.ts, VJControls.tsx, sidecar.py, ClipGrid.tsx (logs/comments/aria). Standing rule now: no emojis/glyphs anywhere, ever (memory feedback_no_emojis_ever).
576+
577+
### RIGHT-PANEL REORGANIZATION PLAN (proposed, NOT built; awaiting the user's call)
578+
VJControls.tsx is one 1223-line component = a single endless scroll column: header, SOURCE deck, the huge AUTOPILOT OVERRIDE, Deck A (Geometrics) / B (Corruption) / C (Chromatics) / D (Timecode), Plugins manager, Master Sync Bus. Generous padding (px-3 py-3, space-y-4) + a crowded header add length.
579+
580+
Proposed: a TABBED deck. Header stays top, Master Sync Bus stays sticky bottom (transport). Tab strip between them so each view is ~one screen:
581+
- SOURCE: CAM/MEM crossfader, source toggles (DEVICE/SCREEN/LBR8/CYMATICS), source-specific controls (device picker, LBR8 view + log, cymatics mode), Canvas Format, MUTE + Import.
582+
- EFFECTS: Decks A-D as collapsible sections (collapse-all default), Render Performance, FX looks tier.
583+
- AUTOPILOT: the whole Autopilot Override (so it stops pushing everything down when on).
584+
- PLUGINS: the plugins catalog.
585+
Space/ergonomics: halve section padding/gaps, shrink oversized headers, turn the header's crowded row (REC + 2 selects + folder + CAM ERR + 4 layout buttons + reset) into grouped clusters with layout modes as a small segmented control.
586+
587+
THREE MIDI SURFACES (user: "3 buttons called MIDI, one in the middle of the video feed"): (1) host toolbar "MIDI" input chip (forwards MIDI into the iframe), (2) host toolbar "MIDI" master toggle (enables Web MIDI), (3) the VJ app MIDI Mapper pill that floats top-right over the canvas (MidiPanel.tsx, absolute top-2 right-32). Plan: move the mapper off the feed into the panel, consolidate the two host buttons into one MIDI master.
588+
589+
REDUNDANCIES (flagged per "do not remove features, but tell me if anything is redundant"):
590+
1. Clip management in 3 places: ClipGrid banks, Library Pool browser, and the Archive Bin inside the SOURCE deck (Archive Bin duplicates banks/pool).
591+
2. Import has 3 entry points: ClipGrid "Import", SOURCE deck "Import Media" bar, drop-anywhere empty state.
592+
3. The two host MIDI buttons overlap.
593+
4. The MIDI Mapper pill over the video is a 3rd MIDI surface.
594+
5. Audio reactivity shows as "AUDIO FIX/MIC OFF" in the Sync Bus and as the host "Audio" input chip.
595+
596+
CLEAN CODE: split VJControls.tsx (1223 lines) into ControlDeckHeader, SourceDeck, AutopilotDeck, EffectsDecks (+ existing PluginsPanel + sync bus); lets tabs lazy-render.
597+
598+
DECISIONS PENDING before building the reorg: tabs vs accordions; keep all 3 clip surfaces or merge the Archive Bin; consolidate the 2 host MIDI buttons; move the mapper off the canvas. Build only what the user greenlights. Also pending: the cinematic tuning pass (form timing, scatter, particle density, bloom, camera, model scale, the purple) once the user sees it live.

docs/reports/feature-doc-coverage-report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Feature Documentation Coverage Report
22

33
> [!NOTE]
4-
> Generated: 2026-06-14T22:17:26.774Z · Git revision: `8087b5cad73a` · Repomix tracked: **no**
4+
> Generated: 2026-06-15T17:28:58.613Z · Git revision: `9aadc4360f1a` · Repomix tracked: **no**
55
66
## Audit Dashboard
77

docs/reports/feature-doc-coverage.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"generatedAt": "2026-06-14T22:17:26.774Z",
3-
"repoRevision": "8087b5cad73a",
2+
"generatedAt": "2026-06-15T17:28:58.613Z",
3+
"repoRevision": "9aadc4360f1a",
44
"repomixContext": {
55
"path": "repomix-output.md",
66
"present": false,

docs/screenshots/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"generatedAt": "2026-06-14T22:19:34.182Z",
2+
"generatedAt": "2026-06-15T17:31:32.472Z",
33
"entries": [
44
{
55
"file": "01-shell-make.png",

frontend/index.html

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,23 @@
99
"please refetch" signal. Bump when /favicon.svg changes. -->
1010
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=4" />
1111
<title>theDAW · by GANTASMO</title>
12+
<!-- Instant boot cover. Pure inline HTML/CSS so it paints on the FIRST byte,
13+
before the JS bundle, React, or any /api request — the user never sees a
14+
blank page (or the launcher console) during startup. The React loading
15+
screen renders on top of it, then main.tsx removes this node. -->
16+
<style>
17+
/* Instant cover: the same dark purple steel as the cinematic background,
18+
so the browser paints it immediately with no blank gap and no text. */
19+
#boot-splash {
20+
position: fixed;
21+
inset: 0;
22+
z-index: 2147483647;
23+
background: radial-gradient(120% 120% at 50% 42%, #241640 0%, #160e28 55%, #0d0818 100%);
24+
}
25+
</style>
1226
</head>
13-
<body>
27+
<body style="margin: 0; background: #0d0818">
28+
<div id="boot-splash"></div>
1429
<div id="root"></div>
1530
<script type="module" src="/src/main.tsx"></script>
1631
</body>

frontend/public/Orbitron.ttf

37.7 KB
Binary file not shown.

frontend/public/theDAW.gltf

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)