diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index 87dc6d61..d0a4d3d3 100755 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -16,7 +16,7 @@ import * as Tone from "https://esm.sh/tone"; import { sendAction } from "./socket.js"; //-------------------------------------------------------------- -// +// MANIPULAÇÃO DE ARQUIVOS E PARSING //-------------------------------------------------------------- export function handleLocalProjectReset() { @@ -32,10 +32,14 @@ export function handleLocalProjectReset() { resetProjectState(); - 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; + const bpmInput = document.getElementById("bpm-input"); + if(bpmInput) bpmInput.value = 140; + + // Reseta inputs visuais se existirem + ["bars-input", "compasso-a-input", "compasso-b-input"].forEach(id => { + const el = document.getElementById(id); + if(el) el.value = (id === "bars-input") ? 1 : 4; + }); renderAll(); } @@ -82,6 +86,9 @@ export async function loadProjectFromServer(fileName) { } } +// ================================================================= +// 🔥 FUNÇÃO DE PARSING PRINCIPAL (CORRIGIDA) +// ================================================================= export async function parseMmpContent(xmlString) { resetProjectState(); initializeAudioContext(); @@ -98,74 +105,40 @@ export async function parseMmpContent(xmlString) { appState.global.originalXmlDoc = xmlDoc; let newTracks = []; + // Configuração Global (BPM, Compasso) 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; + const setVal = (id, attr, def) => { + const el = document.getElementById(id); + if(el) el.value = head.getAttribute(attr) || def; + }; + setVal("bpm-input", "bpm", 140); + setVal("compasso-a-input", "timesig_numerator", 4); + setVal("compasso-b-input", "timesig_denominator", 4); } - // --- CORREÇÃO DA SELEÇÃO DE TRACKS --- - - // 1. Identifica as faixas containers de Beat/Bassline (Type 1) - const bbEditorTrackNodes = Array.from( - xmlDoc.querySelectorAll( - 'song > trackcontainer[type="song"] > track[type="1"]' - ) - ); - - // 2. Identifica os nomes dos patterns (colunas do B/B Editor) - tag - // Precisamos disso para dar nome aos patterns e saber quantos criar - let sortedBBTrackNameNodes = []; - if (bbEditorTrackNodes.length > 0) { - // Pega do primeiro editor encontrado - sortedBBTrackNameNodes = Array.from( - bbEditorTrackNodes[0].querySelectorAll("bbtco") - ).sort((a, b) => { - const posA = parseInt(a.getAttribute("pos"), 10) || 0; - const posB = parseInt(b.getAttribute("pos"), 10) || 0; - return posA - posB; - }); - } - - // 3. Identifica os instrumentos dentro do Beat/Bassline (Type 0 aninhado) - const bbInstrumentTracks = []; - bbEditorTrackNodes.forEach((container) => { - const instruments = container.querySelectorAll( - 'bbtrack > trackcontainer > track[type="0"]' - ); - bbInstrumentTracks.push(...Array.from(instruments)); - }); - - // 4. Identifica os instrumentos do Song Editor (Type 0 direto) - const songInstrumentTracks = Array.from( - xmlDoc.querySelectorAll( - 'song > trackcontainer[type="song"] > track[type="0"]' - ) - ); - - // Junta tudo - const allInstrumentTrackNodes = [ - ...bbInstrumentTracks, - ...songInstrumentTracks, - ]; - - if (allInstrumentTrackNodes.length === 0) { - appState.pattern.tracks = []; - renderAll(); - return; - } - - // Define um nome padrão para referência - appState.global.currentBeatBasslineName = "Main Project"; - const pathMap = getSamplePathMap(); - newTracks = Array.from(allInstrumentTrackNodes) - .map((trackNode) => { + // ------------------------------------------------------------- + // 1. EXTRAÇÃO DE INSTRUMENTOS (Normal) + // ------------------------------------------------------------- + + // Seleciona todos os instrumentos (Song Editor + Beat/Bassline internos) + // Nota: Isso pega os instrumentos DENTRO das basslines, o que é correto para o Pattern Editor. + const allInstrumentTrackNodes = Array.from(xmlDoc.querySelectorAll('track[type="0"]')); + + // Identifica colunas de beat (bbtco) para dar nome aos patterns + const bbTrackNodes = Array.from(xmlDoc.querySelectorAll('track[type="1"]')); + let sortedBBTrackNameNodes = []; + if (bbTrackNodes.length > 0) { + // Pega do primeiro container para usar como referência de nomes de coluna + sortedBBTrackNameNodes = Array.from(bbTrackNodes[0].querySelectorAll("bbtco")).sort((a, b) => { + return (parseInt(a.getAttribute("pos"), 10) || 0) - (parseInt(b.getAttribute("pos"), 10) || 0); + }); + } + + // --- Processamento dos Instrumentos --- + const instrumentTracks = allInstrumentTrackNodes.map((trackNode) => { const instrumentNode = trackNode.querySelector("instrument"); const instrumentTrackNode = trackNode.querySelector("instrumenttrack"); if (!instrumentNode || !instrumentTrackNode) return null; @@ -173,60 +146,39 @@ export async function parseMmpContent(xmlString) { const trackName = trackNode.getAttribute("name"); const instrumentName = instrumentNode.getAttribute("name"); + // ... (Lógica de Patterns Mantida) ... const allPatternsNodeList = trackNode.querySelectorAll("pattern"); 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 posA - posB; + return (parseInt(a.getAttribute("pos"), 10) || 0) - (parseInt(b.getAttribute("pos"), 10) || 0); }); - // Mapeia os patterns baseados nas colunas do B/B editor (sortedBBTrackNameNodes) - // Se não houver colunas B/B (ex: projeto só Song Editor), cria 1 pattern padrão - const patternsToCreate = - sortedBBTrackNameNodes.length > 0 + const patternsToCreate = sortedBBTrackNameNodes.length > 0 ? sortedBBTrackNameNodes : [{ getAttribute: () => "Pattern 1" }]; const patterns = patternsToCreate.map((bbTrack, index) => { const patternNode = allPatternsArray[index]; - const bbTrackName = - bbTrack.getAttribute("name") || `Pattern ${index + 1}`; + const bbTrackName = bbTrack.getAttribute("name") || `Pattern ${index + 1}`; if (!patternNode) { - return { - name: bbTrackName, - steps: new Array(16).fill(false), - notes: [], - pos: 0, - }; + return { name: bbTrackName, steps: new Array(16).fill(false), notes: [], pos: 0 }; } - const patternSteps = - parseInt(patternNode.getAttribute("steps"), 10) || 16; + const patternSteps = parseInt(patternNode.getAttribute("steps"), 10) || 16; const steps = new Array(patternSteps).fill(false); const notes = []; - const ticksPerStep = 12; patternNode.querySelectorAll("note").forEach((noteNode) => { - const pos = parseInt(noteNode.getAttribute("pos"), 10); - const len = parseInt(noteNode.getAttribute("len"), 10); - const key = parseInt(noteNode.getAttribute("key"), 10); - const vol = parseInt(noteNode.getAttribute("vol"), 10); - const pan = parseInt(noteNode.getAttribute("pan"), 10); - notes.push({ - pos: pos, - len: len, - key: key, - vol: vol, - pan: pan, + pos: parseInt(noteNode.getAttribute("pos"), 10), + len: parseInt(noteNode.getAttribute("len"), 10), + key: parseInt(noteNode.getAttribute("key"), 10), + vol: parseInt(noteNode.getAttribute("vol"), 10), + pan: parseInt(noteNode.getAttribute("pan"), 10), }); - - const stepIndex = Math.round(pos / ticksPerStep); - if (stepIndex < patternSteps) { - steps[stepIndex] = true; - } + const stepIndex = Math.round(parseInt(noteNode.getAttribute("pos"), 10) / ticksPerStep); + if (stepIndex < patternSteps) steps[stepIndex] = true; }); return { @@ -237,14 +189,7 @@ export async function parseMmpContent(xmlString) { }; }); - // Verifica se tem notas em algum pattern - const hasNotes = patterns.some( - (p) => p.notes.length > 0 || p.steps.includes(true) - ); - - // Opcional: Se quiser carregar tracks vazias, remova a linha abaixo - if (!hasNotes && patterns.length === 0) return null; - + // Lógica de Sample vs Plugin let finalSamplePath = null; let trackType = "plugin"; @@ -252,16 +197,12 @@ export async function parseMmpContent(xmlString) { trackType = "sampler"; const afpNode = instrumentNode.querySelector("audiofileprocessor"); const sampleSrc = afpNode ? afpNode.getAttribute("src") : 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); - } + let cleanSrc = sampleSrc.startsWith("samples/") ? sampleSrc.substring("samples/".length) : sampleSrc; finalSamplePath = `src/samples/${cleanSrc}`; } } @@ -282,41 +223,90 @@ export async function parseMmpContent(xmlString) { instrumentName: instrumentName, instrumentXml: instrumentNode.innerHTML, }; - }) - .filter((track) => track !== null); + }).filter(t => t !== null); - let isFirstTrackWithNotes = true; + // ------------------------------------------------------------- + // 2. EXTRAÇÃO DAS TRILHAS DE BASSLINE (Playlist Container) + // ------------------------------------------------------------- + // 👇 AQUI ESTAVA FALTANDO! 👇 + + const basslineTracks = bbTrackNodes.map(trackNode => { + const trackName = trackNode.getAttribute("name") || "Beat/Bassline"; + + // Extrai os clipes da timeline (tags ) + // Eles são filhos diretos de + const playlistClips = Array.from(trackNode.querySelectorAll(":scope > bbtco")).map(bbtco => { + return { + pos: parseInt(bbtco.getAttribute("pos"), 10) || 0, + len: parseInt(bbtco.getAttribute("len"), 10) || 192, + name: trackName // O clipe leva o nome da trilha (ex: "Caixa") + }; + }); + + // Se não tiver clipes, não precisa criar a trilha visual na playlist, + // mas mantemos para consistência se desejar. + if (playlistClips.length === 0) return null; + + return { + id: `bassline_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + name: trackName, + type: "bassline", // Tipo especial que o audio_ui.js reconhece + playlist_clips: playlistClips, + // Propriedades dummy para não quebrar o pattern_state/ui + volume: 1, + pan: 0, + patterns: [], + isMuted: trackNode.getAttribute("muted") === "1" + }; + }).filter(t => t !== null); + + // Une tudo: Instrumentos + Basslines + // Adicionamos as basslines ao final para organização + newTracks = [...instrumentTracks, ...basslineTracks]; + + // ------------------------------------------------------------- + // 3. INICIALIZAÇÃO DE ÁUDIO E ESTADO + // ------------------------------------------------------------- + + // Inicializa nós de áudio para os instrumentos (ignora basslines pois não tocam áudio direto) newTracks.forEach((track) => { - track.volumeNode = new Tone.Volume(Tone.gainToDb(track.volume)); - track.pannerNode = new Tone.Panner(track.pan); - - track.volumeNode.connect(track.pannerNode); - track.pannerNode.connect(getMainGainNode()); - - if (isFirstTrackWithNotes) { - const activeIdx = track.activePatternIndex || 0; - const activePattern = track.patterns[activeIdx]; - if (activePattern && activePattern.steps) { - const stepsLength = activePattern.steps.length; - const requiredBars = Math.ceil(stepsLength / 16); - document.getElementById("bars-input").value = - requiredBars > 0 ? requiredBars : 1; - isFirstTrackWithNotes = false; - } + if (track.type !== 'bassline') { + track.volumeNode = new Tone.Volume(Tone.gainToDb(track.volume)); + track.pannerNode = new Tone.Panner(track.pan); + track.volumeNode.connect(track.pannerNode); + track.pannerNode.connect(getMainGainNode()); } }); + // Configura tamanho da timeline baseado no conteúdo + let isFirstTrackWithNotes = true; + newTracks.forEach(track => { + if (track.type !== 'bassline' && isFirstTrackWithNotes) { + const activePattern = track.patterns[track.activePatternIndex || 0]; + if (activePattern && activePattern.steps && activePattern.steps.length > 0) { + const bars = Math.ceil(activePattern.steps.length / 16); + const barsInput = document.getElementById("bars-input"); + if(barsInput) barsInput.value = bars > 0 ? bars : 1; + isFirstTrackWithNotes = false; + } + } + }); + + // Carrega samples/plugins try { - const trackLoadPromises = newTracks.map((track) => - loadAudioForTrack(track) - ); + const trackLoadPromises = newTracks + .filter(t => t.type !== 'bassline') // Apenas instrumentos reais + .map(track => loadAudioForTrack(track)); await Promise.all(trackLoadPromises); } catch (error) { console.error("Ocorreu um erro ao carregar os áudios do projeto:", error); } + // Atualiza estado global appState.pattern.tracks = newTracks; - appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id || null; + // Define o track ativo como o primeiro instrumento real encontrado + const firstInstrument = newTracks.find(t => t.type !== 'bassline'); + appState.pattern.activeTrackId = firstInstrument ? firstInstrument.id : null; appState.pattern.activePatternIndex = 0; loadStateFromSession(); @@ -325,6 +315,10 @@ export async function parseMmpContent(xmlString) { renderAll(); } +// -------------------------------------------------------------- +// GERAÇÃO DE ARQUIVO (EXPORT) +// -------------------------------------------------------------- + export function generateMmpFile() { if (appState.global.originalXmlDoc) { modifyAndSaveExistingMmp(); @@ -348,35 +342,22 @@ function generateXmlFromState() { if (head) { head.setAttribute("bpm", document.getElementById("bpm-input").value || 140); - head.setAttribute( - "num_bars", - document.getElementById("bars-input").value || 1 - ); - head.setAttribute( - "timesig_numerator", - document.getElementById("compasso-a-input").value || 4 - ); - head.setAttribute( - "timesig_denominator", - document.getElementById("compasso-b-input").value || 4 - ); + head.setAttribute("num_bars", document.getElementById("bars-input").value || 1); + head.setAttribute("timesig_numerator", document.getElementById("compasso-a-input").value || 4); + head.setAttribute("timesig_denominator", document.getElementById("compasso-b-input").value || 4); } - const bbTrackContainer = xmlDoc.querySelector( - 'track[type="1"] > bbtrack > trackcontainer' - ); + const bbTrackContainer = xmlDoc.querySelector('track[type="1"] > bbtrack > trackcontainer'); if (bbTrackContainer) { - bbTrackContainer - .querySelectorAll('track[type="0"]') - .forEach((node) => node.remove()); + bbTrackContainer.querySelectorAll('track[type="0"]').forEach((node) => node.remove()); + + // Apenas instrumentos reais vão para dentro do bbtrack const tracksXml = appState.pattern.tracks + .filter(t => t.type !== 'bassline') .map((track) => createTrackXml(track)) .join(""); - const tempDoc = new DOMParser().parseFromString( - `${tracksXml}`, - "application/xml" - ); + const tempDoc = new DOMParser().parseFromString(`${tracksXml}`, "application/xml"); Array.from(tempDoc.documentElement.children).forEach((newTrackNode) => { bbTrackContainer.appendChild(newTrackNode); }); @@ -394,33 +375,24 @@ export function syncPatternStateToServer() { } function createTrackXml(track) { - if (track.patterns.length === 0) return ""; + if (!track.patterns || track.patterns.length === 0) return ""; const ticksPerStep = 12; const lmmsVolume = Math.round(track.volume * 100); const lmmsPan = Math.round(track.pan * 100); - // 🔥 PROTEÇÃO: Se não tiver instrumento definido, usa Kicker padrão const instrName = track.instrumentName || "kicker"; - const instrXml = - track.instrumentXml || - ``; + const instrXml = track.instrumentXml || ``; const patternsXml = track.patterns .map((pattern) => { let patternNotesXml = ""; - if ( - track.type === "plugin" && - pattern.notes && - pattern.notes.length > 0 - ) { + if (track.type === "plugin" && pattern.notes && pattern.notes.length > 0) { patternNotesXml = pattern.notes - .map((note) => { - return ``; - }) + .map((note) => ``) .join("\n "); - } else { + } else if (pattern.steps) { patternNotesXml = pattern.steps .map((isActive, index) => { if (isActive) { @@ -432,7 +404,7 @@ function createTrackXml(track) { .join("\n "); } - return ` + return ` ${patternNotesXml} `; }) @@ -460,7 +432,9 @@ function generateNewMmp() { 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.pattern.tracks + .filter(t => t.type !== 'bassline') .map((track) => createTrackXml(track)) .join(""); @@ -504,4 +478,4 @@ function downloadFile(content, fileName) { URL.revokeObjectURL(url); } -export { generateXmlFromState as generateXmlFromStateExported }; +export { generateXmlFromState as generateXmlFromStateExported }; \ No newline at end of file