806 lines
26 KiB
JavaScript
806 lines
26 KiB
JavaScript
// 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 };
|