refactor(api): complete fastapi migration and related features
Finish native FastAPI controllers, drop vendored microdot, and add Wi-Fi driver runtime, beat SSE, simulated BPM, sequence playback improvements, bridge ESP-NOW sources, UI updates, and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,8 +7,6 @@ import time
|
||||
from typing import Any
|
||||
|
||||
|
||||
_HOLDOVER_BPM_MIN = 30.0
|
||||
_HOLDOVER_BPM_MAX = 300.0
|
||||
_HOLDOVER_MAX_S = 300.0
|
||||
# After this many seconds without a detected beat, re-prime aubio and start BPM holdover
|
||||
# (same window as status() uses to hide stale BPM).
|
||||
@@ -257,6 +255,10 @@ class AudioBeatDetector:
|
||||
st["bpm"] = None
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if st.get("bpm") is not None:
|
||||
from util.bpm_limits import clamp_bpm_optional
|
||||
|
||||
st["bpm"] = clamp_bpm_optional(st["bpm"])
|
||||
return st
|
||||
|
||||
def _apply_tracking_reset_status(self) -> None:
|
||||
@@ -275,16 +277,14 @@ class AudioBeatDetector:
|
||||
)
|
||||
|
||||
def _clamp_holdover_bpm(self, bpm: Any) -> float | None:
|
||||
try:
|
||||
v = float(bpm)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if not (_HOLDOVER_BPM_MIN <= v <= _HOLDOVER_BPM_MAX):
|
||||
return None
|
||||
return v
|
||||
from util.bpm_limits import clamp_bpm_optional
|
||||
|
||||
return clamp_bpm_optional(bpm)
|
||||
|
||||
def _holdover_interval_s(self, bpm: float) -> float:
|
||||
return 60.0 / max(_HOLDOVER_BPM_MIN, min(_HOLDOVER_BPM_MAX, float(bpm)))
|
||||
from util.bpm_limits import clamp_bpm
|
||||
|
||||
return 60.0 / clamp_bpm(bpm)
|
||||
|
||||
def _stop_bpm_holdover(self) -> None:
|
||||
with self._lock:
|
||||
@@ -353,6 +353,12 @@ class AudioBeatDetector:
|
||||
return
|
||||
self._emit_holdover_beat(bpm)
|
||||
|
||||
def prime_bpm_holdover(self, bpm: float) -> None:
|
||||
"""Public: tick at *bpm* until the next detected beat (e.g. pending sequence switch)."""
|
||||
if not self._running:
|
||||
return
|
||||
self._start_bpm_holdover(bpm)
|
||||
|
||||
def _start_bpm_holdover(self, bpm: float) -> None:
|
||||
bpm_v = self._clamp_holdover_bpm(bpm)
|
||||
if bpm_v is None:
|
||||
@@ -434,8 +440,12 @@ class AudioBeatDetector:
|
||||
bpm = self._clamp_holdover_bpm(self._status.get("bpm"))
|
||||
holdover = self._holdover_active
|
||||
last_reset = self._last_gap_tempo_reset_ts
|
||||
if last_real is None or bpm is None:
|
||||
if last_real is None:
|
||||
return
|
||||
if bpm is None:
|
||||
from util.bpm_limits import clamp_bpm
|
||||
|
||||
bpm = clamp_bpm(120)
|
||||
try:
|
||||
gap = now - float(last_real)
|
||||
except (TypeError, ValueError):
|
||||
@@ -460,10 +470,15 @@ class AudioBeatDetector:
|
||||
self._last_gap_tempo_reset_ts = now
|
||||
|
||||
def _record_beat(self, bpm, beat_type="unknown", beat_type_confidence=0.0, **phase_fields):
|
||||
from util.bpm_limits import clamp_bpm_optional
|
||||
|
||||
self._stop_bpm_holdover()
|
||||
now = time.time()
|
||||
self._last_real_beat_ts = now
|
||||
bpm = clamp_bpm_optional(bpm)
|
||||
with self._lock:
|
||||
if bpm is None:
|
||||
bpm = clamp_bpm_optional(self._status.get("bpm"))
|
||||
self._last_gap_tempo_reset_ts = 0.0
|
||||
self._status["last_beat_ts"] = now
|
||||
self._status["bpm"] = bpm
|
||||
@@ -486,6 +501,12 @@ class AudioBeatDetector:
|
||||
seq_pb.push_thread_beat()
|
||||
except Exception as e:
|
||||
print(f"[audio] sequence beat queue: {e}")
|
||||
holdover_bpm = None
|
||||
with self._lock:
|
||||
if self._running:
|
||||
holdover_bpm = self._clamp_holdover_bpm(self._status.get("bpm"))
|
||||
if holdover_bpm is not None:
|
||||
self._start_bpm_holdover(holdover_bpm)
|
||||
|
||||
def _run_loop(self, device):
|
||||
try:
|
||||
@@ -525,6 +546,8 @@ class AudioBeatDetector:
|
||||
dev_info = sd.query_devices(device, "input")
|
||||
sample_rate = int(dev_info["default_samplerate"])
|
||||
|
||||
from util.bpm_limits import max_beat_min_ioi_ms
|
||||
|
||||
args = argparse.Namespace(
|
||||
mode="aubio",
|
||||
device=device,
|
||||
@@ -537,7 +560,7 @@ class AudioBeatDetector:
|
||||
flux_weight=0.3,
|
||||
threshold_multiplier=1.35,
|
||||
ema_alpha=0.08,
|
||||
min_ioi_ms=100.0,
|
||||
min_ioi_ms=max_beat_min_ioi_ms(),
|
||||
bpm_window=8,
|
||||
post_url="",
|
||||
aubio_method="default",
|
||||
@@ -645,6 +668,39 @@ def shared_beat_detector_running():
|
||||
return False
|
||||
|
||||
|
||||
def shared_beat_detector_timing_sequences() -> bool:
|
||||
"""True when live audio is running and has clocked a beat recently enough to drive sequences."""
|
||||
d = _shared_beat_detector
|
||||
if d is None:
|
||||
return False
|
||||
try:
|
||||
st = dict(d.status())
|
||||
except Exception:
|
||||
return False
|
||||
if not st.get("running"):
|
||||
return False
|
||||
with d._lock:
|
||||
last = d._last_real_beat_ts
|
||||
holdover = d._holdover_active
|
||||
if holdover:
|
||||
return True
|
||||
if last is None:
|
||||
return False
|
||||
try:
|
||||
gap = time.time() - float(last)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
from util.bpm_limits import clamp_bpm
|
||||
|
||||
bpm_raw = st.get("bpm")
|
||||
try:
|
||||
bpm_v = clamp_bpm(bpm_raw) if bpm_raw is not None else 120.0
|
||||
except (TypeError, ValueError):
|
||||
bpm_v = 120.0
|
||||
max_gap = (60.0 / bpm_v) * 2.0
|
||||
return gap < max_gap
|
||||
|
||||
|
||||
def shared_beat_status_snapshot() -> dict:
|
||||
"""Thread-safe copy of live detector status, or {} if audio is off."""
|
||||
d = _shared_beat_detector
|
||||
@@ -656,6 +712,17 @@ def shared_beat_status_snapshot() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def prime_bpm_holdover(bpm: float) -> None:
|
||||
"""Start BPM holdover on the shared detector when audio is on but not clocking."""
|
||||
d = _shared_beat_detector
|
||||
if d is None:
|
||||
return
|
||||
try:
|
||||
d.prime_bpm_holdover(bpm)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def anchor_shared_bar_phase() -> bool:
|
||||
"""Anchor bar phase on the shared detector (no-op if audio is off)."""
|
||||
d = _shared_beat_detector
|
||||
|
||||
Reference in New Issue
Block a user