feat(audio-sequences): beat phase sync and aligned playback
Add bar-phase tracking, audio reset/anchor APIs, BPM holdover, beat-phase sequence switching, sync-phase endpoint, and sample sequence data. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,7 @@ def get_current_profile_id(session=None):
|
||||
@with_session
|
||||
async def list_sequences(request, session):
|
||||
"""List sequences for the current profile."""
|
||||
sequences.load()
|
||||
current_profile_id = get_current_profile_id(session)
|
||||
if not current_profile_id:
|
||||
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||
@@ -97,6 +98,7 @@ async def import_sequence(request, session):
|
||||
@with_session
|
||||
async def get_sequence(request, session, id):
|
||||
"""Get a specific sequence by ID (current profile only)."""
|
||||
sequences.load()
|
||||
current_profile_id = get_current_profile_id(session)
|
||||
seq = sequences.read(id)
|
||||
if (
|
||||
@@ -203,15 +205,46 @@ async def delete_sequence(request, session, id):
|
||||
return json.dumps({"error": "Sequence not found"}), 404
|
||||
|
||||
|
||||
@controller.post("/sync-phase")
|
||||
@with_session
|
||||
async def sync_sequence_beat_phase(request, session):
|
||||
"""Align beat counters while a sequence is playing (body: {\"mode\": \"step\"|\"pass\"})."""
|
||||
_ = session
|
||||
try:
|
||||
data = request.json or {}
|
||||
except Exception:
|
||||
data = {}
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
mode = data.get("mode") or data.get("align") or "step"
|
||||
try:
|
||||
from util.sequence_playback import sync_beat_phase
|
||||
|
||||
if not await sync_beat_phase(str(mode)):
|
||||
return (
|
||||
json.dumps({"error": "No sequence is playing"}),
|
||||
409,
|
||||
{"Content-Type": "application/json"},
|
||||
)
|
||||
from util.audio_detector import anchor_shared_bar_phase
|
||||
|
||||
anchor_shared_bar_phase()
|
||||
return json.dumps({"ok": True, "mode": str(mode).strip().lower()}), 200, {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}), 500, {"Content-Type": "application/json"}
|
||||
|
||||
|
||||
@controller.post("/stop")
|
||||
@with_session
|
||||
async def stop_sequence_playback(request, session):
|
||||
"""Stop server-driven zone sequence playback."""
|
||||
_ = request
|
||||
try:
|
||||
from util.sequence_playback import stop
|
||||
from util.sequence_playback import stop_playback
|
||||
|
||||
stop()
|
||||
await stop_playback(clear_devices=True)
|
||||
return json.dumps({"ok": True}), 200, {"Content-Type": "application/json"}
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}), 500, {"Content-Type": "application/json"}
|
||||
@@ -251,8 +284,12 @@ async def play_sequence(request, session, id):
|
||||
try:
|
||||
from util.sequence_playback import start
|
||||
|
||||
await start(zone_id, str(id), str(current_profile_id), data if isinstance(data, dict) else None)
|
||||
return json.dumps({"ok": True}), 200, {"Content-Type": "application/json"}
|
||||
play_opts = data if isinstance(data, dict) else None
|
||||
await start(zone_id, str(id), str(current_profile_id), play_opts)
|
||||
from util.sequence_playback import pending_play_status
|
||||
|
||||
body = {"ok": True, **pending_play_status()}
|
||||
return json.dumps(body), 200, {"Content-Type": "application/json"}
|
||||
except ValueError as e:
|
||||
return json.dumps({"error": str(e)}), 400, {"Content-Type": "application/json"}
|
||||
except RuntimeError as e:
|
||||
|
||||
Reference in New Issue
Block a user