246 lines
7.6 KiB
JavaScript
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;
|
|
}
|
|
} |