diff --git a/assets/js/creations/audio.js b/assets/js/creations/audio.js index 42850ea..46edd0d 100644 --- a/assets/js/creations/audio.js +++ b/assets/js/creations/audio.js @@ -22,7 +22,6 @@ export function initializeAudioContext() { mainGainNode = audioContext.createGain(); masterPannerNode = audioContext.createStereoPanner(); - // Roteamento: Gain Master -> Panner Master -> Saída mainGainNode.connect(masterPannerNode); masterPannerNode.connect(audioContext.destination); } @@ -75,17 +74,13 @@ export function playSample(filePath, trackId) { const track = trackId ? appState.tracks.find((t) => t.id == trackId) : null; - if (!track) { + if (!track || !track.audioBuffer) { + // Se não houver buffer (ex: preview do sample browser), toca como um áudio simples const audio = new Audio(filePath); audio.play(); return; } - if (!track.audioBuffer) { - console.warn(`Buffer para a trilha ${track.name} ainda não carregado.`); - return; - } - const source = audioContext.createBufferSource(); source.buffer = track.audioBuffer; @@ -124,11 +119,20 @@ function tick() { } } + // --- INÍCIO DA CORREÇÃO --- appState.tracks.forEach((track) => { - if (track.steps[appState.currentStep] && track.samplePath) { + // 1. Verifica se a faixa tem patterns + if (!track.patterns || track.patterns.length === 0) return; + + // 2. Pega o pattern que está ativo para esta faixa + const activePattern = track.patterns[track.activePatternIndex]; + + // 3. Verifica se o pattern existe e se o step atual está ativo NELE + if (activePattern && activePattern.steps[appState.currentStep] && track.samplePath) { playSample(track.samplePath, track.id); } }); + // --- FIM DA CORREÇÃO --- highlightStep(appState.currentStep, true); appState.currentStep = (appState.currentStep + 1) % totalSteps; @@ -151,31 +155,43 @@ export function startPlayback() { } export function stopPlayback() { - clearInterval(appState.playbackIntervalId); + if(appState.playbackIntervalId) { + clearInterval(appState.playbackIntervalId); + } appState.playbackIntervalId = null; appState.isPlaying = false; highlightStep(appState.currentStep - 1, false); + highlightStep(appState.currentStep, false); // Garante que o último step "playing" seja limpo appState.currentStep = 0; if (timerDisplay) timerDisplay.textContent = '00:00:00'; - document.getElementById("play-btn").classList.remove("fa-pause"); - document.getElementById("play-btn").classList.add("fa-play"); + const playBtn = document.getElementById("play-btn"); + if (playBtn) { + playBtn.classList.remove("fa-pause"); + playBtn.classList.add("fa-play"); + } } export function rewindPlayback() { + const previousStep = appState.currentStep; appState.currentStep = 0; if (!appState.isPlaying) { if (timerDisplay) timerDisplay.textContent = '00:00:00'; - document - .querySelectorAll(".step.playing") - .forEach((s) => s.classList.remove("playing")); + highlightStep(previousStep - 1, false); + highlightStep(previousStep, false); } } export function togglePlayback() { + initializeAudioContext(); // Garante que o contexto de áudio foi iniciado por um gesto do usuário if (appState.isPlaying) { - stopPlayback(); + // Pausa a reprodução, mas não reseta + clearInterval(appState.playbackIntervalId); + appState.playbackIntervalId = null; + appState.isPlaying = false; + document.getElementById("play-btn").classList.remove("fa-pause"); + document.getElementById("play-btn").classList.add("fa-play"); } else { startPlayback(); } diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index e31e345..dd21b72 100644 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -2,7 +2,7 @@ import { appState, loadAudioForTrack } from "./state.js"; import { getTotalSteps } from "./utils.js"; import { renderApp, getSamplePathMap } from "./ui.js"; -import { NOTE_LENGTH, TICKS_PER_BAR } from "./config.js"; +import { DEFAULT_PAN, DEFAULT_VOLUME, NOTE_LENGTH, TICKS_PER_BAR } from "./config.js"; import { initializeAudioContext, getAudioContext, @@ -19,9 +19,7 @@ export async function handleFileLoad(file) { name.toLowerCase().endsWith(".mmp") ); if (!projectFile) - throw new Error( - "Não foi possível encontrar um arquivo .mmp dentro do .mmpz" - ); + throw new Error("Não foi possível encontrar um arquivo .mmp dentro do .mmpz"); xmlContent = await zip.files[projectFile].async("string"); } else { xmlContent = await file.text(); @@ -39,77 +37,159 @@ export async function parseMmpContent(xmlString) { const xmlDoc = parser.parseFromString(xmlString, "application/xml"); appState.originalXmlDoc = xmlDoc; - const newTracks = []; + let newTracks = []; const head = xmlDoc.querySelector("head"); if (head) { 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 allBBTrackNodes = Array.from(xmlDoc.querySelectorAll('song > trackcontainer[type="song"] > track[type="1"]')); + if (allBBTrackNodes.length === 0) { + appState.tracks = []; renderApp(); return; + } + + // --- INÍCIO DA CORREÇÃO FINAL DE ORDENAÇÃO --- + // A lista de NOMES é ordenada em ordem CRESCENTE (a ordem correta, cronológica). + const sortedBBTrackNameNodes = [...allBBTrackNodes].sort((a, b) => { + const bbtcoA = a.querySelector('bbtco'); + const bbtcoB = b.querySelector('bbtco'); + const posA = bbtcoA ? parseInt(bbtcoA.getAttribute('pos'), 10) : Infinity; + const posB = bbtcoB ? parseInt(bbtcoB.getAttribute('pos'), 10) : Infinity; + return posA - posB; // Ordem crescente + }); + + const dataSourceTrack = allBBTrackNodes[0]; + appState.currentBeatBasslineName = dataSourceTrack.getAttribute("name") || "Beat/Bassline"; + const bbTrackContainer = dataSourceTrack.querySelector('bbtrack > trackcontainer'); + if (!bbTrackContainer) { + appState.tracks = []; renderApp(); return; + } + + const instrumentTracks = bbTrackContainer.querySelectorAll('track[type="0"]'); const pathMap = getSamplePathMap(); - - sampleTrackElements.forEach((instrumentNode) => { - const afpNode = instrumentNode.querySelector("audiofileprocessor"); - const instrumentTrackNode = instrumentNode.parentElement; - const trackNode = instrumentTrackNode.parentElement; - if (!afpNode || !instrumentTrackNode || !trackNode) return; - - const audioContext = getAudioContext(); - const mainGainNode = getMainGainNode(); + + newTracks = Array.from(instrumentTracks).map(trackNode => { + const instrumentNode = trackNode.querySelector("instrument"); + const instrumentTrackNode = trackNode.querySelector("instrumenttrack"); + if (!instrumentNode || !instrumentTrackNode) return null; - const totalSteps = getTotalSteps(); - const newSteps = new Array(totalSteps).fill(false); + const trackName = trackNode.getAttribute("name"); - const ticksPerStep = 12; + if (instrumentNode.getAttribute("name") === 'tripleoscillator') { + return null; + } - 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 allPatternsNodeList = trackNode.querySelectorAll("pattern"); + // A lista de CONTEÚDO dos patterns é ordenada de forma DECRESCENTE para corresponder. + const allPatternsArray = Array.from(allPatternsNodeList).sort((a, b) => { + const posA = parseInt(a.getAttribute('pos'), 10) || 0; + const posB = parseInt(b.getAttribute('pos'), 10) || 0; + return posB - posA; // Ordem decrescente }); + // --- FIM DA CORREÇÃO FINAL DE ORDENAÇÃO --- - const srcAttribute = afpNode.getAttribute("src"); - const filename = srcAttribute.split("/").pop(); - const finalSamplePath = pathMap[filename] || `src/samples/${srcAttribute}`; + const patterns = sortedBBTrackNameNodes.map((bbTrack, index) => { + const patternNode = allPatternsArray[index]; + const bbTrackName = bbTrack.getAttribute("name") || `Pattern ${index + 1}`; - const newTrack = { + if (!patternNode) { + const firstPattern = allPatternsArray[0]; + const stepsLength = firstPattern ? parseInt(firstPattern.getAttribute("steps"), 10) || 16 : 16; + return { name: bbTrackName, steps: new Array(stepsLength).fill(false), pos: 0 }; + } + + const patternSteps = parseInt(patternNode.getAttribute("steps"), 10) || 16; + const steps = new Array(patternSteps).fill(false); + const ticksPerStep = 12; + + patternNode.querySelectorAll("note").forEach((noteNode) => { + const noteLocalPos = parseInt(noteNode.getAttribute("pos"), 10); + const stepIndex = Math.round(noteLocalPos / ticksPerStep); + if (stepIndex < patternSteps) { + steps[stepIndex] = true; + } + }); + + return { + name: bbTrackName, + steps: steps, + pos: parseInt(patternNode.getAttribute("pos"), 10) || 0 + }; + }); + + const hasNotes = patterns.some(p => p.steps.includes(true)); + if (!hasNotes) return null; + + const afpNode = instrumentNode.querySelector("audiofileprocessor"); + const sampleSrc = afpNode ? afpNode.getAttribute("src") : null; + let finalSamplePath = null; + if (sampleSrc) { + const filename = sampleSrc.split("/").pop(); + if (pathMap[filename]) { + finalSamplePath = pathMap[filename]; + } else { + let cleanSrc = sampleSrc; + if (cleanSrc.startsWith('samples/')) { + cleanSrc = cleanSrc.substring('samples/'.length); + } + finalSamplePath = `src/samples/${cleanSrc}`; + } + } + + const volFromFile = parseFloat(instrumentTrackNode.getAttribute("vol")); + const panFromFile = parseFloat(instrumentTrackNode.getAttribute("pan")); + const firstPatternWithNotesIndex = patterns.findIndex(p => p.steps.includes(true)); + + return { id: Date.now() + Math.random(), - name: filename || trackNode.getAttribute("name"), + name: trackName, samplePath: finalSamplePath, - audioBuffer: null, - steps: newSteps, - volume: parseFloat(instrumentTrackNode.getAttribute("vol")) / 100, - pan: parseFloat(instrumentTrackNode.getAttribute("pan")) / 100, - gainNode: audioContext.createGain(), - pannerNode: audioContext.createStereoPanner(), + patterns: patterns, + activePatternIndex: firstPatternWithNotesIndex !== -1 ? firstPatternWithNotesIndex : 0, + volume: !isNaN(volFromFile) ? volFromFile / 100 : DEFAULT_VOLUME, + pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN, + instrumentName: instrumentNode.getAttribute("name"), + instrumentXml: instrumentNode.innerHTML, }; - newTrack.gainNode.connect(newTrack.pannerNode); - newTrack.pannerNode.connect(mainGainNode); - newTrack.gainNode.gain.value = newTrack.volume; - newTrack.pannerNode.pan.value = newTrack.pan; - newTracks.push(newTrack); + }).filter(track => track !== null); + + let isFirstTrackWithNotes = true; + newTracks.forEach(track => { + const audioContext = getAudioContext(); + track.gainNode = audioContext.createGain(); + track.pannerNode = audioContext.createStereoPanner(); + track.gainNode.connect(track.pannerNode); + track.pannerNode.connect(getMainGainNode()); + track.gainNode.gain.value = track.volume; + track.pannerNode.pan.value = track.pan; + + if (isFirstTrackWithNotes) { + const activeIdx = track.activePatternIndex || 0; + const activePattern = track.patterns[activeIdx]; + if (activePattern) { + const firstPatternSteps = activePattern.steps.length; + const stepsPerBar = 16; + const requiredBars = Math.ceil(firstPatternSteps / stepsPerBar); + document.getElementById("bars-input").value = requiredBars > 0 ? requiredBars : 1; + isFirstTrackWithNotes = false; + } + } }); 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; + appState.activeTrackId = appState.tracks[0]?.id || null; renderApp(); - console.log("Projeto carregado com sucesso!", appState); } export function generateMmpFile() { @@ -120,8 +200,80 @@ export function generateMmpFile() { } } +function createTrackXml(track) { + if (track.patterns.length === 0) return ""; + + const ticksPerStep = 12; + const lmmsVolume = Math.round(track.volume * 100); + const lmmsPan = Math.round(track.pan * 100); + + const patternsXml = track.patterns.map(pattern => { + const patternNotes = pattern.steps.map((isActive, index) => { + if (isActive) { + const notePos = Math.round(index * ticksPerStep); + return ``; + } + return ""; + }).join("\n "); + + return ` + ${patternNotes} + `; + }).join('\n '); + + return ` + + + + ${track.instrumentXml} + + + + ${patternsXml} + `; +} + +function modifyAndSaveExistingMmp() { + console.log("Modificando arquivo .mmp existente..."); + const xmlDoc = appState.originalXmlDoc.cloneNode(true); + 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 + ); + head.setAttribute( + "timesig_denominator", + document.getElementById("compasso-b-input").value + ); + } + + const bbTrackContainer = xmlDoc.querySelector('track[type="1"] > bbtrack > trackcontainer'); + + if (bbTrackContainer) { + bbTrackContainer.querySelectorAll('track[type="0"]').forEach(node => node.remove()); + + const tracksXml = appState.tracks + .map((track) => createTrackXml(track)) + .join(""); + + const tempDoc = new DOMParser().parseFromString( + `${tracksXml}`, + "application/xml" + ); + Array.from(tempDoc.documentElement.children).forEach((newTrackNode) => { + bbTrackContainer.appendChild(newTrackNode); + }); + } + + const serializer = new XMLSerializer(); + const mmpContent = serializer.serializeToString(xmlDoc); + downloadFile(mmpContent, "projeto_editado.mmp"); +} + function generateNewMmp() { - console.log("Gerando novo arquivo .mmp do zero..."); 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; @@ -151,90 +303,13 @@ function generateNewMmp() { -

Feito com MMPCreator no https://alice.ufsj.edu.br/MMPSearch/creator

+

Feito com MMPCreator

]]> `; downloadFile(mmpContent, "novo_projeto.mmp"); } -function modifyAndSaveExistingMmp() { - console.log("Modificando arquivo .mmp existente..."); - const xmlDoc = appState.originalXmlDoc.cloneNode(true); - 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 - ); - head.setAttribute( - "timesig_denominator", - document.getElementById("compasso-b-input").value - ); - } - const bbTrackContainer = xmlDoc.querySelector("bbtrack > trackcontainer"); - if (bbTrackContainer) { - const oldSampleTracks = bbTrackContainer.querySelectorAll( - 'instrument[name="audiofileprocessor"]' - ); - oldSampleTracks.forEach((node) => node.closest("track").remove()); - const tracksXml = appState.tracks - .map((track) => createTrackXml(track)) - .join(""); - const tempDoc = new DOMParser().parseFromString( - `${tracksXml}`, - "application/xml" - ); - Array.from(tempDoc.documentElement.children).forEach((newTrackNode) => { - bbTrackContainer.appendChild(newTrackNode); - }); - } - - const serializer = new XMLSerializer(); - const mmpContent = serializer.serializeToString(xmlDoc); - downloadFile(mmpContent, "projeto_editado.mmp"); -} - -function createTrackXml(track) { - if (!track.samplePath) return ""; - const totalSteps = track.steps.length || getTotalSteps(); - const ticksPerStep = 12; - const lmmsVolume = Math.round(track.volume * 100); - const lmmsPan = Math.round(track.pan * 100); - const sampleSrc = track.samplePath.replace("src/samples/", ""); - - 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 ` - - - - - - - - ${patternsXml} - `; -} - function downloadFile(content, fileName) { const blob = new Blob([content], { type: "application/xml;charset=utf-8" }); const url = URL.createObjectURL(blob); diff --git a/assets/js/creations/state.js b/assets/js/creations/state.js index 2945a76..b2b9060 100644 --- a/assets/js/creations/state.js +++ b/assets/js/creations/state.js @@ -6,10 +6,12 @@ import { getMainGainNode, } from "./audio.js"; import { renderApp } from "./ui.js"; +import { getTotalSteps } from "./utils.js"; -// O "cérebro" da aplicação export let appState = { tracks: [], + activeTrackId: null, + activePatternIndex: 0, // <-- VOLTOU A SER GLOBAL isPlaying: false, playbackIntervalId: null, currentStep: 0, @@ -20,19 +22,14 @@ export let appState = { }; export async function loadAudioForTrack(track) { - if (!track.samplePath) { - console.warn("Track sem samplePath, pulando o carregamento de áudio."); - return track; - } + if (!track.samplePath) 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; @@ -44,13 +41,18 @@ export function addTrackToState() { initializeAudioContext(); const audioContext = getAudioContext(); const mainGainNode = getMainGainNode(); + const totalSteps = getTotalSteps(); + const referenceTrack = appState.tracks[0]; const newTrack = { id: Date.now(), name: "novo instrumento", samplePath: null, audioBuffer: null, - steps: [], + patterns: referenceTrack + ? referenceTrack.patterns.map(p => ({ name: p.name, steps: new Array(p.steps.length).fill(false), pos: p.pos })) + : [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), pos: 0 }], + // activePatternIndex foi removido daqui volume: DEFAULT_VOLUME, pan: DEFAULT_PAN, gainNode: audioContext.createGain(), @@ -62,12 +64,20 @@ export function addTrackToState() { newTrack.pannerNode.pan.value = newTrack.pan; appState.tracks.push(newTrack); + if (!appState.activeTrackId) { + appState.activeTrackId = newTrack.id; + } renderApp(); } export function removeLastTrackFromState() { - appState.tracks.pop(); - renderApp(); + if (appState.tracks.length > 0) { + const removedTrack = appState.tracks.pop(); + if (appState.activeTrackId === removedTrack.id) { + appState.activeTrackId = appState.tracks[0]?.id || null; + } + renderApp(); + } } export async function updateTrackSample(trackId, samplePath) { @@ -76,41 +86,43 @@ export async function updateTrackSample(trackId, samplePath) { track.samplePath = samplePath; track.name = samplePath.split("/").pop(); track.audioBuffer = null; - renderApp(); await loadAudioForTrack(track); + const trackLane = document.querySelector(`.track-lane[data-track-id="${trackId}"] .track-name`); + if (trackLane) { + trackLane.textContent = track.name; + } } } export function toggleStepState(trackId, stepIndex) { const track = appState.tracks.find((t) => t.id == trackId); - if (track) { - track.steps[stepIndex] = !track.steps[stepIndex]; + if (track && track.patterns && track.patterns.length > 0) { + // Usa o índice GLOBAL para saber qual pattern modificar + const activePattern = track.patterns[appState.activePatternIndex]; + if (activePattern && activePattern.steps.length > stepIndex) { + activePattern.steps[stepIndex] = !activePattern.steps[stepIndex]; + } } } export function updateTrackVolume(trackId, volume) { const track = appState.tracks.find((t) => t.id == trackId); - const audioContext = getAudioContext(); if (track) { const clampedVolume = Math.max(0, Math.min(1.5, volume)); track.volume = clampedVolume; if (track.gainNode) { - track.gainNode.gain.setValueAtTime( - clampedVolume, - audioContext.currentTime - ); + track.gainNode.gain.setValueAtTime(clampedVolume, getAudioContext().currentTime); } } } export function updateTrackPan(trackId, pan) { const track = appState.tracks.find((t) => t.id == trackId); - const audioContext = getAudioContext(); if (track) { const clampedPan = Math.max(-1, Math.min(1, pan)); track.pan = clampedPan; if (track.pannerNode) { - track.pannerNode.pan.setValueAtTime(clampedPan, audioContext.currentTime); + track.pannerNode.pan.setValueAtTime(clampedPan, getAudioContext().currentTime); } } } \ No newline at end of file diff --git a/assets/js/creations/ui.js b/assets/js/creations/ui.js index a490d56..dc2fdd3 100644 --- a/assets/js/creations/ui.js +++ b/assets/js/creations/ui.js @@ -10,66 +10,111 @@ import { playSample } from "./audio.js"; import { getTotalSteps } from "./utils.js"; import { loadProjectFromServer } from "./file.js"; -// Variável para armazenar o mapa de samples (nome do arquivo -> caminho completo) let samplePathMap = {}; +const globalPatternSelector = document.getElementById('global-pattern-selector'); + +globalPatternSelector.addEventListener('change', () => { + // Atualiza o índice GLOBAL + appState.activePatternIndex = parseInt(globalPatternSelector.value, 10); + + const firstTrack = appState.tracks[0]; + if (firstTrack) { + const activePattern = firstTrack.patterns[appState.activePatternIndex]; + if (activePattern) { + const stepsPerBar = 16; + const requiredBars = Math.ceil(activePattern.steps.length / stepsPerBar); + document.getElementById("bars-input").value = requiredBars > 0 ? requiredBars : 1; + } + } + redrawSequencer(); +}); + +export function updateGlobalPatternSelector() { + const referenceTrack = appState.tracks[0]; + globalPatternSelector.innerHTML = ''; + + if (referenceTrack && referenceTrack.patterns.length > 0) { + referenceTrack.patterns.forEach((pattern, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = pattern.name; + globalPatternSelector.appendChild(option); + }); + // Usa o índice GLOBAL + globalPatternSelector.selectedIndex = appState.activePatternIndex; + globalPatternSelector.disabled = false; + } else { + const option = document.createElement('option'); + option.textContent = 'Sem patterns'; + globalPatternSelector.appendChild(option); + globalPatternSelector.disabled = true; + } +} -// Função para exportar o mapa de samples, que estava faltando 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 - + if (key === "_isFile") continue; 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() { const trackContainer = document.getElementById("track-container"); trackContainer.innerHTML = ""; + appState.tracks.forEach((trackData) => { const trackLane = document.createElement("div"); trackLane.className = "track-lane"; trackLane.dataset.trackId = trackData.id; - trackLane.innerHTML = ` -
${trackData.name}
-
-
-
-
-
- VOL -
-
-
-
-
- PAN -
-
-
- `; + if (trackData.id === appState.activeTrackId) { + trackLane.classList.add('active-track'); + } - trackLane.addEventListener("dragover", (e) => { - e.preventDefault(); - trackLane.classList.add("drag-over"); + trackLane.innerHTML = ` +
+ +
+ ${trackData.name} +
+
+
+
+
+
+ VOL +
+
+
+
+
+ PAN +
+
+
+ `; + + trackLane.addEventListener('click', () => { + if (appState.activeTrackId === trackData.id) return; + appState.activeTrackId = trackData.id; + document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track')); + trackLane.classList.add('active-track'); + updateGlobalPatternSelector(); + redrawSequencer(); }); - trackLane.addEventListener("dragleave", () => - trackLane.classList.remove("drag-over") - ); + + trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); }); + trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over")); trackLane.addEventListener("drop", (e) => { e.preventDefault(); trackLane.classList.remove("drag-over"); @@ -80,25 +125,20 @@ export function renderApp() { }); trackContainer.appendChild(trackLane); - const volumeKnob = trackLane.querySelector(".knob[data-control='volume']"); addKnobInteraction(volumeKnob); updateKnobVisual(volumeKnob, "volume"); - const panKnob = trackLane.querySelector(".knob[data-control='pan']"); addKnobInteraction(panKnob); updateKnobVisual(panKnob, "pan"); }); + + updateGlobalPatternSelector(); redrawSequencer(); } export function redrawSequencer() { - const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4; - const noteValue = parseInt(document.getElementById("compasso-b-input").value, 10) || 4; - const subdivisions = Math.round(16 / noteValue); - const stepsPerBar = beatsPerBar * subdivisions; - const totalSteps = getTotalSteps(); - + const totalGridSteps = getTotalSteps(); document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => { let sequencerContainer = wrapper.querySelector(".step-sequencer"); if (!sequencerContainer) { @@ -111,37 +151,45 @@ export function redrawSequencer() { const trackId = parentTrackElement.dataset.trackId; const trackData = appState.tracks.find((t) => t.id == trackId); - if (trackData && trackData.steps.length !== totalSteps) { - const newStepsState = new Array(totalSteps).fill(false); - for (let i = 0; i < Math.min(trackData.steps.length, totalSteps); i++) { - newStepsState[i] = trackData.steps[i]; - } - trackData.steps = newStepsState; + if (!trackData || !trackData.patterns || trackData.patterns.length === 0) { + sequencerContainer.innerHTML = ""; + return; } + // A CORREÇÃO FINAL: Usa o índice GLOBAL para desenhar CADA faixa + const activePattern = trackData.patterns[appState.activePatternIndex]; + if (!activePattern) { + sequencerContainer.innerHTML = ""; + return; + } + const patternSteps = activePattern.steps; + sequencerContainer.innerHTML = ""; - for (let i = 0; i < totalSteps; i++) { + for (let i = 0; i < totalGridSteps; i++) { const stepWrapper = document.createElement("div"); stepWrapper.className = "step-wrapper"; - const stepElement = document.createElement("div"); stepElement.className = "step"; - if (trackData && trackData.steps[i] === true) { + + if (patternSteps[i] === true) { stepElement.classList.add("active"); } + stepElement.addEventListener("click", () => { - toggleStepState(trackData.id, i); + toggleStepState(trackData.id, i); stepElement.classList.toggle("active"); if (trackData && trackData.samplePath) { playSample(trackData.samplePath, trackData.id); } }); + const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4; const groupIndex = Math.floor(i / beatsPerBar); if (groupIndex % 2 === 0) { stepElement.classList.add("step-dark"); } + const stepsPerBar = 16; if (i > 0 && i % stepsPerBar === 0) { const marker = document.createElement("div"); marker.className = "step-marker"; @@ -275,7 +323,6 @@ export async function loadAndRenderSampleBrowser() { samplePathMap = {}; buildSamplePathMap(fileTree, "src/samples"); - console.log("Mapa de samples construído:", samplePathMap); renderFileTree(fileTree, browserContent, "src/samples"); } catch (error) { @@ -294,6 +341,7 @@ function renderFileTree(tree, parentElement, currentPath) { return aIsFile ? 1 : -1; }); for (const key of sortedKeys) { + if (key === '_isFile') continue; const node = tree[key]; const li = document.createElement("li"); const newPath = `${currentPath}/${key}`; diff --git a/creation.html b/creation.html index c457f88..7bac7f6 100644 --- a/creation.html +++ b/creation.html @@ -181,10 +181,10 @@
- +

+