feat(ui): lock zone type and start audio from BPM
Zone preset vs sequence is fixed at create; edit shows read-only type. Header BPM button starts beat detection when the detector is stopped. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
(() => {
|
||||
let pollTimer = null;
|
||||
let audioDetectorRunning = false;
|
||||
let lastBeatSeq = 0;
|
||||
let lastLoggedSequenceBeatFractions = "";
|
||||
/** Prior poll had server zone sequence playback active (`status.sequence.active === true`). */
|
||||
@@ -161,13 +162,37 @@
|
||||
|
||||
function updateSequenceSyncControls(zoneSeqActive) {
|
||||
const topSync = el("audio-top-beat-sync");
|
||||
if (topSync) topSync.disabled = !zoneSeqActive;
|
||||
if (topSync) {
|
||||
topSync.disabled = audioDetectorRunning && !zoneSeqActive;
|
||||
topSync.title = !audioDetectorRunning
|
||||
? "Start beat detection"
|
||||
: zoneSeqActive
|
||||
? "Sync step to music (S)"
|
||||
: "Beat detection running";
|
||||
}
|
||||
const modalBeat = el("audio-modal-beat-readout");
|
||||
if (modalBeat) modalBeat.disabled = !zoneSeqActive;
|
||||
const passBtn = el("audio-sync-pass-btn");
|
||||
if (passBtn) passBtn.disabled = !zoneSeqActive;
|
||||
}
|
||||
|
||||
async function handleTopBpmButtonClick() {
|
||||
if (!audioDetectorRunning) {
|
||||
try {
|
||||
await startAudio();
|
||||
} catch (e) {
|
||||
console.error("audio start failed", e);
|
||||
alert("Failed to start audio input. Check mic permissions.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await syncSequenceBeatPhase("step");
|
||||
} catch (e) {
|
||||
console.warn("sequence beat sync failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncSequenceBeatPhase(mode) {
|
||||
const res = await fetch("/sequences/sync-phase", {
|
||||
method: "POST",
|
||||
@@ -250,6 +275,7 @@
|
||||
|
||||
/** Stop detector and polling; does not clear “resume on load” prefs (used before restart). */
|
||||
async function stopAudioOnly() {
|
||||
audioDetectorRunning = false;
|
||||
setTopBpmVisible(false);
|
||||
setNavResetVisible(false);
|
||||
clearBeatPhaseTimers();
|
||||
@@ -285,6 +311,7 @@
|
||||
node.textContent = String(status.error).trim().slice(0, 120);
|
||||
}
|
||||
updateBeatReadoutDisplays({});
|
||||
audioDetectorRunning = !!status.running;
|
||||
updateBpmDisplay(null);
|
||||
setTopBpmVisible(!!status.running);
|
||||
setNavResetVisible(!!status.running);
|
||||
@@ -294,6 +321,7 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
audioDetectorRunning = !!status.running;
|
||||
const zoneSeqActive = sequencePlaybackActiveFromStatus(status);
|
||||
setTopBpmVisible(!!status.running || zoneSeqActive);
|
||||
setNavResetVisible(!!status.running);
|
||||
@@ -482,7 +510,12 @@
|
||||
}
|
||||
});
|
||||
};
|
||||
bindSync(el("audio-top-beat-sync"), "step");
|
||||
const topBpm = el("audio-top-beat-sync");
|
||||
if (topBpm) {
|
||||
topBpm.addEventListener("click", () => {
|
||||
void handleTopBpmButtonClick();
|
||||
});
|
||||
}
|
||||
bindSync(el("audio-modal-beat-readout"), "step");
|
||||
bindSync(el("audio-sync-pass-btn"), "pass");
|
||||
|
||||
@@ -501,11 +534,14 @@
|
||||
const res = await fetch("/api/audio/status", { cache: "no-store" });
|
||||
const data = await res.json();
|
||||
const status = data?.status || {};
|
||||
audioDetectorRunning = !!status.running;
|
||||
if (status.running && !pollTimer) {
|
||||
pollTimer = setInterval(pollStatus, 250);
|
||||
lastBeatSeq = Number(status.beat_seq || 0);
|
||||
prevZoneSequencePlaybackActive = sequencePlaybackActiveFromStatus(status);
|
||||
await pollStatus();
|
||||
} else {
|
||||
updateSequenceSyncControls(sequencePlaybackActiveFromStatus(status));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("audio resume poll check failed", e);
|
||||
|
||||
Reference in New Issue
Block a user