sync mode on
This commit is contained in:
1 parent
1818932244
commit
1174b328e5
415 files changed
+44294
-1182
No files matched your search
@@ -0,0 +1,805 @@
|
||||
// js/socket.js — V5.5 (Toast para Sync Mode)
|
||||
|
||||
// -------------------relómm------------------------------------------------------
|
||||
// IMPORTS & STATE
|
||||
// -----------------------------------------------------------------------------
|
||||
import { appState, resetProjectState } from "./state.js";
|
||||
import {
|
||||
addTrackToState,
|
||||
removeLastTrackFromState,
|
||||
updateTrackSample,
|
||||
} from "./pattern/pattern_state.js";
|
||||
import {
|
||||
addAudioTrackLane,
|
||||
removeAudioClip,
|
||||
addAudioClipToTimeline,
|
||||
updateAudioClipProperties,
|
||||
sliceAudioClip,
|
||||
getAudioSnapshot, // 👈 novo
|
||||
applyAudioSnapshot, // 👈 novo
|
||||
} from "./audio/audio_state.js";
|
||||
|
||||
import {
|
||||
togglePlayback,
|
||||
stopPlayback,
|
||||
rewindPlayback,
|
||||
} from "./pattern/pattern_audio.js";
|
||||
|
||||
import {
|
||||
startAudioEditorPlayback,
|
||||
stopAudioEditorPlayback,
|
||||
updateTransportLoop,
|
||||
seekAudioEditor, // 👈 Adicionado
|
||||
restartAudioEditorIfPlaying, // 👈 Adicionado
|
||||
} from "./audio/audio_audio.js";
|
||||
|
||||
import { parseMmpContent } from "./file.js";
|
||||
import { renderAll, showToast } from "./ui.js"; // showToast()
|
||||
import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Gera um ID único otimista (ex: "track_1678886401000_abc123")
|
||||
// -----------------------------------------------------------------------------
|
||||
function generateUniqueId(prefix = "item") {
|
||||
return `${prefix}_${Date.now()}_${Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 9)}`;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// CONFIGURAÇÃO DO SOCKET.IO
|
||||
// -----------------------------------------------------------------------------
|
||||
const socket = io("http://localhost:33007", {
|
||||
transports: ["websocket", "polling"],
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
let USER_NAME = `Alicer-${Math.floor(Math.random() * 9999)}`;
|
||||
let currentRoom = null;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// CLOCK SYNC — Sincroniza relógio cliente-servidor
|
||||
// -----------------------------------------------------------------------------
|
||||
let serverOffsetMs = 0;
|
||||
let rttMs = 0;
|
||||
|
||||
function sampleServerTime() {
|
||||
return new Promise((resolve) => {
|
||||
const t0 = Date.now();
|
||||
socket.emit("what_time_is_it", null, (reply) => {
|
||||
const t1 = Date.now();
|
||||
const rtt = t1 - t0;
|
||||
const oneWay = rtt / 2;
|
||||
const serverNow =
|
||||
reply && typeof reply === "object" ? reply.serverNowMs : reply;
|
||||
const offset = serverNow - (t0 + oneWay);
|
||||
resolve({ offset, rtt });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ewma(prev, next, alpha = 0.3) {
|
||||
if (prev === null || prev === undefined) return next;
|
||||
return prev * (1 - alpha) + next * alpha;
|
||||
}
|
||||
|
||||
async function syncServerTime(iterations = 5) {
|
||||
let off = null;
|
||||
let rtt = null;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
try {
|
||||
const s = await sampleServerTime();
|
||||
off = ewma(off, s.offset);
|
||||
rtt = ewma(rtt, s.rtt);
|
||||
} catch {}
|
||||
}
|
||||
if (off != null) serverOffsetMs = off;
|
||||
if (rtt != null) rttMs = rtt;
|
||||
}
|
||||
|
||||
function delayFromServerTimeMs(scheduleAtServerMs) {
|
||||
const localNow = Date.now();
|
||||
const serverNowEst = localNow + serverOffsetMs;
|
||||
return Math.max(0, scheduleAtServerMs - serverNowEst);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ESTADO DE TOKENS / ACK / FALLBACK
|
||||
// -----------------------------------------------------------------------------
|
||||
let lastActionTimeout = null;
|
||||
let lastBroadcastTimeout = null;
|
||||
let pendingToken = null;
|
||||
let lastActionToken = 0;
|
||||
const processedTokens = new Set();
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// FUNÇÕES AUXILIARES
|
||||
// -----------------------------------------------------------------------------
|
||||
export function setUserName(name) {
|
||||
USER_NAME = name;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// CONEXÃO / JOIN / LOGS
|
||||
// -----------------------------------------------------------------------------
|
||||
socket.on("connect", () => {
|
||||
console.log(`Conectado ao servidor com ID: ${socket.id}`);
|
||||
showToast("✅ Conectado ao servidor", "success");
|
||||
if (USER_NAME.startsWith("Alicer-")) {
|
||||
USER_NAME = `Alicer-${socket.id.substring(0, 4)}`;
|
||||
}
|
||||
const urlRoom = new URLSearchParams(window.location.search).get("room");
|
||||
currentRoom = urlRoom || null;
|
||||
|
||||
if (currentRoom) {
|
||||
console.log(`Modo Online. Sala detectada: ${currentRoom}`);
|
||||
showToast(`🎧 Conectado à sala ${currentRoom}`, "info");
|
||||
// Mostra o botão se estiver online
|
||||
const syncModeBtn = document.getElementById("sync-mode-btn");
|
||||
//if (syncModeBtn) syncModeBtn.style.display = ""; // Garante visibilidade
|
||||
} else {
|
||||
console.log("Modo Local. Conectado, mas não em uma sala.");
|
||||
showToast("🔌 Modo local (fora de sala)", "warning");
|
||||
// Esconde se offline
|
||||
const syncModeBtn = document.getElementById("sync-mode-btn");
|
||||
//if (syncModeBtn) syncModeBtn.style.display = "none";
|
||||
}
|
||||
|
||||
syncServerTime();
|
||||
setInterval(syncServerTime, 10000);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// DETECÇÃO DE AD BLOCK
|
||||
// -----------------------------------------------------------------------------
|
||||
socket.on("connect_error", (err) => {
|
||||
console.error("Falha crítica na conexão do Socket:", err.message);
|
||||
alert(
|
||||
"🚧 FALHA NA CONEXÃO 🚧\n\n❌Não foi possível conectar ao servidor em tempo real. ❌\n\n😅 Causa provável: Um Ad Blocker ou Firewall está bloqueando a conexão.\n\n😎 Por favor, desative seu Ad Blocker para este site e recarregue a página."
|
||||
);
|
||||
showToast(
|
||||
"❌ Falha grave de conexão. Desativa o Ad Blocker? 😅",
|
||||
"error",
|
||||
10000
|
||||
);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// RECEBER ESTADO SALVO DA SALA
|
||||
// -----------------------------------------------------------------------------
|
||||
socket.on("load_project_state", async (projectXml) => {
|
||||
console.log("Recebendo estado salvo da sala...");
|
||||
showToast("🔄 Recebendo estado atual da sala...", "info", 4000);
|
||||
if (isLoadingProject) return;
|
||||
isLoadingProject = true;
|
||||
try {
|
||||
await parseMmpContent(projectXml);
|
||||
renderAll();
|
||||
showToast("🎵 Projeto carregado com sucesso", "success");
|
||||
} catch (e) {
|
||||
console.error("Erro ao carregar projeto:", e);
|
||||
showToast("❌ Erro ao carregar projeto", "error");
|
||||
}
|
||||
isLoadingProject = false;
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ENTRAR NA SALA (Join)
|
||||
// -----------------------------------------------------------------------------
|
||||
export function joinRoom() {
|
||||
const urlRoom = new URLSearchParams(window.location.search).get("room");
|
||||
currentRoom = urlRoom || currentRoom;
|
||||
if (currentRoom) {
|
||||
console.log(`Entrando na sala: ${currentRoom} como ${USER_NAME}`);
|
||||
showToast(`🚪 Entrando na sala ${currentRoom}`, "info");
|
||||
socket.emit("join_room", { roomName: currentRoom, userName: USER_NAME });
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const hasAudio =
|
||||
(appState.audio?.clips?.length || 0) > 0 ||
|
||||
(appState.audio?.tracks?.length || 0) > 0;
|
||||
if (!hasAudio && currentRoom) {
|
||||
sendAction({ type: "AUDIO_SNAPSHOT_REQUEST" });
|
||||
}
|
||||
} catch {}
|
||||
}, 800);
|
||||
} else {
|
||||
console.warn("joinRoom() chamado, mas nenhuma sala encontrada na URL.");
|
||||
showToast("⚠️ Nenhuma sala encontrada na URL", "warning");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ENVIAR AÇÃO COM ACK + AGENDAMENTO DE TRANSPORTE
|
||||
// -----------------------------------------------------------------------------
|
||||
export function sendAction(action) {
|
||||
const inRoom = Boolean(currentRoom);
|
||||
|
||||
// (Blindagem de ID/Validação)
|
||||
if (action.type === "ADD_AUDIO_LANE" && !action.trackId) {
|
||||
action.trackId = generateUniqueId("track");
|
||||
console.log(`[SOCKET] ADD_AUDIO_LANE ID gerado: ${action.trackId}`);
|
||||
}
|
||||
if (action.type === "ADD_AUDIO_CLIP") {
|
||||
if (!action.clipId) {
|
||||
action.clipId = generateUniqueId("clip");
|
||||
}
|
||||
if (
|
||||
!action.trackId ||
|
||||
action.startTimeInSeconds == null ||
|
||||
isNaN(action.startTimeInSeconds)
|
||||
) {
|
||||
console.error("[SOCKET] ADD_AUDIO_CLIP bloqueada:", action);
|
||||
showToast("❌ Erro clip (inválido)", "error");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (action.type === "UPDATE_AUDIO_CLIP") {
|
||||
if (!action.clipId || !action.props) {
|
||||
console.error("[SOCKET] UPDATE_AUDIO_CLIP bloqueada (base):", action);
|
||||
return;
|
||||
}
|
||||
const { trackId, startTimeInSeconds } = action.props;
|
||||
if (
|
||||
trackId !== undefined &&
|
||||
(trackId == null || (typeof trackId === "number" && isNaN(trackId)))
|
||||
) {
|
||||
console.error("[SOCKET] UPDATE_AUDIO_CLIP bloqueada (trackId):", action);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
startTimeInSeconds !== undefined &&
|
||||
(startTimeInSeconds == null || isNaN(startTimeInSeconds))
|
||||
) {
|
||||
console.error(
|
||||
"[SOCKET] UPDATE_AUDIO_CLIP bloqueada (startTime):",
|
||||
action
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const token = (++lastActionToken).toString();
|
||||
action.__token = token;
|
||||
action.__senderId = socket.id;
|
||||
action.__senderName = USER_NAME;
|
||||
|
||||
// =================================================================
|
||||
// 👇 INÍCIO DA CORREÇÃO (Expandir `isTransport` para SET_SYNC_MODE)
|
||||
// =================================================================
|
||||
const isTransport =
|
||||
action.type === "TOGGLE_PLAYBACK" ||
|
||||
action.type === "STOP_PLAYBACK" ||
|
||||
action.type === "REWIND_PLAYBACK" ||
|
||||
action.type === "START_AUDIO_PLAYBACK" ||
|
||||
action.type === "STOP_AUDIO_PLAYBACK" ||
|
||||
action.type === "SET_LOOP_STATE" ||
|
||||
action.type === "SET_SEEK_TIME" ||
|
||||
action.type === "SET_SYNC_MODE"; // 👈 Adicionado
|
||||
// =================================================================
|
||||
// 👆 FIM DA CORREÇÃO
|
||||
// =================================================================
|
||||
|
||||
if (inRoom && isTransport) {
|
||||
const leadTimeMs = 200;
|
||||
const serverNowEst = Date.now() + serverOffsetMs;
|
||||
if (
|
||||
action.type === "TOGGLE_PLAYBACK" ||
|
||||
action.type === "START_AUDIO_PLAYBACK"
|
||||
) {
|
||||
action.scheduleAtServerMs = Math.round(serverNowEst + leadTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
// (Lógica Global/Local)
|
||||
if (!inRoom || (isTransport && appState.global.syncMode === "local")) {
|
||||
console.log("[SOCKET] (local) executando ação:", action.type);
|
||||
handleActionBroadcast(action);
|
||||
return;
|
||||
}
|
||||
if (isTransport && appState.global.syncMode === "global") {
|
||||
action.__syncMode = "global";
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[SOCKET] Enviando broadcast_action:",
|
||||
action.type,
|
||||
"para",
|
||||
currentRoom,
|
||||
"token=",
|
||||
token
|
||||
);
|
||||
|
||||
socket
|
||||
.compress(false)
|
||||
.emit("broadcast_action", { roomName: currentRoom, action }, (ack) => {
|
||||
if (ack && ack.ok && ack.token === token) {
|
||||
if (lastActionTimeout) {
|
||||
clearTimeout(lastActionTimeout);
|
||||
lastActionTimeout = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (lastActionTimeout) clearTimeout(lastActionTimeout);
|
||||
lastActionTimeout = setTimeout(() => {
|
||||
console.warn("[SOCKET] ACK não recebido:", action.type, token);
|
||||
showToast(`⚠️ ACK (${action.type})`, "warning");
|
||||
}, 500);
|
||||
|
||||
if (lastBroadcastTimeout) clearTimeout(lastBroadcastTimeout);
|
||||
pendingToken = token;
|
||||
lastBroadcastTimeout = setTimeout(() => {
|
||||
if (processedTokens.has(token)) return;
|
||||
console.warn("[SOCKET] Eco não recebido, fallback:", action.type, token);
|
||||
showToast(`⚠️ Eco (${action.type}), fallback`, "warning");
|
||||
processedTokens.add(token);
|
||||
handleActionBroadcast(action);
|
||||
}, 900);
|
||||
}
|
||||
|
||||
// HELPERS POP-UPS
|
||||
function basenameNoExt(path) {
|
||||
if (!path) return "";
|
||||
const base = String(path).split(/[\\/]/).pop();
|
||||
return base.replace(/\.[^/.]+$/, "");
|
||||
}
|
||||
function trackLabel(track, idx) {
|
||||
const name =
|
||||
track?.name ||
|
||||
track?.label ||
|
||||
track?.sampleName ||
|
||||
track?.sample?.displayName;
|
||||
return name ? `(Faixa ${idx + 1})` : `Faixa ${idx + 1}`;
|
||||
}
|
||||
function actorOf(action) {
|
||||
const n = action.__senderName || action.userName;
|
||||
if (n && typeof n === "string") return n;
|
||||
const sid = action.__senderId ? String(action.__senderId) : "";
|
||||
return `Alicer-${sid.slice(0, 4) || "????"}`;
|
||||
}
|
||||
function instrumentLabel(track, tIdx) {
|
||||
const name =
|
||||
track?.name ||
|
||||
track?.label ||
|
||||
track?.sampleName ||
|
||||
track?.sample?.displayName ||
|
||||
track?.filePath ||
|
||||
track?.sampleFile ||
|
||||
track?.samplePath;
|
||||
if (name && typeof name === "string") {
|
||||
const base = name.split(/[\\/]/).pop();
|
||||
return base.replace(/\.[^/.]+$/, "");
|
||||
}
|
||||
return `Faixa ${tIdx + 1}`;
|
||||
}
|
||||
|
||||
let rerenderScheduled = false;
|
||||
function schedulePatternRerender() {
|
||||
if (rerenderScheduled) return;
|
||||
rerenderScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
renderPatternEditor();
|
||||
rerenderScheduled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// RECEBER BROADCAST
|
||||
socket.on("feedback", (msg) => {
|
||||
console.log("[Servidor]", msg);
|
||||
showToast(msg, "info");
|
||||
});
|
||||
|
||||
socket.on("action_broadcast", (payload) => {
|
||||
const action = payload && payload.type ? payload : payload?.action || payload;
|
||||
if (action && action.__token) {
|
||||
processedTokens.add(action.__token);
|
||||
if (lastBroadcastTimeout && pendingToken === action.__token) {
|
||||
clearTimeout(lastBroadcastTimeout);
|
||||
lastBroadcastTimeout = null;
|
||||
pendingToken = null;
|
||||
}
|
||||
if (lastActionTimeout) {
|
||||
clearTimeout(lastActionTimeout);
|
||||
lastActionTimeout = null;
|
||||
}
|
||||
}
|
||||
handleActionBroadcast(action);
|
||||
});
|
||||
|
||||
// PROCESSAR AÇÕES RECEBIDAS
|
||||
let isLoadingProject = false;
|
||||
async function handleActionBroadcast(action) {
|
||||
if (!action || !action.type) return;
|
||||
if (
|
||||
action.type !== "LOAD_PROJECT" &&
|
||||
action.type !== "RESET_PROJECT" &&
|
||||
isLoadingProject
|
||||
) {
|
||||
console.warn(`[AÇÃO IGNORADA] ${action.type} (carregando).`);
|
||||
return;
|
||||
}
|
||||
|
||||
// (Filtro Global/Local)
|
||||
const isTransport =
|
||||
action.type === "TOGGLE_PLAYBACK" ||
|
||||
action.type === "STOP_PLAYBACK" ||
|
||||
action.type === "REWIND_PLAYBACK" ||
|
||||
action.type === "START_AUDIO_PLAYBACK" ||
|
||||
action.type === "STOP_AUDIO_PLAYBACK" ||
|
||||
action.type === "SET_LOOP_STATE" ||
|
||||
action.type === "SET_SEEK_TIME" ||
|
||||
action.type === "SET_SYNC_MODE"; // 👈 Adicionado
|
||||
const isFromSelf = action.__senderId === socket.id;
|
||||
if (isTransport && !isFromSelf) {
|
||||
if (appState.global.syncMode === "local") {
|
||||
console.log(`[SOCKET] (ignorado) ${action.type}, modo local.`);
|
||||
return;
|
||||
}
|
||||
if (action.__syncMode !== "global") {
|
||||
console.log(`[SOCKET] (ignorado) ${action.type}, remetente não global.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleAtServerMs = action.scheduleAtServerMs ?? null;
|
||||
const delayMs = scheduleAtServerMs
|
||||
? delayFromServerTimeMs(scheduleAtServerMs)
|
||||
: 0;
|
||||
|
||||
switch (action.type) {
|
||||
// Transporte Principal
|
||||
case "TOGGLE_PLAYBACK": {
|
||||
setTimeout(togglePlayback, delayMs);
|
||||
const who = actorOf(action);
|
||||
showToast(`▶ ${who} Play bases`, "info");
|
||||
break;
|
||||
}
|
||||
case "STOP_PLAYBACK": {
|
||||
setTimeout(stopPlayback, delayMs);
|
||||
const who = actorOf(action);
|
||||
showToast(`⏹ ${who} Stop bases`, "info");
|
||||
break;
|
||||
}
|
||||
case "REWIND_PLAYBACK":
|
||||
setTimeout(rewindPlayback, delayMs);
|
||||
break;
|
||||
|
||||
// Transporte Áudio Editor
|
||||
case "START_AUDIO_PLAYBACK":
|
||||
if (action.loopState) {
|
||||
appState.global.isLoopActive = action.loopState.isLoopActive;
|
||||
appState.global.loopStartTime = action.loopState.loopStartTime;
|
||||
appState.global.loopEndTime = action.loopState.loopEndTime;
|
||||
const btn = document.getElementById("audio-editor-loop-btn");
|
||||
if (btn) {
|
||||
btn.classList.toggle("active", appState.global.isLoopActive);
|
||||
}
|
||||
updateTransportLoop();
|
||||
try {
|
||||
const area = document.getElementById("loop-region");
|
||||
if (area)
|
||||
area.classList.toggle("visible", appState.global.isLoopActive);
|
||||
} catch (e) {}
|
||||
}
|
||||
const seekTime = action.seekTime ?? appState.audio.audioEditorSeekTime;
|
||||
setTimeout(() => startAudioEditorPlayback(seekTime), delayMs);
|
||||
break;
|
||||
case "STOP_AUDIO_PLAYBACK":
|
||||
setTimeout(
|
||||
() => stopAudioEditorPlayback(action.rewind || false),
|
||||
delayMs
|
||||
);
|
||||
break;
|
||||
|
||||
// =================================================================
|
||||
// 👇 INÍCIO DA CORREÇÃO (Handlers Sincronia de Loop/Seek/SyncMode)
|
||||
// =================================================================
|
||||
case "SET_LOOP_STATE": {
|
||||
const changed =
|
||||
appState.global.isLoopActive !== !!action.isLoopActive ||
|
||||
appState.global.loopStartTime !== action.loopStartTime ||
|
||||
appState.global.loopEndTime !== action.loopEndTime;
|
||||
|
||||
if (changed) {
|
||||
appState.global.isLoopActive = !!action.isLoopActive;
|
||||
appState.global.loopStartTime = action.loopStartTime;
|
||||
appState.global.loopEndTime = action.loopEndTime;
|
||||
|
||||
const btn = document.getElementById("audio-editor-loop-btn");
|
||||
if (btn) {
|
||||
btn.classList.toggle("active", appState.global.isLoopActive);
|
||||
}
|
||||
|
||||
updateTransportLoop();
|
||||
restartAudioEditorIfPlaying();
|
||||
renderAll();
|
||||
|
||||
if (!isFromSelf) {
|
||||
const who = actorOf(action);
|
||||
showToast(`🔁 ${who} alterou o loop.`, "info");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "SET_SEEK_TIME": {
|
||||
// Evita aplicar seek se o tempo for muito próximo para não causar "tremidas"
|
||||
if (
|
||||
Math.abs((appState.audio.audioEditorSeekTime || 0) - action.seekTime) >
|
||||
0.05
|
||||
) {
|
||||
seekAudioEditor(action.seekTime);
|
||||
if (!isFromSelf) {
|
||||
const who = actorOf(action);
|
||||
showToast(`⏯️ ${who} moveu a agulha.`, "info");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "SET_SYNC_MODE": {
|
||||
const newMode = action.mode === "local" ? "local" : "global";
|
||||
const changed = appState.global.syncMode !== newMode;
|
||||
if (changed) {
|
||||
appState.global.syncMode = newMode;
|
||||
const btn = document.getElementById("sync-mode-btn");
|
||||
if (btn) {
|
||||
btn.classList.toggle("active", newMode === "global");
|
||||
btn.textContent = newMode === "global" ? "Global" : "Local";
|
||||
}
|
||||
// Mostra o Toast AQUI, após a mudança ser aplicada
|
||||
const who = actorOf(action);
|
||||
const modeText = newMode === "global" ? "Global 🌐" : "Local 🏠";
|
||||
showToast(`${who} mudou modo para ${modeText}`, "info");
|
||||
}
|
||||
break;
|
||||
}
|
||||
// =================================================================
|
||||
// 👆 FIM DA CORREÇÃO
|
||||
// =================================================================
|
||||
|
||||
// Estado Global
|
||||
case "LOAD_PROJECT":
|
||||
isLoadingProject = true;
|
||||
showToast("📂 Carregando...", "info");
|
||||
try {
|
||||
await parseMmpContent(action.xml);
|
||||
renderAll();
|
||||
showToast("🎶 Projeto sync", "success");
|
||||
} catch (e) {
|
||||
console.error("Erro LOAD_PROJECT:", e);
|
||||
showToast("❌ Erro projeto", "error");
|
||||
}
|
||||
isLoadingProject = false;
|
||||
break;
|
||||
case "RESET_PROJECT":
|
||||
resetProjectState();
|
||||
document.getElementById("bpm-input").value = 140;
|
||||
document.getElementById("bars-input").value = 1;
|
||||
document.getElementById("compasso-a-input").value = 4;
|
||||
document.getElementById("compasso-b-input").value = 4;
|
||||
renderAll();
|
||||
const who = actorOf(action);
|
||||
showToast(`🧹 Reset por ${who}`, "warning");
|
||||
break;
|
||||
|
||||
// Configs
|
||||
case "SET_BPM": {
|
||||
document.getElementById("bpm-input").value = action.value;
|
||||
renderAll();
|
||||
const who = actorOf(action);
|
||||
showToast(`🕰 ${who} BPM ${action.value}`, "info");
|
||||
break;
|
||||
}
|
||||
case "SET_BARS": {
|
||||
document.getElementById("bars-input").value = action.value;
|
||||
renderAll();
|
||||
const who = actorOf(action);
|
||||
showToast(`🕰 ${who} Compasso add`, "info");
|
||||
break;
|
||||
}
|
||||
case "SET_TIMESIG_A":
|
||||
document.getElementById("compasso-a-input").value = action.value;
|
||||
renderAll();
|
||||
showToast("Compasso alt", "info");
|
||||
break;
|
||||
case "SET_TIMESIG_B":
|
||||
document.getElementById("compasso-b-input").value = action.value;
|
||||
renderAll();
|
||||
showToast("Compasso alt", "info");
|
||||
break;
|
||||
|
||||
// Tracks
|
||||
case "ADD_TRACK": {
|
||||
addTrackToState();
|
||||
renderPatternEditor();
|
||||
const who = actorOf(action);
|
||||
showToast(`🥁 Faixa add por ${who}`, "info");
|
||||
break;
|
||||
}
|
||||
case "REMOVE_LAST_TRACK": {
|
||||
removeLastTrackFromState();
|
||||
renderPatternEditor();
|
||||
const who = actorOf(action);
|
||||
showToast(`❌ Faixa remov. por ${who}`, "warning");
|
||||
break;
|
||||
}
|
||||
case "ADD_AUDIO_LANE": {
|
||||
const id = action.trackId;
|
||||
if (!id) {
|
||||
console.warn("ADD_AUDIO_LANE sem ID.");
|
||||
break;
|
||||
}
|
||||
const exists = appState.audio.tracks.some((t) => t.id === id);
|
||||
if (exists) {
|
||||
console.log(`ADD_AUDIO_LANE ${id} já existe.`);
|
||||
break;
|
||||
}
|
||||
appState.audio.tracks.push({
|
||||
id: id,
|
||||
name: `Pista ${appState.audio.tracks.length + 1}`,
|
||||
});
|
||||
renderAll();
|
||||
const who = actorOf(action);
|
||||
showToast(`🎧 Pista add por ${who}`, "info");
|
||||
break;
|
||||
}
|
||||
case "REMOVE_AUDIO_CLIP":
|
||||
if (removeAudioClip(action.clipId)) {
|
||||
appState.global.selectedClipId = null;
|
||||
renderAll();
|
||||
const who = actorOf(action);
|
||||
showToast(`🎚️ Clip remov. por ${who}`, "info");
|
||||
}
|
||||
break;
|
||||
|
||||
// Notes
|
||||
case "TOGGLE_NOTE": {
|
||||
const {
|
||||
trackIndex: ti,
|
||||
patternIndex: pi,
|
||||
stepIndex: si,
|
||||
isActive,
|
||||
} = action;
|
||||
const t = appState.pattern.tracks[ti];
|
||||
if (t) {
|
||||
t.patterns[pi] = t.patterns[pi] || { steps: [] };
|
||||
t.patterns[pi].steps[si] = isActive;
|
||||
try {
|
||||
updateStepUI(ti, pi, si, isActive);
|
||||
} catch {}
|
||||
if (!isFromSelf) {
|
||||
schedulePatternRerender();
|
||||
}
|
||||
}
|
||||
const who = actorOf(action);
|
||||
const v = isActive ? "+" : "-";
|
||||
showToast(`🎯 ${who} ${v} nota ${ti + 1}.${pi + 1}.${si + 1}`, "info");
|
||||
break;
|
||||
}
|
||||
|
||||
// Samples
|
||||
case "SET_TRACK_SAMPLE": {
|
||||
const ti = action.trackIndex;
|
||||
const t = appState.pattern.tracks[ti];
|
||||
if (t) {
|
||||
try {
|
||||
await updateTrackSample(ti, action.filePath);
|
||||
renderPatternEditor();
|
||||
const who = actorOf(action);
|
||||
const sn = basenameNoExt(action.filePath) || "?";
|
||||
const tl = trackLabel(t, ti);
|
||||
showToast(`🔊 ${who} trocou ${sn} em ${tl}`, "success");
|
||||
} catch (err) {
|
||||
console.error("Erro SET_TRACK_SAMPLE:", err);
|
||||
showToast("❌ Erro sample", "error");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Snapshots
|
||||
case "AUDIO_SNAPSHOT_REQUEST": {
|
||||
const clips = appState.audio?.clips?.length || 0,
|
||||
tracks = appState.audio?.tracks?.length || 0;
|
||||
const iHave = clips > 0 || tracks > 0;
|
||||
if (!iHave || isFromSelf) break;
|
||||
if (!window.__lastSnapshotSentAt) window.__lastSnapshotSentAt = 0;
|
||||
const now = Date.now();
|
||||
if (now - window.__lastSnapshotSentAt < 1500) break;
|
||||
window.__lastSnapshotSentAt = now;
|
||||
try {
|
||||
const snap = getAudioSnapshot();
|
||||
sendAction({
|
||||
type: "AUDIO_SNAPSHOT",
|
||||
snapshot: snap,
|
||||
__target: action.__senderId,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("Erro AUDIO_SNAPSHOT_REQUEST:", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "AUDIO_SNAPSHOT": {
|
||||
if (action.__target && action.__target !== socket.id) break;
|
||||
const hasClips = (appState.audio?.clips?.length || 0) > 0;
|
||||
if (hasClips) break;
|
||||
try {
|
||||
await applyAudioSnapshot(action.snapshot);
|
||||
renderAll();
|
||||
const who = actorOf(action);
|
||||
showToast(`🔁 Sync áudio por ${who}`, "success");
|
||||
} catch (e) {
|
||||
console.error("Erro AUDIO_SNAPSHOT:", e);
|
||||
showToast("❌ Erro sync áudio", "error");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Clip Sync
|
||||
case "ADD_AUDIO_CLIP": {
|
||||
try {
|
||||
const { filePath, trackId, startTimeInSeconds, clipId, name } = action;
|
||||
if (
|
||||
appState.audio?.clips?.some((c) => String(c.id) === String(clipId))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
const trackExists = appState.audio?.tracks?.some(
|
||||
(t) => t.id === trackId
|
||||
);
|
||||
if (!trackExists) {
|
||||
console.warn(
|
||||
`ADD_AUDIO_CLIP Pista ${trackId} não existe, criando...`
|
||||
);
|
||||
appState.audio.tracks.push({
|
||||
id: trackId,
|
||||
name: `Pista ${appState.audio.tracks.length + 1}`,
|
||||
});
|
||||
}
|
||||
addAudioClipToTimeline(
|
||||
filePath,
|
||||
trackId,
|
||||
startTimeInSeconds,
|
||||
clipId,
|
||||
name
|
||||
);
|
||||
renderAll();
|
||||
const who = actorOf(action);
|
||||
const track = appState.audio?.tracks?.find((t) => t.id === trackId);
|
||||
const pista = track?.name || `Pista ${trackId}`;
|
||||
showToast(
|
||||
`🎧 ${who} add “${(name || filePath || "")
|
||||
.split(/[\\/]/)
|
||||
.pop()}” em ${pista}`,
|
||||
"success"
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Erro ADD_AUDIO_CLIP:", e);
|
||||
showToast("❌ Erro add clip", "error");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "UPDATE_AUDIO_CLIP": {
|
||||
try {
|
||||
if (action.props?.__operation === "slice") {
|
||||
sliceAudioClip(action.clipId, action.props.sliceTimeInTimeline);
|
||||
} else {
|
||||
updateAudioClipProperties(action.clipId, action.props || {});
|
||||
}
|
||||
renderAll();
|
||||
const who = actorOf(action);
|
||||
showToast(`✂️ Clip ${action.clipId} att por ${who}`, "info");
|
||||
} catch (e) {
|
||||
console.error("Erro UPDATE_AUDIO_CLIP:", e);
|
||||
showToast("❌ Erro att clip", "error");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("Ação desconhecida:", action.type);
|
||||
}
|
||||
}
|
||||
|
||||
// EXPORTS
|
||||
export { socket };
|
||||
Reference in new issue
Block a user