From 2aefa5303d901206eff4e489aa9c9ba15ef4da18 Mon Sep 17 00:00:00 2001 From: Shawn Date: Sat, 13 Jun 2026 20:47:35 -0400 Subject: [PATCH] Add /api/audio/profile endpoints for runtime latency profile switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/audio/profiles — list available profiles with params - GET /api/audio/profile — current profile + jack_running + xrun_count - POST /api/audio/profile {profile: 'standard'|'low'} — switch profile, restart JACK server + JACK audio client atomically - Adds audio_system + jack_audio refs to WebServerDeps - Rollback on JACK restart failure --- main.py | 2 + src/web/server.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/main.py b/main.py index 59b838d..4c21074 100644 --- a/main.py +++ b/main.py @@ -248,6 +248,8 @@ class PedalApp: pipeline=self.pipeline, nam_host=self.nam_host, ir_loader=self.ir_loader, + audio_system=self.audio_system, + jack_audio=self.jack_audio, ), host="0.0.0.0", port=8080, diff --git a/src/web/server.py b/src/web/server.py index e618514..ffe292e 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -41,6 +41,7 @@ from ..system.tonedownload import ( discover_anon_key, format_size, ) +from ..system.audio import AudioSystem, JackAudioClient, LATENCY_PROFILES logger = logging.getLogger(__name__) @@ -201,6 +202,11 @@ class WebServerDeps: bass_nam_host: Optional[NAMHost] = None bass_ir_loader: Optional[IRLoader] = None + # Audio system (for JACK restart on profile change) + audio_system: Optional[AudioSystem] = None + # JACK audio client (needs restart when JACK restarts) + jack_audio: Optional[JackAudioClient] = None + @property def is_connected(self) -> bool: """True if we're wired to a live pedal instance (any channel).""" @@ -1269,6 +1275,106 @@ class WebServer: from ..system.bluetooth import bt_midi_connected_devices return {"devices": bt_midi_connected_devices()} + # ── Audio latency profile endpoints ─────────────────────── + + @app.get("/api/audio/profiles") + async def list_audio_profiles(): + """List all available JACK latency profiles.""" + return { + "profiles": [ + { + "key": k, + **v, + } + for k, v in LATENCY_PROFILES.items() + ], + "current": ( + self.deps.audio_system.config.profile + if self.deps.audio_system + else "standard" + ), + } + + @app.get("/api/audio/profile") + async def get_audio_profile(): + """Get the current JACK latency profile and audio stats.""" + if not self.deps.audio_system: + return { + "profile": "standard", + "period": 256, + "nperiods": 2, + "rate": 48000, + "jack_running": False, + "xrun_count": None, + } + profile_key = self.deps.audio_system.config.profile + profile = self.deps.audio_system.config.latency_profile + xruns = self.deps.audio_system.read_xrun_count() + return { + "profile": profile_key, + "period": profile["period"], + "nperiods": profile["nperiods"], + "rate": profile["rate"], + "jack_running": True, + "xrun_count": xruns, + } + + @app.post("/api/audio/profile") + async def set_audio_profile(data: dict): + """Switch JACK latency profile (restarts JACK server). + + Body: { "profile": "standard" | "low" } + + The switch takes effect immediately by restarting JACK with the + new period / nperiods / priority settings. + """ + profile_key = data.get("profile", "") + if profile_key not in LATENCY_PROFILES: + raise HTTPException( + status_code=400, + detail=f"Unknown profile: {profile_key!r}. Options: {list(LATENCY_PROFILES.keys())}", + ) + audio_sys = self.deps.audio_system + if not audio_sys: + raise HTTPException(status_code=503, detail="Audio system not available") + old_key = audio_sys.config.profile + if old_key == profile_key: + return {"ok": True, "profile": profile_key, "restarted": False} + # Apply new profile + audio_sys.config.profile = profile_key + profile = audio_sys.config.latency_profile + logger.info("Switching audio profile: %s → %s (period=%d, nperiods=%d)", + old_key, profile_key, profile["period"], profile["nperiods"]) + # Stop JACK audio client first (holds a live JACK C client) + jack_client = self.deps.jack_audio + if jack_client: + jack_client.stop() + # Restart JACK server with new parameters + ok = audio_sys.restart_jack(timeout=10) + if not ok: + # Rollback on failure + audio_sys.config.profile = old_key + audio_sys.start_jack(timeout=10) + if jack_client: + jack_client.start() + raise HTTPException(status_code=500, detail=f"Failed to start JACK with profile {profile_key!r} — rolled back to {old_key!r}") + # Restart JACK audio client + if jack_client: + jack_client.start() + await self._manager.broadcast({ + "type": "audio_profile_changed", + "profile": profile_key, + "period": profile["period"], + "nperiods": profile["nperiods"], + }) + return { + "ok": True, + "profile": profile_key, + "period": profile["period"], + "nperiods": profile["nperiods"], + "restarted": True, + } + # ── FX block param schemas ─────────────────────────────── @app.get("/api/block-params/{fx_type}")