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) // 👇 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) { if (syncModeBtn) {
// // Só define default se ainda não existir
// Define o estado inicial (global por padrão) if (!appState.global.syncMode) {
appState.global.syncMode = "global"; // appState.global.syncMode = "global";
syncModeBtn.classList.add("active"); // }
syncModeBtn.textContent = "Global"; //
syncModeBtn.classList.toggle("active", appState.global.syncMode === "global");
syncModeBtn.textContent =
appState.global.syncMode === "global" ? "Global" : "Local";
syncModeBtn.addEventListener("click", () => { syncModeBtn.addEventListener("click", () => {
//
// 1. Determina qual será o *novo* modo
const newMode = 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({ sendAction({
type: "SET_SYNC_MODE", type: "SET_SYNC_MODE",
mode: newMode, 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 // Excluir clipe
if (deleteClipBtn) { 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, handleLocalProjectReset,
syncPatternStateToServer, syncPatternStateToServer,
BLANK_PROJECT_XML, BLANK_PROJECT_XML,
generateXmlFromStateExported,
} from "./file.js"; } from "./file.js";
import { renderAll, showToast } from "./ui.js"; // showToast() import { renderAll, showToast } from "./ui.js"; // showToast()
import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js"; import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
import { PORT_SOCK } from "./config.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) => { socket.on("system_update", (data) => {
if (data.type === "RELOAD_SAMPLES") { if (data.type === "RELOAD_SAMPLES") {
console.log( console.log(
`[System Update] Recebida ordem para recarregar samples: ${data.message}` `[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(); 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; if (isLoadingProject) return;
isLoadingProject = true; isLoadingProject = true;
try { 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(); renderAll();
showToast("🎵 Projeto carregado com sucesso", "success"); showToast("🎵 Projeto carregado com sucesso", "success");
// --- INÍCIO DA CORREÇÃO ---
const hasAudio = const hasAudio =
(appState.audio?.clips?.length || 0) > 0 || (appState.audio?.clips?.length || 0) > 0 ||
(appState.audio?.tracks?.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..." "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" // Caso contrário, a resposta chega muito rápido e cai no bloqueio "justReset=true"
appState.global.justReset = false; appState.global.justReset = false;
sendAction({ type: "AUDIO_SNAPSHOT_REQUEST" }); sendAction({ type: "AUDIO_SNAPSHOT_REQUEST" });
} }
// --- FIM DA CORREÇÃO ---
} catch (e) { } catch (e) {
console.error("Erro ao carregar projeto:", e); console.error("Erro ao carregar projeto:", e);
@ -301,9 +318,6 @@ export function sendAction(action) {
action.__senderId = socket.id; action.__senderId = socket.id;
action.__senderName = USER_NAME; action.__senderName = USER_NAME;
// =================================================================
// 👇 INÍCIO DA CORREÇÃO (Expandir `isTransport` para SET_SYNC_MODE)
// =================================================================
const isTransport = const isTransport =
action.type === "TOGGLE_PLAYBACK" || action.type === "TOGGLE_PLAYBACK" ||
action.type === "STOP_PLAYBACK" || action.type === "STOP_PLAYBACK" ||
@ -312,10 +326,7 @@ export function sendAction(action) {
action.type === "STOP_AUDIO_PLAYBACK" || action.type === "STOP_AUDIO_PLAYBACK" ||
action.type === "SET_LOOP_STATE" || action.type === "SET_LOOP_STATE" ||
action.type === "SET_SEEK_TIME" || action.type === "SET_SEEK_TIME" ||
action.type === "SET_SYNC_MODE"; // 👈 Adicionado action.type === "SET_SYNC_MODE";
// =================================================================
// 👆 FIM DA CORREÇÃO
// =================================================================
if (inRoom && isTransport) { if (inRoom && isTransport) {
const leadTimeMs = 200; const leadTimeMs = 200;
@ -375,12 +386,16 @@ export function sendAction(action) {
}, 900); }, 900);
} }
// HELPERS POP-UPS
// -----------------------------------------------------------------------------
// POP-UPS
// -----------------------------------------------------------------------------
function basenameNoExt(path) { function basenameNoExt(path) {
if (!path) return ""; if (!path) return "";
const base = String(path).split(/[\\/]/).pop(); const base = String(path).split(/[\\/]/).pop();
return base.replace(/\.[^/.]+$/, ""); return base.replace(/\.[^/.]+$/, "");
} }
function trackLabel(track, idx) { function trackLabel(track, idx) {
const name = const name =
track?.name || track?.name ||
@ -389,12 +404,14 @@ function trackLabel(track, idx) {
track?.sample?.displayName; track?.sample?.displayName;
return name ? `(Faixa ${idx + 1})` : `Faixa ${idx + 1}`; return name ? `(Faixa ${idx + 1})` : `Faixa ${idx + 1}`;
} }
function actorOf(action) { function actorOf(action) {
const n = action.__senderName || action.userName; const n = action.__senderName || action.userName;
if (n && typeof n === "string") return n; if (n && typeof n === "string") return n;
const sid = action.__senderId ? String(action.__senderId) : ""; const sid = action.__senderId ? String(action.__senderId) : "";
return `Alicer-${sid.slice(0, 4) || "????"}`; return `Alicer-${sid.slice(0, 4) || "????"}`;
} }
function instrumentLabel(track, tIdx) { function instrumentLabel(track, tIdx) {
const name = const name =
track?.name || track?.name ||
@ -421,7 +438,9 @@ function schedulePatternRerender() {
}); });
} }
// RECEBER BROADCAST // -----------------------------------------------------------------------------
// BROADCAST
// -----------------------------------------------------------------------------
socket.on("feedback", (msg) => { socket.on("feedback", (msg) => {
console.log("[Servidor]", msg); console.log("[Servidor]", msg);
showToast(msg, "info"); showToast(msg, "info");
@ -444,7 +463,9 @@ socket.on("action_broadcast", (payload) => {
handleActionBroadcast(action); handleActionBroadcast(action);
}); });
// PROCESSAR AÇÕES RECEBIDAS // -----------------------------------------------------------------------------
// PROCESSAR AÇÕES
// -----------------------------------------------------------------------------
let isLoadingProject = false; let isLoadingProject = false;
async function handleActionBroadcast(action) { async function handleActionBroadcast(action) {
if (!action || !action.type) return; if (!action || !action.type) return;
@ -618,26 +639,7 @@ async function handleActionBroadcast(action) {
await parseMmpContent(action.xml); // await parseMmpContent(action.xml); //
renderAll(); renderAll();
showToast("🎶 Projeto sync", "success"); 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(); saveStateToSession();
// --- FIM DA CORREÇÃO ---
} catch (e) { } catch (e) {
console.error("Erro LOAD_PROJECT:", e); console.error("Erro LOAD_PROJECT:", e);
showToast("❌ Erro projeto", "error"); showToast("❌ Erro projeto", "error");
@ -661,15 +663,9 @@ async function handleActionBroadcast(action) {
case "RESET_ROOM": case "RESET_ROOM":
console.log("Socket: Recebendo comando de RESET_ROOM do servidor."); // 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); 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! // Salva o estado localmente também!
saveStateToSession(); // saveStateToSession(); //
break; break;
@ -768,31 +764,78 @@ async function handleActionBroadcast(action) {
// Notes // Notes
case "TOGGLE_NOTE": { 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 { const {
trackIndex: ti, trackIndex: ti,
patternIndex: pi, patternIndex: pi,
stepIndex: si, notes,
isActive, steps: incomingSteps,
} = action; } = action;
const t = appState.pattern.tracks[ti]; const t = appState.pattern.tracks[ti];
if (t) { if (!t) break;
t.patterns[pi] = t.patterns[pi] || { steps: [] };
t.patterns[pi].steps[si] = isActive; // 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 { try {
updateStepUI(ti, pi, si, isActive); const xml = generateXmlFromStateExported(); // ← CORREÇÃO
} catch {} sendAction({
if (!isFromSelf) { type: "SYNC_PATTERN_STATE",
schedulePatternRerender(); xml,
});
} catch (e) {
console.error("Erro gerando XML UPDATE_PATTERN_NOTES", e);
} }
} }
const who = actorOf(action);
const v = isActive ? "+" : "-"; // 5) Salva no sessionStorage
showToast(`🎯 ${who} ${v} nota ${ti + 1}.${pi + 1}.${si + 1}`, "info");
// Salva o estado localmente também!
saveStateToSession(); saveStateToSession();
break; break;
} }
// Samples // Samples
case "SET_TRACK_SAMPLE": { case "SET_TRACK_SAMPLE": {
const ti = action.trackIndex; const ti = action.trackIndex;

View File

@ -1,9 +1,14 @@
// js/state.js // state.js (versão conceitual nova)
import { audioState, initializeAudioState } from "./audio/audio_state.js"; import {
audioState,
initializeAudioState,
getAudioSnapshot,
applyAudioSnapshot,
} from "./audio/audio_state.js";
import { DEFAULT_VOLUME, DEFAULT_PAN } from "./config.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 = { const patternState = {
tracks: [], tracks: [],
@ -38,14 +43,82 @@ const globalState = {
export let appState = { export let appState = {
global: globalState, global: globalState,
pattern: patternState, pattern: patternState,
audio: audioState, audio: audioState, // compartilhado com módulo de áudio
}; };
window.appState = appState; 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() { export function resetProjectState() {
console.log("Executando resetProjectState (Limpeza Profunda)..."); console.log("Executando resetProjectState (Limpeza Profunda)...");
// 1. Reseta Global
Object.assign(appState.global, { Object.assign(appState.global, {
sliceToolActive: false, sliceToolActive: false,
isPlaying: false, isPlaying: false,
@ -67,180 +140,110 @@ export function resetProjectState() {
clipboard: null, clipboard: null,
lastRulerClickTime: 0, lastRulerClickTime: 0,
justReset: false, justReset: false,
// syncMode mantemos o que estava syncMode: appState.global.syncMode ?? "global",
}); });
// 2. Reseta Pattern
Object.assign(appState.pattern, { Object.assign(appState.pattern, {
tracks: [], tracks: [],
activeTrackId: null, activeTrackId: null,
activePatternIndex: 0, 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 SESSÃO LOCAL ----------------
* 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.
*/
export function saveStateToSession() { export function saveStateToSession() {
if (!window.ROOM_NAME) return; 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 { try {
const roomName = window.ROOM_NAME || "default_room"; // Pattern “puro”
// Agora o JSON.stringify funciona porque só tem dados simples const patternSnapshot = makePatternSnapshot();
sessionStorage.setItem(`temp_state_${roomName}`, JSON.stringify(stateToSave));
// 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) { } catch (e) {
console.error("Erro ao salvar sessão:", e); console.error("Erro salvando sessão:", e);
} }
} }
/** // ---------------- CARREGAR SESSÃO LOCAL ----------------
* CARREGAR (Hydration): // mode:
* o JSON leve e RECONSTRÓI os objetos pesados (carrega os arquivos via HTTP). // - "full" (global + pattern + audio)
*/ // - "audioOnly" (apenas áudio, usado como fallback
export async function loadStateFromSession() { // depois de carregar XML do servidor)
const roomName = window.ROOM_NAME || "default_room";
const tempStateJSON = sessionStorage.getItem(`temp_state_${roomName}`);
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 { try {
const tempState = JSON.parse(tempStateJSON); const data = JSON.parse(raw);
// 1. Restaura Pattern Tracks // GLOBAL & PATTERN só se for "full"
// Precisamos recriar os Nodes do Tone.js que não foram salvos if (mode === "full") {
appState.pattern.tracks.forEach((liveTrack) => { if (data.global) {
const savedTrack = tempState.pattern.tracks.find(t => t.id === liveTrack.id); const { originalXmlDoc: xmlString, ...rest } = data.global;
if (savedTrack) { Object.assign(appState.global, rest);
// 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 (xmlString) {
if (liveTrack.volumeNode) liveTrack.volumeNode.volume.value = Tone.gainToDb(savedTrack.volume); try {
if (liveTrack.pannerNode) liveTrack.pannerNode.pan.value = savedTrack.pan; const parser = new DOMParser();
} appState.global.originalXmlDoc = parser.parseFromString(
}); xmlString,
"application/xml"
// Sincroniza lista de tracks (remove deletadas) );
appState.pattern.tracks = appState.pattern.tracks.filter(liveTrack => } catch (e) {
tempState.pattern.tracks.some(t => t.id === liveTrack.id) console.warn("Falha ao reconstituir originalXmlDoc:", e);
); }
// 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 (data.pattern) {
if (tempState.global) { Object.assign(appState.pattern, data.pattern);
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";
}
} }
} }
appState.pattern.activeTrackId = tempState.pattern.activeTrackId; // ÁUDIO em qualquer modo, se existir
if (hasAudioInSnapshot(data.audioSnapshot)) {
initializeAudioState();
await applyAudioSnapshot(data.audioSnapshot);
}
return true; return true;
} catch (e) { } catch (e) {
console.error("Erro crítico ao carregar sessão:", e); console.error("Erro carregando sessão:", e);
return false; return false;
} }
} }