// js/state.js import { audioState, initializeAudioState } 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 --- const patternState = { tracks: [], activeTrackId: null, activePatternIndex: 0, }; const globalState = { sliceToolActive: false, isPlaying: false, isAudioEditorPlaying: false, playbackIntervalId: null, currentStep: 0, metronomeEnabled: false, originalXmlDoc: null, currentBeatBasslineName: "Novo Projeto", masterVolume: DEFAULT_VOLUME, masterPan: DEFAULT_PAN, zoomLevelIndex: 2, isLoopActive: false, loopStartTime: 0, loopEndTime: 8, resizeMode: "trim", selectedClipId: null, isRecording: false, clipboard: null, lastRulerClickTime: 0, justReset: false, syncMode: "global", }; export let appState = { global: globalState, pattern: patternState, audio: audioState, }; window.appState = appState; export function resetProjectState() { console.log("Executando resetProjectState (Limpeza Profunda)..."); // 1. Reseta Global Object.assign(appState.global, { sliceToolActive: false, isPlaying: false, isAudioEditorPlaying: false, playbackIntervalId: null, currentStep: 0, metronomeEnabled: false, originalXmlDoc: null, currentBeatBasslineName: "Novo Projeto", masterVolume: DEFAULT_VOLUME, masterPan: DEFAULT_PAN, zoomLevelIndex: 2, isLoopActive: false, loopStartTime: 0, loopEndTime: 8, resizeMode: "trim", selectedClipId: null, isRecording: false, clipboard: null, lastRulerClickTime: 0, justReset: false, // syncMode mantemos o que estava }); // 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. */ 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)); } catch (e) { console.error("Erro ao salvar 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}`); if (!tempStateJSON) return false; console.log("Hidratando estado da sessão..."); try { const tempState = JSON.parse(tempStateJSON); // 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) ); // 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"; } } } appState.pattern.activeTrackId = tempState.pattern.activeTrackId; return true; } catch (e) { console.error("Erro crítico ao carregar sessão:", e); return false; } }