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:
1 parent
6eb81089f7
commit
3f714c88de
3 files changed
+259
-232
No files matched your search
+10
-29
@@ -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) {
|
||||
|
||||
+103
-60
@@ -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 { 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();
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
const who = actorOf(action);
|
||||
const v = isActive ? "+" : "-";
|
||||
showToast(`🎯 ${who} ${v} nota ${ti + 1}.${pi + 1}.${si + 1}`, "info");
|
||||
// Salva o estado localmente também!
|
||||
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
|
||||
case "UPDATE_PATTERN_NOTES": {
|
||||
const {
|
||||
trackIndex: ti,
|
||||
patternIndex: pi,
|
||||
notes,
|
||||
steps: incomingSteps,
|
||||
} = action;
|
||||
|
||||
const t = appState.pattern.tracks[ti];
|
||||
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 {
|
||||
const xml = generateXmlFromStateExported(); // ← CORREÇÃO
|
||||
sendAction({
|
||||
type: "SYNC_PATTERN_STATE",
|
||||
xml,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Erro gerando XML UPDATE_PATTERN_NOTES", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 5) Salva no sessionStorage
|
||||
saveStateToSession();
|
||||
|
||||
break;
|
||||
}
|
||||
// Samples
|
||||
case "SET_TRACK_SAMPLE": {
|
||||
const ti = action.trackIndex;
|
||||
|
||||
+146
-143
@@ -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";
|
||||
|
||||
// --- 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
}));
|
||||
try {
|
||||
// Pattern “puro”
|
||||
const patternSnapshot = makePatternSnapshot();
|
||||
|
||||
// 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)
|
||||
}));
|
||||
// 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 stateToSave = {
|
||||
pattern: { ...appState.pattern, tracks: cleanPatternTracks },
|
||||
audio: {
|
||||
tracks: appState.audio.tracks || [], // Tracks são apenas containers leves
|
||||
clips: cleanAudioClips
|
||||
},
|
||||
const audioSnapshot = getAudioSnapshot();
|
||||
|
||||
const snapshot = {
|
||||
version: 1,
|
||||
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,
|
||||
...appState.global,
|
||||
playbackIntervalId: null,
|
||||
originalXmlDoc: originalXmlString,
|
||||
},
|
||||
pattern: patternSnapshot,
|
||||
audioSnapshot,
|
||||
};
|
||||
|
||||
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));
|
||||
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
|
||||
});
|
||||
// GLOBAL & PATTERN só se for "full"
|
||||
if (mode === "full") {
|
||||
if (data.global) {
|
||||
const { originalXmlDoc: xmlString, ...rest } = data.global;
|
||||
Object.assign(appState.global, rest);
|
||||
|
||||
// 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)
|
||||
if (xmlString) {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
appState.global.originalXmlDoc = parser.parseFromString(
|
||||
xmlString,
|
||||
"application/xml"
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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";
|
||||
} catch (e) {
|
||||
console.warn("Falha ao reconstituir originalXmlDoc:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appState.pattern.activeTrackId = tempState.pattern.activeTrackId;
|
||||
if (data.pattern) {
|
||||
Object.assign(appState.pattern, data.pattern);
|
||||
}
|
||||
}
|
||||
|
||||
// Á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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user