mmpSearch/assets/js/creations/main.js

570 lines
20 KiB
JavaScript

// js/main.js (ESM com import absoluto de socket.js + ROOM_NAME local)
import { appState } from "./state.js";
import {
updateTransportLoop,
restartAudioEditorIfPlaying,
} from "./audio/audio_audio.js";
import { initializeAudioContext } from "./audio.js";
import { handleFileLoad, generateMmpFile, BLANK_PROJECT_XML } from "./file.js";
import {
renderAll,
loadAndRenderSampleBrowser,
showOpenProjectModal,
closeOpenProjectModal,
} from "./ui.js";
import { renderAudioEditor } from "./audio/audio_ui.js";
import { adjustValue, enforceNumericInput } from "./utils.js";
import { ZOOM_LEVELS } from "./config.js";
import { loadProjectFromServer } from "./file.js";
// ⚠️ IMPORT ABSOLUTO para evitar 404/text/html quando a página estiver em /creation/ ou fora dela.
// Ajuste o prefixo abaixo para o caminho real onde seus assets vivem no servidor:
import { sendAction, joinRoom, setUserName } from "./socket.js";
import { renderActivePatternToBlob } from "./pattern/pattern_audio.js"; // <-- ADICIONE ESTA LINHA
import { showToast } from "./ui.js";
import { toggleRecording } from "./recording.js"
// Descobre a sala pela URL (local ao main.js) e expõe no window para debug
const ROOM_NAME = new URLSearchParams(window.location.search).get("room");
window.ROOM_NAME = ROOM_NAME;
const PROJECT_NAME = new URLSearchParams(window.location.search).get("project");
if (PROJECT_NAME) {
// O nome do projeto deve corresponder ao arquivo no servidor, por ex: "mmp/nome-do-seu-projeto-salvo.mmp"
// O arquivo 'file.js' já espera que loadProjectFromServer receba apenas o nome
// do arquivo dentro da pasta 'mmp/' (ex: 'nome-do-projeto.mmp').
console.log(`[MAIN] Carregando projeto do servidor: ${PROJECT_NAME}`);
// Adicione a extensão se ela não estiver no link
const filename =
PROJECT_NAME.endsWith(".mmp") || PROJECT_NAME.endsWith(".mmpz")
? PROJECT_NAME
: `${PROJECT_NAME}.mmp`;
// Chama a função de file.js para carregar (que já envia a ação 'LOAD_PROJECT')
loadProjectFromServer(filename);
}
// ✅ NOVO: se tem sala na URL, entra já na sala (independe do áudio)
if (ROOM_NAME) {
// entra na sala para receber estado/broadcasts imediatamente
joinRoom();
}
// Função util para alternar estado dos botões de ferramenta
function updateToolButtons() {
const sliceToolBtn = document.getElementById("slice-tool-btn");
const trimToolBtn = document.getElementById("resize-tool-trim");
const stretchToolBtn = document.getElementById("resize-tool-stretch");
if (sliceToolBtn)
sliceToolBtn.classList.toggle("active", appState.global.sliceToolActive);
if (trimToolBtn)
trimToolBtn.classList.toggle(
"active",
!appState.global.sliceToolActive && appState.global.resizeMode === "trim"
);
if (stretchToolBtn)
stretchToolBtn.classList.toggle(
"active",
!appState.global.sliceToolActive &&
appState.global.resizeMode === "stretch"
);
document.body.classList.toggle(
"slice-tool-active",
appState.global.sliceToolActive
);
}
document.addEventListener("DOMContentLoaded", () => {
// Botões e elementos
const newProjectBtn = document.getElementById("new-project-btn");
const openMmpBtn = document.getElementById("open-mmp-btn");
const saveMmpBtn = document.getElementById("save-mmp-btn");
const uploadSampleBtn = document.getElementById("upload-sample-btn");
const addInstrumentBtn = document.getElementById("add-instrument-btn");
const removeInstrumentBtn = document.getElementById("remove-instrument-btn");
const playBtn = document.getElementById("play-btn");
const stopBtn = document.getElementById("stop-btn");
const audioEditorPlayBtn = document.getElementById("audio-editor-play-btn");
const audioEditorStopBtn = document.getElementById("audio-editor-stop-btn");
const audioEditorLoopBtn = document.getElementById("audio-editor-loop-btn");
const addAudioTrackBtn = document.getElementById("add-audio-track-btn");
const rewindBtn = document.getElementById("rewind-btn");
const metronomeBtn = document.getElementById("metronome-btn");
const sliceToolBtn = document.getElementById("slice-tool-btn");
const resizeToolTrimBtn = document.getElementById("resize-tool-trim");
const resizeToolStretchBtn = document.getElementById("resize-tool-stretch");
const createRoomBtn = document.getElementById("create-room-btn");
const mmpFileInput = document.getElementById("mmp-file-input");
const sampleFileInput = document.getElementById("sample-file-input");
const openProjectModal = document.getElementById("open-project-modal");
const openModalCloseBtn = document.getElementById("open-modal-close-btn");
const loadFromComputerBtn = document.getElementById("load-from-computer-btn");
const sidebarToggle = document.getElementById("sidebar-toggle");
const addBarBtn = document.getElementById("add-bar-btn");
const zoomInBtn = document.getElementById("zoom-in-btn");
const zoomOutBtn = document.getElementById("zoom-out-btn");
const deleteClipBtn = document.getElementById("delete-clip");
// Configuração do botão de Gravação
const recordBtn = document.getElementById('record-btn');
if (recordBtn) {
recordBtn.addEventListener('click', async () => {
// PASSO CRÍTICO: O navegador exige isso antes de gravar
if (Tone.context.state !== 'running') {
await Tone.start();
console.log("Audio Context iniciado via clique.");
}
// Chama a função que você criou no recording.js
toggleRecording();
});
} else {
console.error("Botão de gravação (#record-btn) não encontrado no DOM.");
}
//envia pattern pro editor de áudio
const bouncePatternBtn = document.getElementById(
"send-pattern-to-playlist-btn"
);
bouncePatternBtn?.addEventListener("click", async () => {
// 1. Verifica se existe uma pista de áudio para onde enviar
const targetTrackId = appState.audio.tracks[0]?.id;
if (!targetTrackId) {
showToast(
"Crie uma Pista de Áudio (no editor de amostras) primeiro!",
"error"
);
return;
}
showToast("Renderizando pattern...", "info");
try {
// 2. Chama a função de renderização que criamos
const audioBlob = await renderActivePatternToBlob();
// 3. Cria uma URL local para o áudio renderizado
const audioUrl = URL.createObjectURL(audioBlob);
// --- INÍCIO DA NOVA MODIFICAÇÃO (Visualização de Steps) ---
// 4. Pega o índice do pattern que foi renderizado
const activePatternIndex =
appState.pattern.tracks[0]?.activePatternIndex || 0;
// 5. Coleta os dados de steps de CADA trilha para esse pattern
const patternData = appState.pattern.tracks.map((track) => {
const pattern = track.patterns[activePatternIndex];
// Retorna o array de steps, ou um array vazio se não houver
return pattern && pattern.steps ? pattern.steps : [];
});
// --- FIM DA NOVA MODIFICAÇÃO ---
// 6. Prepara o nome e ID
const patternName =
appState.pattern.tracks[0]?.patterns[activePatternIndex]?.name ||
"Pattern";
const clipName = `${patternName} (Bounced).wav`;
const clipId = `bounced_${Date.now()}`;
// 7. Envia a ação (a lógica ADD_AUDIO_CLIP já sabe como carregar o áudio)
sendAction({
type: "ADD_AUDIO_CLIP",
filePath: audioUrl, // O player de áudio sabe ler essa URL 'blob:'
trackId: targetTrackId, // Envia para a primeira pista de áudio
startTimeInSeconds: 0, // Coloca no início da timeline
clipId: clipId,
name: clipName,
patternData: patternData, // <-- AQUI ESTÁ A "PARTITURA"
});
showToast("Pattern enviada para a Pista de Áudio!", "success");
} catch (error) {
console.error("Erro ao renderizar pattern:", error);
showToast("Erro ao renderizar pattern", "error");
}
});
//Seleção de pattern
const globalPatternSelector = document.getElementById(
"global-pattern-selector"
);
// Adiciona o novo "ouvinte" de evento para o seletor de pattern
globalPatternSelector?.addEventListener("change", () => {
// Pega o novo índice (ex: 0, 1, 2...) do seletor
const newPatternIndex = parseInt(globalPatternSelector.value, 10);
// --- CORREÇÃO DE LÓGICA (não precisamos mais do activeTrackId) ---
// A ação agora é global e afeta TODAS as tracks.
if (isNaN(newPatternIndex)) {
console.warn("Não é possível trocar pattern: índice inválido.");
return;
}
// Envia a ação para todos (incluindo você)
sendAction({
type: "SET_ACTIVE_PATTERN",
patternIndex: newPatternIndex,
});
});
// =================================================================
// 👇 INÍCIO DA CORREÇÃO (Botão de Sincronia - Agora envia Ação)
// =================================================================
const syncModeBtn = document.getElementById("sync-mode-btn"); //
if (syncModeBtn) {
//
// Define o estado inicial (global por padrão)
appState.global.syncMode = "global"; //
syncModeBtn.classList.add("active"); //
syncModeBtn.textContent = "Global"; //
syncModeBtn.addEventListener("click", () => {
//
// 1. Determina qual será o *novo* modo
const newMode =
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({
type: "SET_SYNC_MODE",
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
if (deleteClipBtn) {
deleteClipBtn.addEventListener("click", () => {
initializeAudioContext();
const clipId = appState.global.selectedClipId;
if (clipId) {
sendAction({ type: "REMOVE_AUDIO_CLIP", clipId });
appState.global.selectedClipId = null;
}
const menu = document.getElementById("timeline-context-menu");
if (menu) menu.style.display = "none";
});
}
// Delete/Backspace
document.addEventListener("keydown", (e) => {
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
const clipId = appState.global.selectedClipId;
if ((e.key === "Delete" || e.key === "Backspace") && clipId) {
e.preventDefault();
sendAction({ type: "REMOVE_AUDIO_CLIP", clipId });
appState.global.selectedClipId = null;
}
});
// Fechar menu contexto
document.addEventListener("click", (e) => {
const menu = document.getElementById("timeline-context-menu");
if (menu && !e.target.closest("#timeline-context-menu")) {
menu.style.display = "none";
}
});
// Ações principais (broadcast)
newProjectBtn?.addEventListener("click", () => {
initializeAudioContext();
if (
(appState.pattern.tracks.length > 0 || appState.audio.clips.length > 0) &&
!confirm(
"Você tem certeza? Isso irá resetar o projeto para TODOS na sala."
)
)
return;
sendAction({ type: "LOAD_PROJECT", xml: BLANK_PROJECT_XML });
});
addBarBtn?.addEventListener("click", () => {
const barsInput = document.getElementById("bars-input");
if (barsInput) {
adjustValue(barsInput, 1);
barsInput.dispatchEvent(new Event("change", { bubbles: true }));
}
});
openMmpBtn?.addEventListener("click", showOpenProjectModal);
loadFromComputerBtn?.addEventListener("click", () => mmpFileInput?.click());
mmpFileInput?.addEventListener("change", (event) => {
const file = event.target.files[0];
if (file) handleFileLoad(file).then(() => closeOpenProjectModal());
});
uploadSampleBtn?.addEventListener("click", () => sampleFileInput?.click());
saveMmpBtn?.addEventListener("click", generateMmpFile);
addInstrumentBtn?.addEventListener("click", () => {
initializeAudioContext();
sendAction({ type: "ADD_TRACK" });
});
removeInstrumentBtn?.addEventListener("click", () => {
initializeAudioContext();
sendAction({ type: "REMOVE_LAST_TRACK" });
});
playBtn?.addEventListener("click", () => {
initializeAudioContext();
sendAction({ type: "TOGGLE_PLAYBACK" });
});
stopBtn?.addEventListener("click", () => {
initializeAudioContext();
sendAction({ type: "STOP_PLAYBACK" });
});
rewindBtn?.addEventListener("click", () => {
initializeAudioContext();
sendAction({ type: "REWIND_PLAYBACK" });
});
metronomeBtn?.addEventListener("click", () => {
initializeAudioContext();
appState.global.metronomeEnabled = !appState.global.metronomeEnabled;
metronomeBtn.classList.toggle("active", appState.global.metronomeEnabled);
});
// Ferramentas locais
if (sliceToolBtn) {
sliceToolBtn.addEventListener("click", () => {
appState.global.sliceToolActive = !appState.global.sliceToolActive;
updateToolButtons();
});
}
if (resizeToolTrimBtn) {
resizeToolTrimBtn.addEventListener("click", () => {
appState.global.resizeMode = "trim";
appState.global.sliceToolActive = false;
updateToolButtons();
});
}
if (resizeToolStretchBtn) {
resizeToolStretchBtn.addEventListener("click", () => {
appState.global.resizeMode = "stretch";
appState.global.sliceToolActive = false;
updateToolButtons();
});
}
openModalCloseBtn?.addEventListener("click", closeOpenProjectModal);
sidebarToggle?.addEventListener("click", () => {
document.body.classList.toggle("sidebar-hidden");
const icon = sidebarToggle.querySelector("i");
if (icon) {
icon.className = document.body.classList.contains("sidebar-hidden")
? "fa-solid fa-caret-right"
: "fa-solid fa-caret-left";
}
});
const inputs = document.querySelectorAll(".value-input");
inputs.forEach((input) => {
input.addEventListener("input", (event) => {
enforceNumericInput(event);
if (
appState.global.isPlaying &&
(event.target.id.startsWith("compasso-") ||
event.target.id === "bars-input")
) {
sendAction({ type: "STOP_PLAYBACK" });
}
});
input.addEventListener("change", (event) => {
const target = event.target;
if (target.id === "bpm-input") {
sendAction({ type: "SET_BPM", value: target.value });
} else if (target.id === "bars-input") {
sendAction({ type: "SET_BARS", value: target.value });
} else if (target.id === "compasso-a-input") {
sendAction({ type: "SET_TIMESIG_A", value: target.value });
} else if (target.id === "compasso-b-input") {
sendAction({ type: "SET_TIMESIG_B", value: target.value });
}
});
input.addEventListener("wheel", (event) => {
event.preventDefault();
const step = event.deltaY < 0 ? 1 : -1;
adjustValue(event.target, step);
event.target.dispatchEvent(new Event("change", { bubbles: true }));
});
});
const buttons = document.querySelectorAll(".adjust-btn");
buttons.forEach((button) => {
button.addEventListener("click", () => {
const targetId = button.dataset.target + "-input";
const targetInput = document.getElementById(targetId);
const step = parseInt(button.dataset.step, 10) || 1;
if (targetInput) {
adjustValue(targetInput, step);
targetInput.dispatchEvent(new Event("change", { bubbles: true }));
}
});
});
// Zoom local
zoomInBtn?.addEventListener("click", () => {
if (appState.global.zoomLevelIndex < ZOOM_LEVELS.length - 1) {
appState.global.zoomLevelIndex++;
renderAll();
}
});
zoomOutBtn?.addEventListener("click", () => {
if (appState.global.zoomLevelIndex > 0) {
appState.global.zoomLevelIndex--;
renderAll();
}
});
// Editor de Áudio
audioEditorPlayBtn?.addEventListener("click", () => {
initializeAudioContext();
if (appState.global.isAudioEditorPlaying) {
sendAction({ type: "STOP_AUDIO_PLAYBACK", rewind: false });
} else {
sendAction({
type: "START_AUDIO_PLAYBACK",
seekTime: appState.audio.audioEditorSeekTime, // Corrigido
loopState: {
isLoopActive: appState.global.isLoopActive,
loopStartTime: appState.global.loopStartTime,
loopEndTime: appState.global.loopEndTime,
},
});
}
});
audioEditorStopBtn?.addEventListener("click", () => {
initializeAudioContext();
sendAction({ type: "STOP_AUDIO_PLAYBACK", rewind: true });
});
// Loop Button (agora envia ação)
audioEditorLoopBtn?.addEventListener("click", () => {
initializeAudioContext(); // Garante contexto
const newLoopState = !appState.global.isLoopActive;
sendAction({
type: "SET_LOOP_STATE",
isLoopActive: newLoopState,
loopStartTime: appState.global.loopStartTime,
loopEndTime: appState.global.loopEndTime,
});
});
if (addAudioTrackBtn) {
addAudioTrackBtn.addEventListener("click", () => {
initializeAudioContext();
sendAction({ type: "ADD_AUDIO_LANE" });
});
}
// Navegador de Samples (local)
loadAndRenderSampleBrowser();
const browserContent = document.getElementById("browser-content");
if (browserContent) {
browserContent.addEventListener("click", function (event) {
const folderName = event.target.closest(".folder-name");
if (folderName) {
const folderItem = folderName.parentElement;
folderItem.classList.toggle("open");
}
});
}
// Criar sala (gera link com ?room=...)
if (createRoomBtn) {
createRoomBtn.addEventListener("click", () => {
initializeAudioContext();
const currentParams = new URLSearchParams(window.location.search);
if (currentParams.has("room")) {
alert(
`Você já está na sala: ${currentParams.get(
"room"
)}\n\Copie o link da barra de endereços para convidar.`
);
return;
}
const defaultName = `sessao-${Math.random()
.toString(36)
.substring(2, 7)}`;
const roomName = prompt(
"Digite um nome para a sala compartilhada:",
defaultName
);
if (!roomName) return;
const currentUrl = window.location.origin + window.location.pathname;
const shareableLink = `${currentUrl}?room=${encodeURIComponent(
roomName
)}`;
try {
navigator.clipboard.writeText(shareableLink);
alert(
`Link da sala copiado para a área de transferência!\n\n${shareableLink}\n\nA página será recarregada agora para entrar na nova sala.`
);
} catch (err) {
alert(
`Link da sala: ${shareableLink}\n\nA página será recarregada agora para entrar na nova sala.`
);
}
window.location.href = shareableLink;
});
}
// Modal “destravar áudio” + entrar na sala
const audioUnlockModal = document.getElementById("audio-unlock-modal");
const audioUnlockBtn = document.getElementById("audio-unlock-btn");
if (ROOM_NAME && audioUnlockModal && audioUnlockBtn) {
audioUnlockModal.style.display = "flex";
audioUnlockBtn.addEventListener("click", () => {
const userName = prompt(
"Qual o seu nome?",
`Alicer-${Math.floor(Math.random() * 999)}`
);
if (!userName) return;
setUserName(userName);
initializeAudioContext();
// joinRoom() já foi chamado no início se ROOM_NAME existe
audioUnlockModal.style.display = "none";
});
} else {
console.log("Modo local. Áudio será iniciado no primeiro clique.");
// Comentado para permitir teste visual
// if (syncModeBtn) syncModeBtn.style.display = "none";
}
renderAll();
updateToolButtons();
});