diff --git a/assets/css/style.css b/assets/css/style.css index f261937..86ff83e 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -141,7 +141,6 @@ body.sidebar-hidden #sidebar-toggle { padding: 8px 15px; position: fixed; top: 0; - /* Removido width 100% para se adaptar ao padding do body */ left: 300px; right: 0; z-index: 1000; @@ -269,6 +268,7 @@ body.sidebar-hidden .global-toolbar { margin: 0 10px; padding-left: 10px; border-left: 1px solid var(--bg-toolbar); + flex-shrink: 0; } .knob-container { @@ -305,32 +305,34 @@ body.sidebar-hidden .global-toolbar { border-radius: 1px; } -.step-sequencer { - display: flex; +.step-sequencer-wrapper { flex-grow: 1; - gap: 4px; overflow-x: auto; + overflow-y: hidden; padding-bottom: 8px; } -.step-sequencer::-webkit-scrollbar { - height: 8px; +.step-sequencer { + display: flex; + gap: 4px; } -.step-sequencer::-webkit-scrollbar-track { +.step-sequencer-wrapper::-webkit-scrollbar { + height: 8px; +} +.step-sequencer-wrapper::-webkit-scrollbar-track { background: var(--border-color); border-radius: 4px; } - -.step-sequencer::-webkit-scrollbar-thumb { +.step-sequencer-wrapper::-webkit-scrollbar-thumb { background: var(--bg-toolbar); border-radius: 4px; } - -.step-sequencer::-webkit-scrollbar-thumb:hover { +.step-sequencer-wrapper::-webkit-scrollbar-thumb:hover { background: #555; } + .step-wrapper { position: relative; } @@ -341,17 +343,18 @@ body.sidebar-hidden .global-toolbar { left: 1px; font-size: .6rem; color: var(--text-dark); + user-select: none; } .step { width: 28px; - aspect-ratio: 1 / 1; + height: 28px; background-color: #2a2a2a; border: 1px solid #4a4a4a; border-radius: 2px; cursor: pointer; transition: background-color .1s, transform 0.1s; - flex-shrink: 0; /* Impede que os steps encolham */ + flex-shrink: 0; } .step-dark { @@ -374,6 +377,7 @@ body.sidebar-hidden .global-toolbar { box-shadow: inset 0 0 8px rgba(255, 255, 255, 0.8); } + /* =============================================== */ /* CONTROLES E INPUTS /* =============================================== */ @@ -518,7 +522,7 @@ body.sidebar-hidden .global-toolbar { } /* =============================================== */ -/* MODAL (CAIXA DE DIÁLOGO) +/* MODAL (CAIXA DE DIÁLOGO) - (REVISADO) /* =============================================== */ .modal-overlay { position: fixed; @@ -531,6 +535,7 @@ body.sidebar-hidden .global-toolbar { display: flex; justify-content: center; align-items: center; + padding: 1rem; /* Adiciona um respiro nas laterais */ visibility: hidden; opacity: 0; transition: visibility 0s 0.3s, opacity 0.3s; @@ -551,6 +556,14 @@ body.sidebar-hidden .global-toolbar { width: 100%; max-width: 500px; position: relative; + + /* (NOVO) Usando Flexbox para organizar o conteúdo interno */ + display: flex; + flex-direction: column; + gap: 1.5rem; /* Espaçamento consistente entre as seções */ + + /* (NOVO) Controle de altura para telas pequenas ou listas grandes */ + max-height: 90vh; } .modal-close { @@ -569,21 +582,53 @@ body.sidebar-hidden .global-toolbar { } .modal-title { - margin-top: 0; - margin-bottom: 1.5rem; + margin: 0; /* Removido margin para usar o 'gap' do flexbox */ + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--bg-toolbar); color: var(--text-light); text-align: center; + flex-shrink: 0; /* Impede que o título encolha */ } .modal-section { - margin-bottom: 1.5rem; + margin: 0; /* Removido margin para usar o 'gap' do flexbox */ } .modal-section h3 { margin-top: 0; margin-bottom: 0.8rem; - border-bottom: 1px solid var(--bg-toolbar); - padding-bottom: 0.5rem; + font-size: 1rem; + color: var(--text-light); +} + +/* (NOVO) Estilos para a lista de projetos do servidor */ +#server-projects-list { + max-height: 250px; /* Altura máxima para a lista */ + overflow-y: auto; /* Barra de rolagem SÓ para a lista */ + background-color: var(--bg-toolbar); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 0.5rem; + min-height: 50px; /* Altura mínima para não colapsar se estiver vazio */ +} + +/* (REVISADO) Estilos para cada item na lista */ +#server-projects-list .project-item { + background-color: var(--bg-editor); + padding: 10px 15px; + border-radius: 4px; + margin-bottom: 8px; + cursor: pointer; + transition: background-color 0.2s, color 0.2s; + border: 1px solid transparent; +} +#server-projects-list .project-item:last-child { + margin-bottom: 0; +} +#server-projects-list .project-item:hover { + background-color: var(--bg-body); + color: #fff; + border-color: var(--accent-green); } .modal-button { @@ -596,6 +641,7 @@ body.sidebar-hidden .global-toolbar { font-size: 1rem; transition: background-color 0.2s, border-color 0.2s; width: 100%; + text-align: center; } .modal-button:hover { @@ -603,101 +649,68 @@ body.sidebar-hidden .global-toolbar { border-color: #333; } -#server-projects-list .project-item { - background-color: var(--bg-editor); - padding: 10px 15px; - border-radius: 4px; - margin-bottom: 8px; - cursor: pointer; - transition: background-color 0.2s; -} - -#server-projects-list .project-item:hover { - background-color: var(--bg-body); - color: #fff; -} - /* =============================================== */ /* ESTILOS RESPONSIVOS /* =============================================== */ -/* Para telas menores como laptops pequenos e tablets grandes */ @media (max-width: 992px) { .main-content { - padding: 1.5rem; /* Reduz o padding */ + padding: 1.5rem; } - .beat-editor { - max-width: 100%; /* Permite que o editor use mais espaço */ + max-width: 100%; } } - -/* Para tablets e celulares */ @media (max-width: 768px) { body { - padding-left: 0 !important; /* Remove o padding fixo, !important para garantir */ + padding-left: 0 !important; } - .main-content { padding: 1rem; } - - /* A sidebar agora se sobrepõe ao conteúdo e fica escondida por padrão */ .sample-browser { transform: translateX(-100%); - width: 280px; /* Pode diminuir um pouco a largura */ + width: 280px; } - - /* Quando a sidebar for aberta, ela volta a posição original */ body:not(.sidebar-hidden) .sample-browser { transform: translateX(0); } - #sidebar-toggle { - left: 5px; /* Posição fixa do botão */ + left: 5px; } - - /* A toolbar global agora ocupa 100% da largura */ .global-toolbar { left: 0; - padding-left: 45px; /* Espaço para o botão do menu */ + padding-left: 45px; } - - /* Quebra a linha dos itens da toolbar se não couberem */ .editor-toolbar, .control-group { flex-wrap: wrap; gap: 10px; } - - /* Reorganiza a track para um layout vertical */ .track-lane { flex-direction: column; - align-items: stretch; /* Itens ocupam 100% da largura */ + align-items: stretch; gap: 15px; padding: 15px; } - .track-info, .track-controls { width: 100%; } - .track-controls { border-left: none; padding-left: 0; - justify-content: space-around; /* Distribui melhor os knobs */ + justify-content: space-around; } - - .step-sequencer { + .step-sequencer-wrapper { width: 100%; } - - /* Ajusta o modal para telas pequenas */ + /* (REVISADO) Ajuste do modal para telas pequenas */ .modal-content { - max-width: 90vw; - padding: 1.5rem 1rem; + max-width: 95vw; /* Usa quase toda a largura da tela */ + padding: 1rem 1.5rem; + gap: 1rem; } } \ No newline at end of file diff --git a/assets/js/creations/audio.js b/assets/js/creations/audio.js index 7b548a3..8791ea7 100644 --- a/assets/js/creations/audio.js +++ b/assets/js/creations/audio.js @@ -42,44 +42,41 @@ export function playMetronomeSound(isDownbeat) { oscillator.stop(audioContext.currentTime + 0.05); } -// (ALTERADO) Função reescrita para usar AudioBuffers pré-carregados +// Função otimizada para usar AudioBuffers export function playSample(filePath, trackId) { initializeAudioContext(); if (!filePath) return; const track = trackId ? appState.tracks.find((t) => t.id == trackId) : null; - // Se a função for chamada sem um ID de track (ex: clique no browser de samples), - // usa o método antigo como fallback para uma prévia rápida. + // Se for uma prévia (sem trilha), usa o método antigo e rápido if (!track) { const audio = new Audio(filePath); audio.play(); return; } - - // Se a track não tiver o buffer de áudio pronto, avisa no console e não toca. + + // Se a trilha não tiver o buffer carregado, não toca if (!track.audioBuffer) { - console.warn(`Buffer para a track ${track.name} ainda não carregado.`); + console.warn(`Buffer para a trilha ${track.name} ainda não carregado.`); return; } - // 1. Cria uma fonte de áudio leve (BufferSource) + // Cria uma fonte de áudio leve a partir do buffer const source = audioContext.createBufferSource(); - // 2. Conecta o buffer de áudio já decodificado source.buffer = track.audioBuffer; - // 3. Conecta na cadeia de áudio da track (respeitando volume e pan) + // Conecta na cadeia de áudio da trilha (respeitando volume e pan) if (track.gainNode) { source.connect(track.gainNode); } else { source.connect(mainGainNode); // Fallback } - // 4. Toca o som imediatamente + // Toca o som imediatamente source.start(0); } - function tick() { const totalSteps = getTotalSteps(); if (totalSteps === 0) { diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index d9d924d..c6f3ff3 100644 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -1,7 +1,7 @@ // js/file.js -import { appState } from "./state.js"; +import { appState, loadAudioForTrack } from "./state.js"; import { getTotalSteps } from "./utils.js"; -import { renderApp } from "./ui.js"; +import { renderApp, getSamplePathMap } from "./ui.js"; import { NOTE_LENGTH, TICKS_PER_BAR } from "./config.js"; import { initializeAudioContext, @@ -26,14 +26,14 @@ export async function handleFileLoad(file) { } else { xmlContent = await file.text(); } - parseMmpContent(xmlContent); + await parseMmpContent(xmlContent); } catch (error) { console.error("Erro ao carregar o projeto:", error); alert(`Erro ao carregar projeto: ${error.message}`); } } -export function parseMmpContent(xmlString) { +export async function parseMmpContent(xmlString) { initializeAudioContext(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, "application/xml"); @@ -43,16 +43,18 @@ export function parseMmpContent(xmlString) { const head = xmlDoc.querySelector("head"); if (head) { - document.getElementById("bpm-input").value = - head.getAttribute("bpm") || 140; - document.getElementById("compasso-a-input").value = - head.getAttribute("timesig_numerator") || 4; - document.getElementById("compasso-b-input").value = - head.getAttribute("timesig_denominator") || 4; + document.getElementById("bpm-input").value = head.getAttribute("bpm") || 140; + document.getElementById("bars-input").value = head.getAttribute("num_bars") || 1; + document.getElementById("compasso-a-input").value = head.getAttribute("timesig_numerator") || 4; + document.getElementById("compasso-b-input").value = head.getAttribute("timesig_denominator") || 4; } + const sampleTrackElements = xmlDoc.querySelectorAll( 'instrument[name="audiofileprocessor"]' ); + + const pathMap = getSamplePathMap(); + sampleTrackElements.forEach((instrumentNode) => { const afpNode = instrumentNode.querySelector("audiofileprocessor"); const instrumentTrackNode = instrumentNode.parentElement; @@ -61,27 +63,34 @@ export function parseMmpContent(xmlString) { const audioContext = getAudioContext(); const mainGainNode = getMainGainNode(); - const totalSteps = parseInt( - trackNode.querySelector("pattern")?.getAttribute("steps") || - getTotalSteps(), - 10 - ); + + const totalSteps = getTotalSteps(); const newSteps = new Array(totalSteps).fill(false); - const ticksPerStep = totalSteps > 0 ? TICKS_PER_BAR / totalSteps : 0; + + // ================================================================== + // (CORREÇÃO DEFINITIVA) + // 1. Simplificamos o cálculo: cada step de 1/16 vale 12 ticks. + const ticksPerStep = 12; - trackNode.querySelectorAll("pattern note").forEach((noteNode) => { + // 2. Buscamos TODAS as notas da trilha, não importa em qual pattern elas estejam. + trackNode.querySelectorAll("note").forEach((noteNode) => { + // ================================================================== const pos = parseInt(noteNode.getAttribute("pos"), 10); const stepIndex = Math.round(pos / ticksPerStep); if (stepIndex < totalSteps) { newSteps[stepIndex] = true; } }); + + const srcAttribute = afpNode.getAttribute("src"); + const filename = srcAttribute.split("/").pop(); + const finalSamplePath = pathMap[filename] || `src/samples/${srcAttribute}`; + const newTrack = { id: Date.now() + Math.random(), - name: - afpNode.getAttribute("src").split("/").pop() || - trackNode.getAttribute("name"), - samplePath: `samples/${afpNode.getAttribute("src")}`, + name: filename || trackNode.getAttribute("name"), + samplePath: finalSamplePath, + audioBuffer: null, steps: newSteps, volume: parseFloat(instrumentTrackNode.getAttribute("vol")) / 100, pan: parseFloat(instrumentTrackNode.getAttribute("pan")) / 100, @@ -94,6 +103,15 @@ export function parseMmpContent(xmlString) { newTrack.pannerNode.pan.value = newTrack.pan; newTracks.push(newTrack); }); + + try { + const trackLoadPromises = newTracks.map(track => loadAudioForTrack(track)); + await Promise.all(trackLoadPromises); + console.log("Todos os áudios do projeto foram carregados."); + } catch (error) { + console.error("Ocorreu um erro ao carregar os áudios do projeto:", error); + } + appState.tracks = newTracks; renderApp(); console.log("Projeto carregado com sucesso!", appState); @@ -112,6 +130,7 @@ function generateNewMmp() { const bpm = document.getElementById("bpm-input").value; const sig_num = document.getElementById("compasso-a-input").value; const sig_den = document.getElementById("compasso-b-input").value; + const num_bars = document.getElementById("bars-input").value; const tracksXml = appState.tracks .map((track) => createTrackXml(track)) .join(""); @@ -119,7 +138,7 @@ function generateNewMmp() { const mmpContent = ` - + @@ -150,6 +169,7 @@ function modifyAndSaveExistingMmp() { const head = xmlDoc.querySelector("head"); if (head) { head.setAttribute("bpm", document.getElementById("bpm-input").value); + head.setAttribute("num_bars", document.getElementById("bars-input").value); head.setAttribute( "timesig_numerator", document.getElementById("compasso-a-input").value @@ -185,10 +205,10 @@ function modifyAndSaveExistingMmp() { function createTrackXml(track) { if (!track.samplePath) return ""; const totalSteps = track.steps.length || getTotalSteps(); - const ticksPerStep = totalSteps > 0 ? TICKS_PER_BAR / totalSteps : 0; + const ticksPerStep = 12; // Valor fixo de 12 ticks por step de 1/16 const lmmsVolume = Math.round(track.volume * 100); const lmmsPan = Math.round(track.pan * 100); - const sampleSrc = track.samplePath.replace("samples/", ""); + const sampleSrc = track.samplePath.replace("src/samples/", ""); const notesXml = track.steps .map((isActive, index) => { if (isActive) { @@ -198,6 +218,26 @@ function createTrackXml(track) { return ""; }) .join("\n "); + + // Cria um pattern para cada compasso (16 steps) + let patternsXml = ''; + const stepsPerBar = 16; + const numBars = Math.ceil(totalSteps / stepsPerBar); + + for (let i = 0; i < numBars; i++) { + const patternNotes = track.steps.slice(i * stepsPerBar, (i + 1) * stepsPerBar).map((isActive, index) => { + if (isActive) { + const notePos = Math.round(index * ticksPerStep); + return ``; + } + return ""; + }).join("\n "); + + patternsXml += ` + ${patternNotes} + ` + } + return ` @@ -206,9 +246,7 @@ function createTrackXml(track) { - - ${notesXml} - + ${patternsXml} `; } @@ -224,20 +262,17 @@ function downloadFile(content, fileName) { URL.revokeObjectURL(url); } -// (ALTERADO) Adiciona export na frente da função export async function loadProjectFromServer(fileName) { try { const response = await fetch(`mmp/${fileName}`); if (!response.ok) throw new Error(`Não foi possível carregar o arquivo ${fileName}`); const xmlContent = await response.text(); - parseMmpContent(xmlContent); - // Retorna true em caso de sucesso + await parseMmpContent(xmlContent); return true; } catch (error) { console.error("Erro ao carregar projeto do servidor:", error); alert(`Erro ao carregar projeto: ${error.message}`); - // Retorna false em caso de falha return false; } -} +} \ No newline at end of file diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index 65634ab..1089ad3 100644 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -36,6 +36,7 @@ document.addEventListener("DOMContentLoaded", () => { 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"); newProjectBtn.addEventListener("click", () => { if ( @@ -51,18 +52,32 @@ document.addEventListener("DOMContentLoaded", () => { metronomeEnabled: false, originalXmlDoc: null, }); + // Reseta os inputs para o padrão + document.getElementById('bpm-input').value = 140; + document.getElementById('bars-input').value = 1; + document.getElementById('compasso-a-input').value = 4; + document.getElementById('compasso-b-input').value = 4; renderApp(); }); + addBarBtn.addEventListener("click", () => { + const barsInput = document.getElementById("bars-input"); + if (barsInput) { + adjustValue(barsInput, 1); + } + }); + openMmpBtn.addEventListener("click", showOpenProjectModal); loadFromComputerBtn.addEventListener("click", () => mmpFileInput.click()); - mmpFileInput.addEventListener("change", (event) => { + + mmpFileInput.addEventListener("change", async (event) => { const file = event.target.files[0]; if (file) { - handleFileLoad(file); + await handleFileLoad(file); closeOpenProjectModal(); } }); + saveMmpBtn.addEventListener("click", generateMmpFile); addInstrumentBtn.addEventListener("click", addTrackToState); removeInstrumentBtn.addEventListener("click", removeLastTrackFromState); @@ -90,10 +105,10 @@ document.addEventListener("DOMContentLoaded", () => { inputs.forEach((input) => { input.addEventListener("input", (event) => { enforceNumericInput(event); - if (appState.isPlaying && event.target.id.startsWith("compasso-")) { + if (appState.isPlaying && (event.target.id.startsWith("compasso-") || event.target.id === 'bars-input')) { stopPlayback(); } - if (event.target.id.startsWith("compasso-")) { + if (event.target.id.startsWith("compasso-") || event.target.id === 'bars-input') { redrawSequencer(); } }); @@ -109,6 +124,7 @@ document.addEventListener("DOMContentLoaded", () => { 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); } @@ -118,4 +134,4 @@ document.addEventListener("DOMContentLoaded", () => { // Inicia a aplicação loadAndRenderSampleBrowser(); renderApp(); -}); +}); \ No newline at end of file diff --git a/assets/js/creations/state.js b/assets/js/creations/state.js index 64d74dc..5f1ee81 100644 --- a/assets/js/creations/state.js +++ b/assets/js/creations/state.js @@ -17,6 +17,29 @@ export let appState = { originalXmlDoc: null, }; +// ESSA É A FUNÇÃO QUE ESTÁ FALTANDO NO SEU ARQUIVO ATUAL +// Função auxiliar para carregar o buffer de áudio para uma track específica +export async function loadAudioForTrack(track) { + if (!track.samplePath) { + console.warn("Track sem samplePath, pulando o carregamento de áudio."); + return track; + } + try { + const audioContext = getAudioContext(); + if (!audioContext) initializeAudioContext(); + + const response = await fetch(track.samplePath); + if (!response.ok) throw new Error(`Erro ao buscar o sample: ${response.statusText}`); + const arrayBuffer = await response.arrayBuffer(); + track.audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + console.log(`Áudio carregado para a trilha: ${track.name}`); + } catch (error) { + console.error(`Falha ao carregar áudio para a trilha ${track.name}:`, error); + track.audioBuffer = null; // Marca como falha para não tentar tocar + } + return track; +} + export function addTrackToState() { initializeAudioContext(); const audioContext = getAudioContext(); @@ -26,7 +49,7 @@ export function addTrackToState() { id: Date.now(), name: "novo instrumento", samplePath: null, - audioBuffer: null, // (NOVO) Adicionado para armazenar o áudio decodificado + audioBuffer: null, steps: [], volume: DEFAULT_VOLUME, pan: DEFAULT_PAN, @@ -47,33 +70,14 @@ export function removeLastTrackFromState() { renderApp(); } -// (ALTERADO) A função agora é 'async' para carregar e decodificar o áudio export async function updateTrackSample(trackId, samplePath) { const track = appState.tracks.find((t) => t.id == trackId); if (track) { track.samplePath = samplePath; track.name = samplePath.split("/").pop(); - track.audioBuffer = null; // Limpa o buffer antigo enquanto carrega o novo - renderApp(); // Renderiza imediatamente para mostrar o novo nome - - // (NOVO) Lógica para carregar e decodificar o áudio em segundo plano - try { - const audioContext = getAudioContext(); - if (!audioContext) initializeAudioContext(); // Garante que o contexto de áudio exista - - const response = await fetch(samplePath); - const arrayBuffer = await response.arrayBuffer(); - const decodedAudio = await audioContext.decodeAudioData(arrayBuffer); - - track.audioBuffer = decodedAudio; // Armazena o buffer decodificado no estado da track - console.log(`Sample ${track.name} carregado e decodificado com sucesso.`); - - } catch (error) { - console.error("Erro ao carregar ou decodificar o sample:", error); - track.samplePath = null; - track.name = "erro ao carregar"; - renderApp(); // Re-renderiza para mostrar a mensagem de erro - } + track.audioBuffer = null; + renderApp(); + await loadAudioForTrack(track); // Reutiliza a nova função aqui } } diff --git a/assets/js/creations/ui.js b/assets/js/creations/ui.js index 3ed6ecd..8fdfdef 100644 --- a/assets/js/creations/ui.js +++ b/assets/js/creations/ui.js @@ -8,7 +8,32 @@ import { } from "./state.js"; import { playSample } from "./audio.js"; import { getTotalSteps } from "./utils.js"; -import { loadProjectFromServer } from "./file.js"; // (CORREÇÃO) Importa a função que faltava +import { loadProjectFromServer } from "./file.js"; + +// Variável para armazenar o mapa de samples (nome do arquivo -> caminho completo) +let samplePathMap = {}; + +// Função para exportar o mapa de samples para outros módulos +export function getSamplePathMap() { + return samplePathMap; +} + +// Função recursiva para construir o mapa de samples a partir do manifest +function buildSamplePathMap(tree, currentPath) { + for (const key in tree) { + if (key === "_isFile") continue; // Ignora a propriedade de metadados + + const node = tree[key]; + const newPath = `${currentPath}/${key}`; + if (node._isFile) { + // Se for um arquivo, adiciona ao mapa + samplePathMap[key] = newPath; + } else { + // Se for um diretório, continua a busca recursivamente + buildSamplePathMap(node, newPath); + } + } +} // RENDERIZAÇÃO PRINCIPAL export function renderApp() { @@ -116,94 +141,97 @@ export function redrawSequencer() { // LÓGICA DE INTERAÇÃO DOS KNOBS function addKnobInteraction(knobElement) { - const controlType = knobElement.dataset.control; - knobElement.addEventListener("mousedown", (e) => { - if (e.button === 1) { + const controlType = knobElement.dataset.control; + knobElement.addEventListener("mousedown", (e) => { + if (e.button === 1) { // Middle mouse button + e.preventDefault(); + const trackId = knobElement.dataset.trackId; + const defaultValue = controlType === "volume" ? 0.8 : 0.0; + if (controlType === "volume") { + updateTrackVolume(trackId, defaultValue); + } else { + updateTrackPan(trackId, defaultValue); + } + updateKnobVisual(knobElement, controlType); + } + }); + knobElement.addEventListener("mousedown", (e) => { + if (e.button !== 0) return; // Apenas botão esquerdo e.preventDefault(); const trackId = knobElement.dataset.trackId; - const defaultValue = controlType === "volume" ? 0.8 : 0.0; - if (controlType === "volume") { - updateTrackVolume(trackId, defaultValue); - } else { - updateTrackPan(trackId, defaultValue); + const track = appState.tracks.find((t) => t.id == trackId); + if (!track) return; + const startY = e.clientY; + const startValue = controlType === "volume" ? track.volume : track.pan; + document.body.classList.add("knob-dragging"); + function onMouseMove(moveEvent) { + const deltaY = startY - moveEvent.clientY; + const sensitivity = controlType === "volume" ? 150 : 200; + const newValue = startValue + deltaY / sensitivity; + if (controlType === "volume") { + updateTrackVolume(trackId, newValue); + } else { + updateTrackPan(trackId, newValue); + } + updateKnobVisual(knobElement, controlType); } - } - }); - knobElement.addEventListener("mousedown", (e) => { - if (e.button !== 0) return; - e.preventDefault(); - const trackId = knobElement.dataset.trackId; - const track = appState.tracks.find((t) => t.id == trackId); - if (!track) return; - const startY = e.clientY; - const startValue = controlType === "volume" ? track.volume : track.pan; - document.body.classList.add("knob-dragging"); - function onMouseMove(moveEvent) { - const deltaY = startY - moveEvent.clientY; - const sensitivity = controlType === "volume" ? 150 : 200; - const newValue = startValue + deltaY / sensitivity; + function onMouseUp() { + document.body.classList.remove("knob-dragging"); + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + } + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }); + knobElement.addEventListener("wheel", (e) => { + e.preventDefault(); + const trackId = knobElement.dataset.trackId; + const track = appState.tracks.find((t) => t.id == trackId); + if (!track) return; + const step = 0.05; + const direction = e.deltaY < 0 ? 1 : -1; if (controlType === "volume") { + const newValue = track.volume + direction * step; updateTrackVolume(trackId, newValue); } else { + const newValue = track.pan + direction * step; updateTrackPan(trackId, newValue); } - } - function onMouseUp() { - document.body.classList.remove("knob-dragging"); - document.removeEventListener("mousemove", onMouseMove); - document.removeEventListener("mouseup", onMouseUp); - } - document.addEventListener("mousemove", onMouseMove); - document.addEventListener("mouseup", onMouseUp); - }); - knobElement.addEventListener("wheel", (e) => { - e.preventDefault(); - const trackId = knobElement.dataset.trackId; - const track = appState.tracks.find((t) => t.id == trackId); - if (!track) return; - const step = 0.05; - const direction = e.deltaY < 0 ? 1 : -1; - if (controlType === "volume") { - const newValue = track.volume + direction * step; - updateTrackVolume(trackId, newValue); - } else { - const newValue = track.pan + direction * step; - updateTrackPan(trackId, newValue); - } - }); + updateKnobVisual(knobElement, controlType); + }); } function updateKnobVisual(knobElement, controlType) { - const trackId = knobElement.dataset.trackId; - const track = appState.tracks.find((t) => t.id == trackId); - if (!track) return; - const indicator = knobElement.querySelector(".knob-indicator"); - if (!indicator) return; - const minAngle = -135; - const maxAngle = 135; - let percentage = 0.5; - let title = ""; - if (controlType === "volume") { - const value = track.volume; - const clampedValue = Math.max(0, Math.min(1.5, value)); - percentage = clampedValue / 1.5; - title = `Volume: ${Math.round(clampedValue * 100)}%`; - } else { - const value = track.pan; - const clampedValue = Math.max(-1, Math.min(1, value)); - percentage = (clampedValue + 1) / 2; - const panDisplay = Math.round(clampedValue * 100); - title = `Pan: ${ - panDisplay === 0 - ? "Centro" - : panDisplay < 0 - ? `${-panDisplay} L` - : `${panDisplay} R` - }`; - } - const angle = minAngle + percentage * (maxAngle - minAngle); - indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`; - knobElement.title = title; + const trackId = knobElement.dataset.trackId; + const track = appState.tracks.find((t) => t.id == trackId); + if (!track) return; + const indicator = knobElement.querySelector(".knob-indicator"); + if (!indicator) return; + const minAngle = -135; + const maxAngle = 135; + let percentage = 0.5; + let title = ""; + if (controlType === "volume") { + const value = track.volume; + const clampedValue = Math.max(0, Math.min(1.5, value)); + percentage = clampedValue / 1.5; + title = `Volume: ${Math.round(clampedValue * 100)}%`; + } else { + const value = track.pan; + const clampedValue = Math.max(-1, Math.min(1, value)); + percentage = (clampedValue + 1) / 2; + const panDisplay = Math.round(clampedValue * 100); + title = `Pan: ${ + panDisplay === 0 + ? "Centro" + : panDisplay < 0 + ? `${-panDisplay} L` + : `${panDisplay} R` + }`; + } + const angle = minAngle + percentage * (maxAngle - minAngle); + indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`; + knobElement.title = title; } export function highlightStep(stepIndex, isActive) { @@ -230,6 +258,11 @@ export async function loadAndRenderSampleBrowser() { throw new Error("Arquivo samples-manifest.json não encontrado."); } const fileTree = await response.json(); + + samplePathMap = {}; + buildSamplePathMap(fileTree, "src/samples"); + console.log("Mapa de samples construído:", samplePathMap); + renderFileTree(fileTree, browserContent, "src/samples"); } catch (error) { console.error("Erro ao carregar samples:", error); @@ -238,82 +271,82 @@ export async function loadAndRenderSampleBrowser() { } function renderFileTree(tree, parentElement, currentPath) { - parentElement.innerHTML = ""; - const ul = document.createElement("ul"); - const sortedKeys = Object.keys(tree).sort((a, b) => { - const aIsFile = tree[a]._isFile; - const bIsFile = tree[b]._isFile; - if (aIsFile === bIsFile) return a.localeCompare(b); - return aIsFile ? 1 : -1; - }); - for (const key of sortedKeys) { - const node = tree[key]; - const li = document.createElement("li"); - const newPath = `${currentPath}/${key}`; - if (node._isFile) { - li.innerHTML = ` ${key}`; - li.setAttribute("draggable", true); - li.addEventListener("click", (e) => { - e.stopPropagation(); - playSample(newPath, null); - }); - li.addEventListener("dragstart", (e) => { - e.dataTransfer.setData("text/plain", newPath); - e.dataTransfer.effectAllowed = "copy"; - }); - ul.appendChild(li); - } else { - li.className = "directory"; - li.innerHTML = ` ${key}`; - const nestedUl = document.createElement("ul"); - renderFileTree(node, nestedUl, newPath); - li.appendChild(nestedUl); - li.addEventListener("click", (e) => { - e.stopPropagation(); - li.classList.toggle("open"); - }); - ul.appendChild(li); + parentElement.innerHTML = ""; + const ul = document.createElement("ul"); + const sortedKeys = Object.keys(tree).sort((a, b) => { + const aIsFile = tree[a]._isFile; + const bIsFile = tree[b]._isFile; + if (aIsFile === bIsFile) return a.localeCompare(b); + return aIsFile ? 1 : -1; + }); + for (const key of sortedKeys) { + const node = tree[key]; + const li = document.createElement("li"); + const newPath = `${currentPath}/${key}`; + if (node._isFile) { + li.innerHTML = ` ${key}`; + li.setAttribute("draggable", true); + li.addEventListener("click", (e) => { + e.stopPropagation(); + playSample(newPath, null); + }); + li.addEventListener("dragstart", (e) => { + e.dataTransfer.setData("text/plain", newPath); + e.dataTransfer.effectAllowed = "copy"; + }); + ul.appendChild(li); + } else { + li.className = "directory"; + li.innerHTML = ` ${key}`; + const nestedUl = document.createElement("ul"); + renderFileTree(node, nestedUl, newPath); + li.appendChild(nestedUl); + li.addEventListener("click", (e) => { + e.stopPropagation(); + li.classList.toggle("open"); + }); + ul.appendChild(li); + } } - } - parentElement.appendChild(ul); + parentElement.appendChild(ul); } export async function showOpenProjectModal() { - const openProjectModal = document.getElementById("open-project-modal"); - const serverProjectsList = document.getElementById("server-projects-list"); - serverProjectsList.innerHTML = "

Carregando...

"; - openProjectModal.classList.add("visible"); - try { - const response = await fetch("metadata/mmp-manifest.json"); - if (!response.ok) - throw new Error("Arquivo mmp-manifest.json não encontrado."); - const projects = await response.json(); - - serverProjectsList.innerHTML = ""; - if (projects.length === 0) { - serverProjectsList.innerHTML = - '

Nenhum projeto encontrado no servidor.

'; - } - - projects.forEach((projectName) => { - const projectItem = document.createElement("div"); - projectItem.className = "project-item"; - projectItem.textContent = projectName; - projectItem.addEventListener("click", async () => { - const success = await loadProjectFromServer(projectName); - if (success) { - closeOpenProjectModal(); - } + const openProjectModal = document.getElementById("open-project-modal"); + const serverProjectsList = document.getElementById("server-projects-list"); + serverProjectsList.innerHTML = "

Carregando...

"; + openProjectModal.classList.add("visible"); + try { + const response = await fetch("metadata/mmp-manifest.json"); + if (!response.ok) + throw new Error("Arquivo mmp-manifest.json não encontrado."); + const projects = await response.json(); + + serverProjectsList.innerHTML = ""; + if (projects.length === 0) { + serverProjectsList.innerHTML = + '

Nenhum projeto encontrado no servidor.

'; + } + + projects.forEach((projectName) => { + const projectItem = document.createElement("div"); + projectItem.className = "project-item"; + projectItem.textContent = projectName; + projectItem.addEventListener("click", async () => { + const success = await loadProjectFromServer(projectName); + if (success) { + closeOpenProjectModal(); + } + }); + serverProjectsList.appendChild(projectItem); }); - serverProjectsList.appendChild(projectItem); - }); - } catch (error) { - console.error("Erro ao carregar lista de projetos:", error); - serverProjectsList.innerHTML = `

${error.message}

`; - } + } catch (error) { + console.error("Erro ao carregar lista de projetos:", error); + serverProjectsList.innerHTML = `

${error.message}

`; + } } - + export function closeOpenProjectModal() { - const openProjectModal = document.getElementById("open-project-modal"); - openProjectModal.classList.remove("visible"); -} + const openProjectModal = document.getElementById("open-project-modal"); + openProjectModal.classList.remove("visible"); +} \ No newline at end of file diff --git a/assets/js/creations/utils.js b/assets/js/creations/utils.js index ca7f2d8..9187e5f 100644 --- a/assets/js/creations/utils.js +++ b/assets/js/creations/utils.js @@ -1,12 +1,16 @@ // js/utils.js export function getTotalSteps() { + const barsInput = document.getElementById("bars-input"); const compassoAInput = document.getElementById("compasso-a-input"); const compassoBInput = document.getElementById("compasso-b-input"); + + const numberOfBars = parseInt(barsInput.value, 10) || 1; const beatsPerBar = parseInt(compassoAInput.value, 10) || 4; const noteValue = parseInt(compassoBInput.value, 10) || 4; const subdivisions = Math.round(16 / noteValue); - return beatsPerBar * subdivisions; + + return numberOfBars * beatsPerBar * subdivisions; } export function enforceNumericInput(event) { @@ -24,4 +28,4 @@ export function adjustValue(inputElement, step) { // Dispara um evento 'input' para que outros listeners (como o que redesenha o sequenciador) sejam acionados. inputElement.dispatchEvent(new Event("input", { bubbles: true })); -} +} \ No newline at end of file diff --git a/creation.html b/creation.html index 9f6b25c..350f5b9 100644 --- a/creation.html +++ b/creation.html @@ -67,6 +67,23 @@
ANDAMENTO/BPM
+
+
+ +
+
COMPASSOS
+
@@ -119,10 +136,11 @@
- 0:00:00 + 00:00:00
MIN:SEC:MSEC
@@ -159,7 +177,7 @@ + >
- + \ No newline at end of file