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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user