corrigindo estado atual da sala, aparentemente, todos os clientes possuem o mesmo estado de sala com steps e samples de áudio

This commit is contained in:
JotaChina committed 2025-11-26 19:36:54 -03:00
1 parent 6eb81089f7
commit 3f714c88de
3 files changed
+265 -238

No files matched your search

+10 -29
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) {
+102 -59
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;
+153 -150
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):
* Lê 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;
}
}
}