feat: new preset, looper recorder, IR display improvements

- New Preset button: finds first empty slot, saves with default name
- Looper/Recorder tab: Record/Stop via arecord backend, list captures, play back
- Rig: shows loaded IR name more clearly + quick-link buttons to browse IRs/Models
- Backend: capture/start, capture/stop, captures API endpoints with file serve
This commit is contained in:
2026-06-12 17:24:14 -04:00
parent 2a30592ab3
commit a1f35a56e3
5 changed files with 347 additions and 216 deletions
+70
View File
@@ -16,6 +16,7 @@ import json
import os
import logging
import time
import datetime
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from pathlib import Path
@@ -892,6 +893,75 @@ class WebServer:
},
)
# ── Audio capture (looper/recorder) ──────────────────────
_capture_process = None
_capture_path = None
@app.post("/api/capture/start")
async def capture_start():
"""Start recording audio to a WAV file via arecord."""
nonlocal _capture_process, _capture_path
import subprocess, datetime
captures_dir = Path.home() / ".pedal" / "captures"
captures_dir.mkdir(parents=True, exist_ok=True)
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
path = captures_dir / f"capture_{ts}.wav"
try:
_capture_process = subprocess.Popen(
["arecord", "-f", "S16_LE", "-r", "48000", "-c", "2", str(path)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
_capture_path = path
return {"ok": True, "path": str(path), "filename": path.name}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/capture/stop")
async def capture_stop():
"""Stop the current recording."""
nonlocal _capture_process, _capture_path
if _capture_process is None:
raise HTTPException(status_code=400, detail="No active recording")
try:
_capture_process.terminate()
_capture_process.wait(timeout=5)
except Exception:
_capture_process.kill()
name = _capture_path.name if _capture_path else "unknown"
_capture_process = None
_capture_path = None
return {"ok": True, "filename": name}
@app.get("/api/captures")
async def list_captures():
"""List saved captures."""
captures_dir = Path.home() / ".pedal" / "captures"
if not captures_dir.is_dir():
return {"captures": []}
files = sorted(captures_dir.glob("*.wav"), key=lambda f: f.stat().st_mtime, reverse=True)
return {
"captures": [
{
"name": f.name,
"path": str(f),
"size": f.stat().st_size,
"size_display": f"{f.stat().st_size / 1024:.0f} KB" if f.stat().st_size < 1024*1024 else f"{f.stat().st_size / (1024*1024):.1f} MB",
"created": datetime.datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
}
for f in files
]
}
@app.get("/api/captures/{filename}")
async def serve_capture(filename: str):
"""Serve a captured WAV file for download/playback."""
from fastapi.responses import FileResponse
captures_dir = Path.home() / ".pedal" / "captures"
file_path = captures_dir / filename
if not file_path.exists() or not file_path.is_file():
raise HTTPException(status_code=404, detail="Capture not found")
return FileResponse(str(file_path), media_type="audio/wav", filename=filename)
# ── Tonehub API alias routes ─────────────────────────────
# These are direct aliases for Tone3000 search, providing a
# simpler /api/tonehub/search path for the web UI.