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:
@@ -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,
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user