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:
@@ -1,7 +1,9 @@
|
||||
(() => {
|
||||
let pollTimer = null;
|
||||
let beatEventSource = null;
|
||||
let beatEventsReconnectTimer = null;
|
||||
let audioDetectorRunning = false;
|
||||
let lastBeatSeq = 0;
|
||||
let lastSimulatedBeatTick = 0;
|
||||
/** Prior poll had server zone sequence playback active (`status.sequence.active === true`). */
|
||||
let prevZoneSequencePlaybackActive = false;
|
||||
/**
|
||||
@@ -14,26 +16,51 @@
|
||||
let cachedBeatPhaseMs = 0;
|
||||
/** @type {{ device: string|number|null, device_override: string, device_select: string }} */
|
||||
let cachedAudioRun = { device: null, device_override: "", device_select: "" };
|
||||
/** True after client starts sequence playback until server reports stop. */
|
||||
let clientSequenceUiActive = false;
|
||||
/** Last pass readout (e.g. ``6/6``) kept visible briefly after playback ends. */
|
||||
let stickySequenceBeatReadout = "";
|
||||
|
||||
function el(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
/** @param {Record<string, unknown>} status */
|
||||
function resolveBeatReadoutText(status) {
|
||||
let text = String((status && status.beat_readout) || "").trim();
|
||||
if (text) return text;
|
||||
const seq = /** @type {Record<string, unknown>|undefined} */ (
|
||||
status && status.sequence
|
||||
);
|
||||
if (seq && seq.active) {
|
||||
text = String(seq.beat_readout || "").trim();
|
||||
if (text) return text;
|
||||
}
|
||||
if (stickySequenceBeatReadout) {
|
||||
return stickySequenceBeatReadout;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** @param {Record<string, unknown>} status */
|
||||
function updateBeatReadoutDisplays(status) {
|
||||
const text = String((status && status.beat_readout) || "").trim();
|
||||
const text = resolveBeatReadoutText(status);
|
||||
for (const id of ["audio-top-beat-readout", "audio-modal-beat-readout"]) {
|
||||
const n = el(id);
|
||||
if (n) n.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
function updateBpmDisplay(bpm) {
|
||||
function updateBpmDisplay(bpm, simulated = false) {
|
||||
const text = Number.isFinite(bpm) ? bpm.toFixed(1) : "--";
|
||||
for (const id of ["audio-bpm-value", "audio-top-bpm-value"]) {
|
||||
const node = el(id);
|
||||
if (node) node.textContent = text;
|
||||
}
|
||||
for (const id of ["audio-top-indicator", "audio-modal-beat-sync"]) {
|
||||
const node = el(id);
|
||||
if (node) node.classList.toggle("audio-simulated", !!simulated);
|
||||
}
|
||||
}
|
||||
|
||||
/** Zone sequence playback (server); only when `active === true` is beat X/Y meaningful. */
|
||||
@@ -44,6 +71,43 @@
|
||||
return !!(seq && seq.active);
|
||||
}
|
||||
|
||||
/** Sequence playing or waiting on beat/downbeat before start (simulated beats still run). */
|
||||
function sequenceBeatUiActiveFromStatus(status) {
|
||||
if (sequencePlaybackActiveFromStatus(status)) return true;
|
||||
const pending = /** @type {Record<string, unknown>|undefined} */ (
|
||||
status && status.sequence_pending
|
||||
);
|
||||
return !!(pending && pending.pending);
|
||||
}
|
||||
|
||||
function resolveSeqUiActive(status) {
|
||||
return sequenceBeatUiActiveFromStatus(status) || clientSequenceUiActive;
|
||||
}
|
||||
|
||||
/** @param {Record<string, unknown>} status */
|
||||
function updateTopIndicatorFromStatus(status) {
|
||||
const running = !!(status && status.running);
|
||||
const bpmSimulated = !!(status && status.bpm_simulated);
|
||||
const seqUiActive = resolveSeqUiActive(status);
|
||||
const show = running || seqUiActive || bpmSimulated;
|
||||
setTopBpmVisible(show);
|
||||
if (!show || running) return;
|
||||
const simBpm =
|
||||
status && status.audio_simulated_bpm != null
|
||||
? Number(status.audio_simulated_bpm)
|
||||
: getSimulatedBpmPercent();
|
||||
updateBpmDisplay(Number.isFinite(simBpm) ? simBpm : null, true);
|
||||
}
|
||||
|
||||
/** @param {Record<string, unknown>} status */
|
||||
function shouldKeepStatusPolling(status) {
|
||||
return (
|
||||
!!(status && status.running) ||
|
||||
resolveSeqUiActive(status) ||
|
||||
!!(status && status.bpm_simulated)
|
||||
);
|
||||
}
|
||||
|
||||
function updateHitTypeDisplay(hitType, confidence) {
|
||||
const node = el("audio-hit-type-value");
|
||||
if (!node) return;
|
||||
@@ -57,6 +121,8 @@
|
||||
const readout = String((status && status.bar_phase_readout) || "").trim();
|
||||
const phaseConf = Number((status && status.phase_confidence) || 0);
|
||||
const downbeat = !!(status && status.is_downbeat);
|
||||
const simulated = !!(status && status.bpm_simulated);
|
||||
const showPhase = !!(status && status.running) || simulated;
|
||||
let text = readout || "--";
|
||||
if (readout && Number.isFinite(phaseConf) && phaseConf > 0) {
|
||||
text = `${text} (${Math.round(phaseConf * 100)}%)`;
|
||||
@@ -64,8 +130,8 @@
|
||||
for (const id of ["audio-bar-phase-value", "audio-top-bar-phase"]) {
|
||||
const node = el(id);
|
||||
if (!node) continue;
|
||||
node.textContent = status && status.running ? text : "";
|
||||
node.classList.toggle("is-downbeat", downbeat && !!readout);
|
||||
node.textContent = showPhase ? text : "";
|
||||
node.classList.toggle("is-downbeat", downbeat && !!readout && showPhase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +141,50 @@
|
||||
top.classList.toggle("audio-running", !!on);
|
||||
}
|
||||
|
||||
function closeBeatEvents() {
|
||||
if (beatEventsReconnectTimer != null) {
|
||||
clearTimeout(beatEventsReconnectTimer);
|
||||
beatEventsReconnectTimer = null;
|
||||
}
|
||||
if (beatEventSource) {
|
||||
beatEventSource.close();
|
||||
beatEventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleBeatEventsReconnect() {
|
||||
if (beatEventsReconnectTimer != null) return;
|
||||
beatEventsReconnectTimer = setTimeout(() => {
|
||||
beatEventsReconnectTimer = null;
|
||||
void fetchAudioStatusOnce()
|
||||
.then((status) => {
|
||||
applyAudioStatus(status);
|
||||
if (shouldKeepStatusPolling(status)) ensureBeatEvents();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn("audio status reconnect fetch failed", e);
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function ensureBeatEvents() {
|
||||
if (beatEventSource) return;
|
||||
const es = new EventSource("/api/audio/events");
|
||||
beatEventSource = es;
|
||||
es.onmessage = (ev) => {
|
||||
try {
|
||||
const data = JSON.parse(String(ev.data || ""));
|
||||
if (data && data.status) applyAudioStatus(data.status);
|
||||
} catch (e) {
|
||||
console.warn("audio beat event parse failed", e);
|
||||
}
|
||||
};
|
||||
es.onerror = () => {
|
||||
closeBeatEvents();
|
||||
scheduleBeatEventsReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
function setResetDetectorEnabled(on) {
|
||||
const btn = el("audio-reset-btn");
|
||||
if (btn) btn.disabled = !on;
|
||||
@@ -150,21 +260,25 @@
|
||||
return tag === "input" || tag === "textarea" || tag === "select" || target.isContentEditable;
|
||||
}
|
||||
|
||||
function flashBeatSyncButton(btn) {
|
||||
function flashBeatSyncButton(btn, simulated = false) {
|
||||
if (!btn) return;
|
||||
btn.classList.add("flash");
|
||||
setTimeout(() => btn.classList.remove("flash"), 90);
|
||||
btn.classList.add(simulated ? "flash-simulated" : "flash");
|
||||
setTimeout(() => btn.classList.remove(simulated ? "flash-simulated" : "flash"), 90);
|
||||
}
|
||||
|
||||
function flashBeat() {
|
||||
function flashBeat(simulated = false) {
|
||||
const top = el("audio-top-indicator");
|
||||
const topSync = el("audio-top-beat-sync");
|
||||
if (topSync && top && top.classList.contains("audio-running")) {
|
||||
flashBeatSyncButton(topSync);
|
||||
if (
|
||||
topSync &&
|
||||
top &&
|
||||
(top.classList.contains("audio-running") || simulated)
|
||||
) {
|
||||
flashBeatSyncButton(topSync, simulated);
|
||||
}
|
||||
const modalSync = el("audio-modal-beat-sync");
|
||||
if (modalSync && audioDetectorRunning) {
|
||||
flashBeatSyncButton(modalSync);
|
||||
if (modalSync && (audioDetectorRunning || simulated)) {
|
||||
flashBeatSyncButton(modalSync, simulated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +328,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
const SIMULATED_BPM_MIN = 60;
|
||||
const SIMULATED_BPM_MAX = 200;
|
||||
|
||||
function clampSimulatedBpm(n) {
|
||||
if (!Number.isFinite(n)) return 120;
|
||||
return Math.min(SIMULATED_BPM_MAX, Math.max(SIMULATED_BPM_MIN, Math.round(n)));
|
||||
}
|
||||
|
||||
function clampLiveBpm(n) {
|
||||
if (!Number.isFinite(n)) return null;
|
||||
return Math.min(SIMULATED_BPM_MAX, Math.max(SIMULATED_BPM_MIN, n));
|
||||
}
|
||||
|
||||
function getSimulatedBpmPercent() {
|
||||
const inp = el("audio-simulated-bpm");
|
||||
if (!inp) return 120;
|
||||
return clampSimulatedBpm(parseInt(String(inp.value).trim(), 10));
|
||||
}
|
||||
|
||||
async function persistSimulatedBpm() {
|
||||
const bpm = getSimulatedBpmPercent();
|
||||
try {
|
||||
await fetch("/settings", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify({ audio_simulated_bpm: bpm }),
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("simulated bpm save failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function persistInputVolume() {
|
||||
const vol = getInputVolumePercent();
|
||||
updateInputVolumeReadout();
|
||||
@@ -228,11 +374,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleBeatPhaseFire(seq, delayMs) {
|
||||
function scheduleBeatPhaseFire(seq, delayMs, simulated = false) {
|
||||
let tid = null;
|
||||
const run = () => {
|
||||
if (tid != null) pendingBeatPhaseTimers.delete(tid);
|
||||
flashBeat();
|
||||
flashBeat(simulated);
|
||||
try {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("ledControllerAudioBeat", { detail: { beatSeq: seq } }),
|
||||
@@ -252,23 +398,23 @@
|
||||
/** Stop detector and polling; does not clear “resume on load” prefs (used before restart). */
|
||||
async function stopAudioOnly() {
|
||||
audioDetectorRunning = false;
|
||||
setTopBpmVisible(false);
|
||||
setResetDetectorEnabled(false);
|
||||
clearBeatPhaseTimers();
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
lastBeatSeq = 0;
|
||||
lastSimulatedBeatTick = 0;
|
||||
prevZoneSequencePlaybackActive = false;
|
||||
headerBeatStickyIdleAfterSeq = false;
|
||||
updateBeatReadoutDisplays({});
|
||||
updateInputLevelDisplay(0);
|
||||
setTopBpmVisible(true);
|
||||
updateBpmDisplay(getSimulatedBpmPercent(), true);
|
||||
try {
|
||||
await fetch("/api/audio/stop", { method: "POST" });
|
||||
} catch (e) {
|
||||
console.warn("audio stop failed", e);
|
||||
}
|
||||
ensureBeatEvents();
|
||||
await pollStatus();
|
||||
}
|
||||
|
||||
/** User-initiated stop (run intent cleared on server). */
|
||||
@@ -276,11 +422,24 @@
|
||||
await stopAudioOnly();
|
||||
}
|
||||
|
||||
async function fetchAudioStatusOnce() {
|
||||
const res = await fetch("/api/audio/status", { cache: "no-store" });
|
||||
const data = await res.json();
|
||||
return data?.status || {};
|
||||
}
|
||||
|
||||
async function pollStatus() {
|
||||
try {
|
||||
const res = await fetch("/api/audio/status", { cache: "no-store" });
|
||||
const data = await res.json();
|
||||
const status = data?.status || {};
|
||||
const status = await fetchAudioStatusOnce();
|
||||
applyAudioStatus(status);
|
||||
} catch (e) {
|
||||
console.warn("audio status fetch failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {Record<string, unknown>} status */
|
||||
function applyAudioStatus(status) {
|
||||
try {
|
||||
if (status.error && String(status.error).trim()) {
|
||||
const node = el("audio-hit-type-value");
|
||||
if (node) {
|
||||
@@ -288,28 +447,41 @@
|
||||
}
|
||||
updateBeatReadoutDisplays({});
|
||||
audioDetectorRunning = !!status.running;
|
||||
updateBpmDisplay(null);
|
||||
updateInputLevelDisplay(0);
|
||||
setTopBpmVisible(!!status.running);
|
||||
updateTopIndicatorFromStatus(status);
|
||||
setResetDetectorEnabled(!!status.running);
|
||||
if (!status.running && pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
if (!shouldKeepStatusPolling(status)) closeBeatEvents();
|
||||
return;
|
||||
}
|
||||
audioDetectorRunning = !!status.running;
|
||||
const zoneSeqActive = sequencePlaybackActiveFromStatus(status);
|
||||
setTopBpmVisible(!!status.running || zoneSeqActive);
|
||||
const seqUiActive = resolveSeqUiActive(status);
|
||||
const bpmSimulated = !!status.bpm_simulated;
|
||||
if (sequenceBeatUiActiveFromStatus(status)) {
|
||||
clientSequenceUiActive = false;
|
||||
}
|
||||
updateTopIndicatorFromStatus(status);
|
||||
setResetDetectorEnabled(!!status.running);
|
||||
updateSequenceSyncControls(zoneSeqActive);
|
||||
updateBpmDisplay(status.bpm);
|
||||
updateSequenceSyncControls(zoneSeqActive || clientSequenceUiActive);
|
||||
const displayBpm =
|
||||
bpmSimulated && status.audio_simulated_bpm != null
|
||||
? clampSimulatedBpm(Number(status.audio_simulated_bpm))
|
||||
: status.bpm != null
|
||||
? clampLiveBpm(Number(status.bpm))
|
||||
: null;
|
||||
updateBpmDisplay(
|
||||
Number.isFinite(displayBpm) ? displayBpm : null,
|
||||
bpmSimulated,
|
||||
);
|
||||
updateHitTypeDisplay(status.beat_type, Number(status.beat_type_confidence));
|
||||
updateBarPhaseDisplay(status);
|
||||
updateInputLevelDisplay(
|
||||
status.running ? Number(status.input_level) : 0,
|
||||
);
|
||||
applyServerAudioUiFields(status);
|
||||
if (typeof window.setSequenceSwitchSimulatedMode === "function") {
|
||||
window.setSequenceSwitchSimulatedMode(bpmSimulated);
|
||||
}
|
||||
if (typeof window.applySequenceSwitchWaitFromServer === "function") {
|
||||
window.applySequenceSwitchWaitFromServer(status.sequence_switch_wait);
|
||||
}
|
||||
@@ -319,30 +491,56 @@
|
||||
* `sequence` on each poll.
|
||||
*/
|
||||
const beatSeq = Number(status.beat_seq || 0);
|
||||
const simTick = Number(status.simulated_beat_tick || 0);
|
||||
const endedSeq = prevZoneSequencePlaybackActive && !zoneSeqActive;
|
||||
const startedSeq = !prevZoneSequencePlaybackActive && zoneSeqActive;
|
||||
prevZoneSequencePlaybackActive = zoneSeqActive;
|
||||
if (startedSeq) {
|
||||
headerBeatStickyIdleAfterSeq = false;
|
||||
stickySequenceBeatReadout = "";
|
||||
if (bpmSimulated) {
|
||||
lastSimulatedBeatTick = Math.max(0, simTick - 1);
|
||||
}
|
||||
}
|
||||
if (zoneSeqActive) {
|
||||
const liveReadout = String((status.beat_readout || "") || "").trim()
|
||||
|| String((status.sequence && status.sequence.beat_readout) || "").trim();
|
||||
if (liveReadout) {
|
||||
stickySequenceBeatReadout = liveReadout;
|
||||
}
|
||||
}
|
||||
if (endedSeq) {
|
||||
clientSequenceUiActive = false;
|
||||
headerBeatStickyIdleAfterSeq = true;
|
||||
clearBeatPhaseTimers();
|
||||
lastBeatSeq = beatSeq;
|
||||
lastSimulatedBeatTick = simTick;
|
||||
if (!stickySequenceBeatReadout) {
|
||||
const tail = String((status.beat_readout || "") || "").trim();
|
||||
if (tail) stickySequenceBeatReadout = tail;
|
||||
}
|
||||
}
|
||||
if (!zoneSeqActive && headerBeatStickyIdleAfterSeq) {
|
||||
if (bpmSimulated && simTick > lastSimulatedBeatTick) {
|
||||
lastSimulatedBeatTick = simTick;
|
||||
scheduleBeatPhaseFire(simTick, getBeatPhaseDelayMs(), true);
|
||||
} else if (!zoneSeqActive && headerBeatStickyIdleAfterSeq) {
|
||||
if (beatSeq > lastBeatSeq) {
|
||||
lastBeatSeq = beatSeq;
|
||||
scheduleBeatPhaseFire(beatSeq, getBeatPhaseDelayMs());
|
||||
scheduleBeatPhaseFire(beatSeq, getBeatPhaseDelayMs(), false);
|
||||
headerBeatStickyIdleAfterSeq = false;
|
||||
}
|
||||
} else if (beatSeq > lastBeatSeq) {
|
||||
} else if (!bpmSimulated && beatSeq > lastBeatSeq) {
|
||||
lastBeatSeq = beatSeq;
|
||||
scheduleBeatPhaseFire(beatSeq, getBeatPhaseDelayMs());
|
||||
scheduleBeatPhaseFire(beatSeq, getBeatPhaseDelayMs(), false);
|
||||
}
|
||||
updateBeatReadoutDisplays(status);
|
||||
if (shouldKeepStatusPolling(status)) {
|
||||
ensureBeatEvents();
|
||||
} else {
|
||||
closeBeatEvents();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("audio status poll failed", e);
|
||||
console.warn("audio status apply failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,7 +677,7 @@
|
||||
setSelectedDeviceId(selected);
|
||||
updateBpmDisplay(null);
|
||||
updateHitTypeDisplay("unknown", NaN);
|
||||
pollTimer = setInterval(pollStatus, 250);
|
||||
ensureBeatEvents();
|
||||
await pollStatus();
|
||||
}
|
||||
|
||||
@@ -594,6 +792,17 @@
|
||||
});
|
||||
updateInputVolumeReadout();
|
||||
}
|
||||
const simBpmInp = el("audio-simulated-bpm");
|
||||
if (simBpmInp) {
|
||||
const onSimBpmChange = () => {
|
||||
void persistSimulatedBpm();
|
||||
if (!audioDetectorRunning) {
|
||||
updateBpmDisplay(getSimulatedBpmPercent(), true);
|
||||
}
|
||||
};
|
||||
simBpmInp.addEventListener("input", onSimBpmChange);
|
||||
simBpmInp.addEventListener("change", onSimBpmChange);
|
||||
}
|
||||
|
||||
for (const id of ["audio-top-beat-sync", "audio-modal-beat-sync"]) {
|
||||
const btn = el(id);
|
||||
@@ -614,22 +823,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function resumePollingIfDetectorRunning() {
|
||||
async function resumeBeatEventsIfNeeded() {
|
||||
try {
|
||||
const res = await fetch("/api/audio/status", { cache: "no-store" });
|
||||
const data = await res.json();
|
||||
const status = data?.status || {};
|
||||
const status = await fetchAudioStatusOnce();
|
||||
audioDetectorRunning = !!status.running;
|
||||
if (status.running && !pollTimer) {
|
||||
pollTimer = setInterval(pollStatus, 250);
|
||||
updateTopIndicatorFromStatus(status);
|
||||
if (shouldKeepStatusPolling(status)) {
|
||||
lastBeatSeq = Number(status.beat_seq || 0);
|
||||
lastSimulatedBeatTick = Number(status.simulated_beat_tick || 0);
|
||||
prevZoneSequencePlaybackActive = sequencePlaybackActiveFromStatus(status);
|
||||
await pollStatus();
|
||||
applyAudioStatus(status);
|
||||
ensureBeatEvents();
|
||||
} else {
|
||||
updateSequenceSyncControls(sequencePlaybackActiveFromStatus(status));
|
||||
updateSequenceSyncControls(
|
||||
sequencePlaybackActiveFromStatus(status) || clientSequenceUiActive,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("audio resume poll check failed", e);
|
||||
console.warn("audio resume status check failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,6 +873,17 @@
|
||||
updateInputVolumeReadout();
|
||||
}
|
||||
}
|
||||
const simBpmInp = el("audio-simulated-bpm");
|
||||
if (
|
||||
simBpmInp &&
|
||||
status.audio_simulated_bpm != null &&
|
||||
document.activeElement !== simBpmInp
|
||||
) {
|
||||
const bpm = parseInt(String(status.audio_simulated_bpm), 10);
|
||||
if (Number.isFinite(bpm)) {
|
||||
simBpmInp.value = String(clampSimulatedBpm(bpm));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadServerAudioUiFields() {
|
||||
@@ -677,27 +899,38 @@
|
||||
setSelectedDeviceId(saved);
|
||||
}
|
||||
updateInputLevelDisplay(status.running ? Number(status.input_level) : 0);
|
||||
updateTopIndicatorFromStatus(status);
|
||||
if (typeof window.setSequenceSwitchSimulatedMode === "function") {
|
||||
window.setSequenceSwitchSimulatedMode(!!status.bpm_simulated);
|
||||
}
|
||||
if (!status.running) {
|
||||
lastSimulatedBeatTick = Number(status.simulated_beat_tick || 0);
|
||||
applyAudioStatus(status);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("audio status load failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Called from sequences.js when server playback starts/stops without audio polling. */
|
||||
/** Called from sequences.js when server playback starts/stops. */
|
||||
window.ledControllerSequencePlaybackChanged = (active) => {
|
||||
clientSequenceUiActive = !!active;
|
||||
updateSequenceSyncControls(!!active);
|
||||
if (active) {
|
||||
setTopBpmVisible(true);
|
||||
if (!audioDetectorRunning) {
|
||||
updateBpmDisplay(getSimulatedBpmPercent(), true);
|
||||
}
|
||||
ensureBeatEvents();
|
||||
void pollStatus();
|
||||
return;
|
||||
}
|
||||
if (!pollTimer) {
|
||||
setTopBpmVisible(false);
|
||||
updateSequenceSyncControls(false);
|
||||
}
|
||||
void pollStatus();
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
bind();
|
||||
await loadServerAudioUiFields();
|
||||
await resumePollingIfDetectorRunning();
|
||||
await resumeBeatEventsIfNeeded();
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user