corrigindo estado atual da sala, aparentemente, todos os clientes possuem o mesmo estado de sala com steps e samples de áudio

This commit is contained in:
JotaChina committed 2025-11-26 19:36:54 -03:00
1 parent 6eb81089f7
commit 3f714c88de
3 files changed
+265 -238

No files matched your search

+153 -150
View File
@@ -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";
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();
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.
}));
// 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));
// Pattern “puro”
const patternSnapshot = makePatternSnapshot();
// 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) {
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
});
// 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)
);
// GLOBAL & PATTERN só se for "full"
if (mode === "full") {
if (data.global) {
const { originalXmlDoc: xmlString, ...rest } = data.global;
Object.assign(appState.global, rest);
// 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);
}
if (xmlString) {
try {
const parser = new DOMParser();
appState.global.originalXmlDoc = parser.parseFromString(
xmlString,
"application/xml"
);
} catch (e) {
console.warn("Falha ao reconstituir originalXmlDoc:", e);
}
}
// 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";
}
if (data.pattern) {
Object.assign(appState.pattern, data.pattern);
}
}
appState.pattern.activeTrackId = tempState.pattern.activeTrackId;
// Á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;
}
}
}