diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index d0a4d3d3..75409de1 100755 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -1,3 +1,5 @@ +// js/creations/file.js + //-------------------------------------------------------------- // IMPORTS NECESSÁRIOS //-------------------------------------------------------------- @@ -35,7 +37,6 @@ export function handleLocalProjectReset() { 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; @@ -87,7 +88,99 @@ export async function loadProjectFromServer(fileName) { } // ================================================================= -// 🔥 FUNÇÃO DE PARSING PRINCIPAL (CORRIGIDA) +// FUNÇÃO AUXILIAR: PARSE DE INSTRUMENTO ÚNICO +// ================================================================= +function parseInstrumentNode(trackNode, sortedBBTrackNameNodes, pathMap) { + const instrumentNode = trackNode.querySelector("instrument"); + const instrumentTrackNode = trackNode.querySelector("instrumenttrack"); + + // Se não tiver instrumento ou track válida, retorna null + if (!instrumentNode || !instrumentTrackNode) return null; + + const trackName = trackNode.getAttribute("name"); + const instrumentName = instrumentNode.getAttribute("name"); + + // Lógica de Patterns + const allPatternsNodeList = trackNode.querySelectorAll("pattern"); + const allPatternsArray = Array.from(allPatternsNodeList).sort((a, b) => { + return (parseInt(a.getAttribute("pos"), 10) || 0) - (parseInt(b.getAttribute("pos"), 10) || 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}`; + + if (!patternNode) { + return { name: bbTrackName, steps: new Array(16).fill(false), notes: [], pos: 0 }; + } + + 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) => { + notes.push({ + 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(parseInt(noteNode.getAttribute("pos"), 10) / ticksPerStep); + if (stepIndex < patternSteps) steps[stepIndex] = true; + }); + + return { + name: bbTrackName, + steps: steps, + notes: notes, + pos: parseInt(patternNode.getAttribute("pos"), 10) || 0, + }; + }); + + // Lógica de Sample vs Plugin + let finalSamplePath = null; + let trackType = "plugin"; + + if (instrumentName === "audiofileprocessor") { + 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.startsWith("samples/") ? sampleSrc.substring("samples/".length) : sampleSrc; + finalSamplePath = `src/samples/${cleanSrc}`; + } + } + } + + const volFromFile = parseFloat(instrumentTrackNode.getAttribute("vol")); + const panFromFile = parseFloat(instrumentTrackNode.getAttribute("pan")); + + return { + id: Date.now() + Math.random(), + name: trackName, + type: trackType, + samplePath: finalSamplePath, + patterns: patterns, + activePatternIndex: 0, + volume: !isNaN(volFromFile) ? volFromFile / 100 : DEFAULT_VOLUME, + pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN, + instrumentName: instrumentName, + instrumentXml: instrumentNode.innerHTML, + }; +} + +// ================================================================= +// 🔥 FUNÇÃO DE PARSING PRINCIPAL // ================================================================= export async function parseMmpContent(xmlString) { resetProjectState(); @@ -105,7 +198,6 @@ export async function parseMmpContent(xmlString) { appState.global.originalXmlDoc = xmlDoc; let newTracks = []; - // Configuração Global (BPM, Compasso) const head = xmlDoc.querySelector("head"); if (head) { const setVal = (id, attr, def) => { @@ -120,167 +212,97 @@ export async function parseMmpContent(xmlString) { const pathMap = getSamplePathMap(); // ------------------------------------------------------------- - // 1. EXTRAÇÃO DE INSTRUMENTOS (Normal) + // 1. PREPARAÇÃO // ------------------------------------------------------------- - // 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; - - 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) => { - return (parseInt(a.getAttribute("pos"), 10) || 0) - (parseInt(b.getAttribute("pos"), 10) || 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}`; - - if (!patternNode) { - return { name: bbTrackName, steps: new Array(16).fill(false), notes: [], pos: 0 }; - } - - 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) => { - notes.push({ - 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(parseInt(noteNode.getAttribute("pos"), 10) / ticksPerStep); - if (stepIndex < patternSteps) steps[stepIndex] = true; - }); - - return { - name: bbTrackName, - steps: steps, - notes: notes, - pos: parseInt(patternNode.getAttribute("pos"), 10) || 0, - }; - }); - - // Lógica de Sample vs Plugin - let finalSamplePath = null; - let trackType = "plugin"; - - if (instrumentName === "audiofileprocessor") { - 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.startsWith("samples/") ? sampleSrc.substring("samples/".length) : sampleSrc; - finalSamplePath = `src/samples/${cleanSrc}`; - } - } - } - - const volFromFile = parseFloat(instrumentTrackNode.getAttribute("vol")); - const panFromFile = parseFloat(instrumentTrackNode.getAttribute("pan")); - - return { - id: Date.now() + Math.random(), - name: trackName, - type: trackType, - samplePath: finalSamplePath, - patterns: patterns, - activePatternIndex: 0, - volume: !isNaN(volFromFile) ? volFromFile / 100 : DEFAULT_VOLUME, - pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN, - instrumentName: instrumentName, - instrumentXml: instrumentNode.innerHTML, - }; - }).filter(t => t !== null); + // ------------------------------------------------------------- + // 2. EXTRAÇÃO DE INSTRUMENTOS (Song Editor) + // ------------------------------------------------------------- + // Pega apenas os instrumentos que estão soltos no Song Editor (não dentro de BBTracks) + const songInstrumentNodes = Array.from(xmlDoc.querySelectorAll('song > trackcontainer > track[type="0"]')); + + const songTracks = songInstrumentNodes + .map(node => parseInstrumentNode(node, sortedBBTrackNameNodes, pathMap)) + .filter(t => t !== null); // ------------------------------------------------------------- - // 2. EXTRAÇÃO DAS TRILHAS DE BASSLINE (Playlist Container) + // 3. EXTRAÇÃO DAS TRILHAS DE BASSLINE (E SEUS INSTRUMENTOS) // ------------------------------------------------------------- - // 👇 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 + // A. Extrai os clipes da timeline (blocos azuis) 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") + name: trackName }; }); - // 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; + // B. Extrai os instrumentos INTERNOS desta Bassline + // Eles estão dentro de + const internalInstrumentNodes = Array.from(trackNode.querySelectorAll('bbtrack > trackcontainer > track[type="0"]')); + + const internalInstruments = internalInstrumentNodes + .map(node => parseInstrumentNode(node, sortedBBTrackNameNodes, pathMap)) + .filter(t => t !== 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 + type: "bassline", playlist_clips: playlistClips, - // Propriedades dummy para não quebrar o pattern_state/ui + instruments: internalInstruments, // <--- AQUI ESTÁ A CORREÇÃO CRUCIAL! + // Fallbacks para evitar crashes volume: 1, pan: 0, patterns: [], isMuted: trackNode.getAttribute("muted") === "1" }; - }).filter(t => t !== null); + }).filter(t => t !== null && (t.playlist_clips.length > 0 || t.instruments.length > 0)); - // Une tudo: Instrumentos + Basslines - // Adicionamos as basslines ao final para organização - newTracks = [...instrumentTracks, ...basslineTracks]; + // Une tudo: Instrumentos Soltos + Basslines + newTracks = [...songTracks, ...basslineTracks]; // ------------------------------------------------------------- - // 3. INICIALIZAÇÃO DE ÁUDIO E ESTADO + // 4. INICIALIZAÇÃO DE ÁUDIO E ESTADO // ------------------------------------------------------------- - // Inicializa nós de áudio para os instrumentos (ignora basslines pois não tocam áudio direto) + // Inicializa nós de áudio newTracks.forEach((track) => { + // Se for instrumento normal 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()); + } + // Se for Bassline, inicializa os nós dos instrumentos INTERNOS + else if (track.instruments && track.instruments.length > 0) { + track.instruments.forEach(inst => { + inst.volumeNode = new Tone.Volume(Tone.gainToDb(inst.volume)); + inst.pannerNode = new Tone.Panner(inst.pan); + inst.volumeNode.connect(inst.pannerNode); + inst.pannerNode.connect(getMainGainNode()); + }); } }); - // Configura tamanho da timeline baseado no conteúdo + // Configura tamanho da timeline let isFirstTrackWithNotes = true; newTracks.forEach(track => { + // Verifica track normal if (track.type !== 'bassline' && isFirstTrackWithNotes) { const activePattern = track.patterns[track.activePatternIndex || 0]; if (activePattern && activePattern.steps && activePattern.steps.length > 0) { @@ -289,24 +311,56 @@ export async function parseMmpContent(xmlString) { if(barsInput) barsInput.value = bars > 0 ? bars : 1; isFirstTrackWithNotes = false; } + } + // Verifica dentro da bassline + else if (track.type === 'bassline' && isFirstTrackWithNotes && track.instruments.length > 0) { + const firstInst = track.instruments[0]; + const activePattern = firstInst.patterns[firstInst.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 + // Carrega samples/plugins (Async) try { - const trackLoadPromises = newTracks - .filter(t => t.type !== 'bassline') // Apenas instrumentos reais - .map(track => loadAudioForTrack(track)); - await Promise.all(trackLoadPromises); + const promises = []; + + newTracks.forEach(track => { + if (track.type !== 'bassline') { + promises.push(loadAudioForTrack(track)); + } else { + // Carrega áudio dos instrumentos internos da bassline + track.instruments.forEach(inst => { + promises.push(loadAudioForTrack(inst)); + }); + } + }); + + await Promise.all(promises); } catch (error) { console.error("Ocorreu um erro ao carregar os áudios do projeto:", error); } // Atualiza estado global appState.pattern.tracks = newTracks; - // 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; + + // Define faixa ativa (tenta pegar a primeira normal ou a primeira de dentro da bassline) + let firstInstrumentId = null; + const firstInstTrack = newTracks.find(t => t.type !== 'bassline'); + if (firstInstTrack) { + firstInstrumentId = firstInstTrack.id; + } else { + const firstBassline = newTracks.find(t => t.type === 'bassline' && t.instruments.length > 0); + if (firstBassline) { + firstInstrumentId = firstBassline.instruments[0].id; + } + } + + appState.pattern.activeTrackId = firstInstrumentId; appState.pattern.activePatternIndex = 0; loadStateFromSession(); @@ -351,9 +405,14 @@ function generateXmlFromState() { if (bbTrackContainer) { bbTrackContainer.querySelectorAll('track[type="0"]').forEach((node) => node.remove()); - // Apenas instrumentos reais vão para dentro do bbtrack + // Procura por instrumentos que estão dentro de objetos bassline no state + // (Esta lógica de exportação é básica e pode precisar de ajustes se você editar muito os instrumentos internos) const tracksXml = appState.pattern.tracks - .filter(t => t.type !== 'bassline') + .flatMap(track => { + if (track.type === 'bassline') return track.instruments; + return [track]; + }) + .filter(t => t && t.type !== 'bassline') .map((track) => createTrackXml(track)) .join(""); @@ -434,7 +493,11 @@ function generateNewMmp() { const num_bars = document.getElementById("bars-input").value; const tracksXml = appState.pattern.tracks - .filter(t => t.type !== 'bassline') + .flatMap(track => { + if (track.type === 'bassline') return track.instruments; + return [track]; + }) + .filter(t => t && t.type !== 'bassline') .map((track) => createTrackXml(track)) .join("");