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
Deploy / Deploy (push) Successful in 2m22s
Details
This commit is contained in:
parent
5e331243b7
commit
dfe558be1c
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue