mmpSearch/assets/js/creations/state.js

246 lines
7.6 KiB
JavaScript

// 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;
}
}