corrigindo estado atual da sala, aparentemente, todos os clientes possuem o mesmo estado de sala com steps e samples de áudio
Deploy / Deploy (push) Successful in 2m22s Details

This commit is contained in:
JotaChina 2025-11-26 19:36:54 -03:00
parent 5e331243b7
commit dfe558be1c
6 changed files with 665 additions and 238 deletions

View File

@ -228,46 +228,27 @@ document.addEventListener("DOMContentLoaded", () => {
// =================================================================
// 👇 INÍCIO DA CORREÇÃO (Botão de Sincronia - Agora envia Ação)
// =================================================================
const syncModeBtn = document.getElementById("sync-mode-btn"); //
const syncModeBtn = document.getElementById("sync-mode-btn");
if (syncModeBtn) {
//
// Define o estado inicial (global por padrão)
appState.global.syncMode = "global"; //
syncModeBtn.classList.add("active"); //
syncModeBtn.textContent = "Global"; //
// Só define default se ainda não existir
if (!appState.global.syncMode) {
appState.global.syncMode = "global";
}
syncModeBtn.classList.toggle("active", appState.global.syncMode === "global");
syncModeBtn.textContent =
appState.global.syncMode === "global" ? "Global" : "Local";
syncModeBtn.addEventListener("click", () => {
//
// 1. Determina qual será o *novo* modo
const newMode =
appState.global.syncMode === "global" ? "local" : "global"; //
appState.global.syncMode === "global" ? "local" : "global";
// 2. Envia a ação para sincronizar. O handleActionBroadcast
// cuidará de atualizar o appState, o botão e mostrar o toast.
sendAction({
type: "SET_SYNC_MODE",
mode: newMode,
});
// Lógica antiga removida daqui (movida para o handler)
/*
const isNowLocal = appState.global.syncMode === "global";
appState.global.syncMode = isNowLocal ? "local" : "global";
syncModeBtn.classList.toggle("active", !isNowLocal);
syncModeBtn.textContent = isNowLocal ? "Local" : "Global";
showToast( `🎧 Modo de Playback: ${isNowLocal ? "Local" : "Global"}`, "info" );
*/
});
// Esconde o botão se não estiver em uma sala (lógica movida do socket.js)
if (!ROOM_NAME) {
//
//syncModeBtn.style.display = 'none'; // REMOVIDO PARA TESTE VISUAL
}
}
// =================================================================
// 👆 FIM DA CORREÇÃO
// =================================================================
// Excluir clipe
if (deleteClipBtn) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -38,7 +38,9 @@ import {
handleLocalProjectReset,
syncPatternStateToServer,
BLANK_PROJECT_XML,
generateXmlFromStateExported,
} from "./file.js";
import { renderAll, showToast } from "./ui.js"; // showToast()
import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
import { PORT_SOCK } from "./config.js";
@ -170,18 +172,17 @@ socket.on("connect_error", (err) => {
);
});
// -----------------------------------------------------------------------------
// Atualizar os artefatos da plataforma
// -----------------------------------------------------------------------------
socket.on("system_update", (data) => {
if (data.type === "RELOAD_SAMPLES") {
console.log(
`[System Update] Recebida ordem para recarregar samples: ${data.message}`
);
// Certifique-se de que esta função existe e faz o que é esperado:
// 1. Fetch dos novos manifestos (metadata/samples-manifest.json etc.)
// 2. Renderiza a interface do navegador de samples
loadAndRenderSampleBrowser();
}
// Se houver outras notificações, adicione-as aqui (ex: RELOAD_PROJECTS)
});
// -----------------------------------------------------------------------------
@ -193,11 +194,28 @@ socket.on("load_project_state", async (projectXml) => {
if (isLoadingProject) return;
isLoadingProject = true;
try {
await parseMmpContent(projectXml); //
// 1. Carrega o XML que veio do servidor (pode estar desatualizado)
await parseMmpContent(projectXml);
// 🔥 CORREÇÃO: Força a restauração da sessão LOCAL logo após carregar o XML
// Isso garante que suas alterações locais (BPM, steps) "ganhem" do servidor
if (window.ROOM_NAME) {
const raw = sessionStorage.getItem(`temp_state_${window.ROOM_NAME}`);
if (raw) {
const parsed = JSON.parse(raw);
const hasLocalAudio = parsed.audioSnapshot?.clips?.length > 0 ||
parsed.audioSnapshot?.tracks?.length > 0;
if (hasLocalAudio) {
console.log("Re-aplicando sessão local sobre o XML do servidor...");
await loadStateFromSession();
}
}
}
renderAll();
showToast("🎵 Projeto carregado com sucesso", "success");
// --- INÍCIO DA CORREÇÃO ---
const hasAudio =
(appState.audio?.clips?.length || 0) > 0 ||
(appState.audio?.tracks?.length || 0) > 0;
@ -207,13 +225,12 @@ socket.on("load_project_state", async (projectXml) => {
"Projeto XML carregado, sem áudio. Pedindo snapshot de áudio..."
);
// 🔥 FIX CRÍTICO: Libera a trava IMEDIATAMENTE antes de pedir
// Libera a trava IMEDIATAMENTE antes de pedir
// Caso contrário, a resposta chega muito rápido e cai no bloqueio "justReset=true"
appState.global.justReset = false;
sendAction({ type: "AUDIO_SNAPSHOT_REQUEST" });
}
// --- FIM DA CORREÇÃO ---
} catch (e) {
console.error("Erro ao carregar projeto:", e);
@ -301,9 +318,6 @@ export function sendAction(action) {
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" ||
@ -312,10 +326,7 @@ export function sendAction(action) {
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
// =================================================================
action.type === "SET_SYNC_MODE";
if (inRoom && isTransport) {
const leadTimeMs = 200;
@ -375,12 +386,16 @@ export function sendAction(action) {
}, 900);
}
// HELPERS POP-UPS
// -----------------------------------------------------------------------------
// 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 ||
@ -389,12 +404,14 @@ function trackLabel(track, idx) {
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 ||
@ -421,7 +438,9 @@ function schedulePatternRerender() {
});
}
// RECEBER BROADCAST
// -----------------------------------------------------------------------------
// BROADCAST
// -----------------------------------------------------------------------------
socket.on("feedback", (msg) => {
console.log("[Servidor]", msg);
showToast(msg, "info");
@ -444,7 +463,9 @@ socket.on("action_broadcast", (payload) => {
handleActionBroadcast(action);
});
// PROCESSAR AÇÕES RECEBIDAS
// -----------------------------------------------------------------------------
// PROCESSAR AÇÕES
// -----------------------------------------------------------------------------
let isLoadingProject = false;
async function handleActionBroadcast(action) {
if (!action || !action.type) return;
@ -618,26 +639,7 @@ async function handleActionBroadcast(action) {
await parseMmpContent(action.xml); //
renderAll();
showToast("🎶 Projeto sync", "success");
// --- INÍCIO DA CORREÇÃO ---
// O problema é esta chamada. Ela cria um "eco" (SYNC_PATTERN_STATE)
// que força um segundo 'parseMmpContent' e quebra o estado.
// REMOVA ou COMENTE este bloco:
/*
if (window.ROOM_NAME) {
console.log(
"LOAD_PROJECT: Sincronizando novo estado com o servidor..."
);
syncPatternStateToServer(); //
}
*/
// EM VEZ DISSO, apenas salve o estado na sessão local.
// O `syncPatternStateToServer` agora é responsabilidade do servidor
// (que já recebeu o LOAD_PROJECT) ou de outras ações.
saveStateToSession();
// --- FIM DA CORREÇÃO ---
} catch (e) {
console.error("Erro LOAD_PROJECT:", e);
showToast("❌ Erro projeto", "error");
@ -661,15 +663,9 @@ async function handleActionBroadcast(action) {
case "RESET_ROOM":
console.log("Socket: Recebendo comando de RESET_ROOM do servidor."); //
// --- INÍCIO DA CORREÇÃO ---
// Faltava esta linha para definir quem executou a ação
const who = actorOf(action);
// --- FIM DA CORREÇÃO ---
handleLocalProjectReset(); //
showToast(`🧹 Reset por ${who}`, "warning"); //
handleLocalProjectReset();
showToast(`🧹 Reset por ${who}`, "warning");
// Salva o estado localmente também!
saveStateToSession(); //
break;
@ -768,31 +764,78 @@ async function handleActionBroadcast(action) {
// 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();
}
// ✔ ADICIONE ISTO
if (window.ROOM_NAME && isFromSelf) {
const xml = generateXmlFromStateExported();
sendAction({
type: "SYNC_PATTERN_STATE",
xml
});
}
saveStateToSession();
break;
}
case "UPDATE_PATTERN_NOTES": {
const {
trackIndex: ti,
patternIndex: pi,
stepIndex: si,
isActive,
notes,
steps: incomingSteps,
} = action;
const t = appState.pattern.tracks[ti];
if (t) {
t.patterns[pi] = t.patterns[pi] || { steps: [] };
t.patterns[pi].steps[si] = isActive;
if (!t) break;
// Garante o pattern
if (!t.patterns[pi]) t.patterns[pi] = { steps: [], notes: [] };
// 1) Atualiza notas
t.patterns[pi].notes = Array.isArray(notes) ? notes : [];
// 2) Se steps vieram junto, atualiza
if (Array.isArray(incomingSteps)) {
t.patterns[pi].steps = incomingSteps;
}
// 3) Atualiza interface
try {
schedulePatternRerender();
} catch (e) {
console.warn("Erro no render UPDATE_PATTERN_NOTES", e);
}
// 4) Sync online somente se fui EU quem enviei
const isFromSelf = action.__senderId === socket.id;
if (window.ROOM_NAME && isFromSelf) {
try {
updateStepUI(ti, pi, si, isActive);
} catch {}
if (!isFromSelf) {
schedulePatternRerender();
const xml = generateXmlFromStateExported(); // ← CORREÇÃO
sendAction({
type: "SYNC_PATTERN_STATE",
xml,
});
} catch (e) {
console.error("Erro gerando XML UPDATE_PATTERN_NOTES", e);
}
}
const who = actorOf(action);
const v = isActive ? "+" : "-";
showToast(`🎯 ${who} ${v} nota ${ti + 1}.${pi + 1}.${si + 1}`, "info");
// Salva o estado localmente também!
// 5) Salva no sessionStorage
saveStateToSession();
break;
}
// Samples
case "SET_TRACK_SAMPLE": {
const ti = action.trackIndex;

View File

@ -1,9 +1,14 @@
// js/state.js
import { audioState, initializeAudioState } from "./audio/audio_state.js";
// state.js (versão conceitual nova)
import {
audioState,
initializeAudioState,
getAudioSnapshot,
applyAudioSnapshot,
} from "./audio/audio_state.js";
import { DEFAULT_VOLUME, DEFAULT_PAN } from "./config.js";
import * as Tone from "https://esm.sh/tone";
import * as Tone from "https://esm.sh/tone";
// --- DEFINIÇÃO DOS ESTADOS INICIAIS ---
// ---------------- ESTADOS INICIAIS ----------------
const patternState = {
tracks: [],
@ -38,14 +43,82 @@ const globalState = {
export let appState = {
global: globalState,
pattern: patternState,
audio: audioState,
audio: audioState, // compartilhado com módulo de áudio
};
window.appState = appState;
// ---------------- HELPERS ----------------
function makePatternSnapshot() {
return {
tracks: appState.pattern.tracks.map((t) => ({
id: t.id,
name: t.name,
type: t.type,
samplePath: t.samplePath,
patterns: (t.patterns || []).map((p) => ({
name: p.name,
steps: p.steps,
notes: p.notes,
pos: p.pos,
})),
activePatternIndex: t.activePatternIndex || 0,
volume: t.volume,
pan: t.pan,
instrumentName: t.instrumentName,
instrumentXml: t.instrumentXml,
})),
activeTrackId: appState.pattern.activeTrackId,
activePatternIndex: appState.pattern.activePatternIndex,
};
}
// ----------------------
// Helper: existe snapshot local com áudio?
// ----------------------
export function hasLocalAudioSnapshot() {
if (!window.ROOM_NAME) return false;
const raw = sessionStorage.getItem(`temp_state_${window.ROOM_NAME}`);
if (!raw) return false;
try {
const data = JSON.parse(raw);
// se tem tracks ou clips, consideramos snapshot válido
const audio = data.audioSnapshot;
if (!audio) return false;
const hasTracks = audio.tracks && audio.tracks.length > 0;
const hasClips = audio.clips && audio.clips.length > 0;
return hasTracks || hasClips;
} catch (e) {
console.warn("hasLocalAudioSnapshot: erro ao analisar snapshot", e);
return false;
}
}
// ----------------------
// Helper: checa se um snapshot de áudio é "não vazio"
// ----------------------
function hasAudioInSnapshot(audioSnapshot) {
if (!audioSnapshot) return false;
const hasTracks =
Array.isArray(audioSnapshot.tracks) && audioSnapshot.tracks.length > 0;
const hasClips =
Array.isArray(audioSnapshot.clips) && audioSnapshot.clips.length > 0;
return hasTracks || hasClips;
}
// ---------------- RESET GERAL ----------------
export function resetProjectState() {
console.log("Executando resetProjectState (Limpeza Profunda)...");
// 1. Reseta Global
Object.assign(appState.global, {
sliceToolActive: false,
isPlaying: false,
@ -67,180 +140,110 @@ export function resetProjectState() {
clipboard: null,
lastRulerClickTime: 0,
justReset: false,
// syncMode mantemos o que estava
syncMode: appState.global.syncMode ?? "global",
});
// 2. Reseta Pattern
Object.assign(appState.pattern, {
tracks: [],
activeTrackId: null,
activePatternIndex: 0,
});
// 3. Reseta Áudio (Remove tudo da memória)
if (appState.audio) {
appState.audio.tracks = [];
appState.audio.clips = [];
appState.audio.audioEditorSeekTime = 0;
}
initializeAudioState();
initializeAudioState();
}
/**
* SALVAR (Serialization):
* Transforma o estado complexo em um JSON leve (apenas strings e números).
* Remove objetos cíclicos como AudioBuffers e Nodes do Tone.js.
*/
// ---------------- SALVAR SESSÃO LOCAL ----------------
export function saveStateToSession() {
if (!window.ROOM_NAME) return;
// 1. Sanitiza Tracks do Pattern (Instrumentos)
const cleanPatternTracks = appState.pattern.tracks.map((track) => ({
id: track.id,
name: track.name,
samplePath: track.samplePath, // Caminho do arquivo (String)
patterns: track.patterns,
activePatternIndex: track.activePatternIndex,
volume: track.volume,
pan: track.pan,
instrumentName: track.instrumentName,
instrumentXml: track.instrumentXml,
// Note: NÃO salvamos volumeNode, pannerNode, etc.
}));
// 2. Sanitiza Clipes de Áudio (Timeline)
// AQUI ESTÁ O SEGREDO: Salvamos apenas o 'filePath', não o buffer de áudio.
const cleanAudioClips = (appState.audio.clips || []).map((clip) => ({
id: clip.id,
trackId: clip.trackId,
name: clip.name,
filePath: clip.filePath, // O endereço do áudio
startTimeInSeconds: clip.startTimeInSeconds,
durationInSeconds: clip.durationInSeconds,
offset: clip.offset,
pitch: clip.pitch,
originalDuration: clip.originalDuration,
patternData: clip.patternData, // Visualização dos steps (leve)
// Note: NÃO salvamos clip.buffer (que é o áudio pesado)
}));
const stateToSave = {
pattern: { ...appState.pattern, tracks: cleanPatternTracks },
audio: {
tracks: appState.audio.tracks || [], // Tracks são apenas containers leves
clips: cleanAudioClips
},
global: {
bpm: document.getElementById("bpm-input")?.value || 140,
compassoA: document.getElementById("compasso-a-input")?.value || 4,
compassoB: document.getElementById("compasso-b-input")?.value || 4,
bars: document.getElementById("bars-input")?.value || 1,
syncMode: appState.global.syncMode,
},
};
try {
const roomName = window.ROOM_NAME || "default_room";
// Agora o JSON.stringify funciona porque só tem dados simples
sessionStorage.setItem(`temp_state_${roomName}`, JSON.stringify(stateToSave));
// Pattern “puro”
const patternSnapshot = makePatternSnapshot();
// XML original em string
let originalXmlString = null;
if (appState.global.originalXmlDoc) {
const serializer = new XMLSerializer();
try {
originalXmlString = serializer.serializeToString(
appState.global.originalXmlDoc
);
} catch (e) {
console.warn("Não consegui serializar originalXmlDoc:", e);
}
}
const audioSnapshot = getAudioSnapshot();
const snapshot = {
version: 1,
global: {
...appState.global,
playbackIntervalId: null,
originalXmlDoc: originalXmlString,
},
pattern: patternSnapshot,
audioSnapshot,
};
sessionStorage.setItem(
`temp_state_${window.ROOM_NAME}`,
JSON.stringify(snapshot)
);
} catch (e) {
console.error("Erro ao salvar sessão:", e);
console.error("Erro salvando sessão:", e);
}
}
/**
* CARREGAR (Hydration):
* o JSON leve e RECONSTRÓI os objetos pesados (carrega os arquivos via HTTP).
*/
export async function loadStateFromSession() {
const roomName = window.ROOM_NAME || "default_room";
const tempStateJSON = sessionStorage.getItem(`temp_state_${roomName}`);
// ---------------- CARREGAR SESSÃO LOCAL ----------------
// mode:
// - "full" (global + pattern + audio)
// - "audioOnly" (apenas áudio, usado como fallback
// depois de carregar XML do servidor)
if (!tempStateJSON) return false;
export async function loadStateFromSession(mode = "full") {
if (!window.ROOM_NAME) return false;
const raw = sessionStorage.getItem(`temp_state_${window.ROOM_NAME}`);
if (!raw) return false;
console.log("Hidratando estado da sessão...");
try {
const tempState = JSON.parse(tempStateJSON);
const data = JSON.parse(raw);
// 1. Restaura Pattern Tracks
// Precisamos recriar os Nodes do Tone.js que não foram salvos
appState.pattern.tracks.forEach((liveTrack) => {
const savedTrack = tempState.pattern.tracks.find(t => t.id === liveTrack.id);
if (savedTrack) {
// Copia dados simples
Object.assign(liveTrack, {
name: savedTrack.name,
patterns: savedTrack.patterns,
activePatternIndex: savedTrack.activePatternIndex,
volume: savedTrack.volume,
pan: savedTrack.pan
});
// Reconecta Nodes de Áudio (Hidratação)
if (liveTrack.volumeNode) liveTrack.volumeNode.volume.value = Tone.gainToDb(savedTrack.volume);
if (liveTrack.pannerNode) liveTrack.pannerNode.pan.value = savedTrack.pan;
}
});
// Sincroniza lista de tracks (remove deletadas)
appState.pattern.tracks = appState.pattern.tracks.filter(liveTrack =>
tempState.pattern.tracks.some(t => t.id === liveTrack.id)
);
// GLOBAL & PATTERN só se for "full"
if (mode === "full") {
if (data.global) {
const { originalXmlDoc: xmlString, ...rest } = data.global;
Object.assign(appState.global, rest);
// 2. Restaura Áudio Timeline (A parte mais importante)
if (tempState.audio) {
appState.audio.tracks = tempState.audio.tracks || [];
const clipsMetadata = tempState.audio.clips || [];
const loadedClips = [];
console.log(`Recarregando ${clipsMetadata.length} clips de áudio...`);
// Para cada clipe salvo, baixamos o áudio novamente usando o filePath
for (const metaClip of clipsMetadata) {
let buffer = null;
if (metaClip.filePath) {
try {
// Tone.Buffer carrega o arquivo .wav/.mp3 da URL
buffer = await new Tone.Buffer(metaClip.filePath).loaded;
} catch (err) {
console.warn(`Arquivo não encontrado: ${metaClip.filePath}`, err);
}
if (xmlString) {
try {
const parser = new DOMParser();
appState.global.originalXmlDoc = parser.parseFromString(
xmlString,
"application/xml"
);
} catch (e) {
console.warn("Falha ao reconstituir originalXmlDoc:", e);
}
}
// Recria o objeto completo na memória
loadedClips.push({
...metaClip, // Pega id, start, duration, pitch...
buffer: buffer // Anexa o áudio pesado recém-carregado
});
}
appState.audio.clips = loadedClips;
}
// 3. Restaura Global UI
if (tempState.global) {
const g = tempState.global;
const setVal = (id, val) => { const el = document.getElementById(id); if(el) el.value = val; };
setVal("bpm-input", g.bpm);
setVal("compasso-a-input", g.compassoA);
setVal("compasso-b-input", g.compassoB);
setVal("bars-input", g.bars);
if (g.syncMode) {
appState.global.syncMode = g.syncMode;
const btn = document.getElementById("sync-mode-btn");
if (btn) {
btn.classList.toggle("active", g.syncMode === "global");
btn.textContent = g.syncMode === "global" ? "Global" : "Local";
}
if (data.pattern) {
Object.assign(appState.pattern, data.pattern);
}
}
appState.pattern.activeTrackId = tempState.pattern.activeTrackId;
// ÁUDIO em qualquer modo, se existir
if (hasAudioInSnapshot(data.audioSnapshot)) {
initializeAudioState();
await applyAudioSnapshot(data.audioSnapshot);
}
return true;
} catch (e) {
console.error("Erro crítico ao carregar sessão:", e);
console.error("Erro carregando sessão:", e);
return false;
}
}
}