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:
1 parent
6eb81089f7
commit
3f714c88de
3 files changed
+265
-238
No files matched your search
+10
-29
@@ -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) {
|
||||||
|
|||||||
+102
-59
@@ -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;
|
||||||
|
|||||||
+153
-150
@@ -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)
|
initializeAudioState();
|
||||||
if (appState.audio) {
|
|
||||||
appState.audio.tracks = [];
|
|
||||||
appState.audio.clips = [];
|
|
||||||
appState.audio.audioEditorSeekTime = 0;
|
|
||||||
}
|
|
||||||
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:
|
||||||
* Lê 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 (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 (xmlString) {
|
||||||
if (tempState.audio) {
|
try {
|
||||||
appState.audio.tracks = tempState.audio.tracks || [];
|
const parser = new DOMParser();
|
||||||
|
appState.global.originalXmlDoc = parser.parseFromString(
|
||||||
const clipsMetadata = tempState.audio.clips || [];
|
xmlString,
|
||||||
const loadedClips = [];
|
"application/xml"
|
||||||
|
);
|
||||||
console.log(`Recarregando ${clipsMetadata.length} clips de áudio...`);
|
} catch (e) {
|
||||||
|
console.warn("Falha ao reconstituir originalXmlDoc:", e);
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user