Add /api/audio/profile endpoints for runtime latency profile switching

- 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
This commit is contained in:
2026-06-13 20:47:35 -04:00
parent c0c0ffe4f2
commit 2aefa5303d
2 changed files with 108 additions and 0 deletions
+106
View File
@@ -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}")