// js/file.js import { appState, loadAudioForTrack } from "./state.js"; import { getTotalSteps } from "./utils.js"; import { renderApp, getSamplePathMap } from "./ui.js"; import { DEFAULT_PAN, DEFAULT_VOLUME, NOTE_LENGTH, TICKS_PER_BAR } from "./config.js"; import { initializeAudioContext, getAudioContext, getMainGainNode, } from "./audio.js"; export async function handleFileLoad(file) { let xmlContent = ""; try { if (file.name.toLowerCase().endsWith(".mmpz")) { const jszip = new JSZip(); const zip = await jszip.loadAsync(file); const projectFile = Object.keys(zip.files).find((name) => name.toLowerCase().endsWith(".mmp") ); if (!projectFile) 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(); } await parseMmpContent(xmlContent); } catch (error) { console.error("Erro ao carregar o projeto:", error); alert(`Erro ao carregar projeto: ${error.message}`); } } export async function parseMmpContent(xmlString) { initializeAudioContext(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, "application/xml"); appState.originalXmlDoc = xmlDoc; let newTracks = []; 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 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(); newTracks = Array.from(instrumentTracks).map(trackNode => { const instrumentNode = trackNode.querySelector("instrument"); const instrumentTrackNode = trackNode.querySelector("instrumenttrack"); if (!instrumentNode || !instrumentTrackNode) return null; const trackName = trackNode.getAttribute("name"); if (instrumentNode.getAttribute("name") === 'tripleoscillator') { return null; } 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 patterns = sortedBBTrackNameNodes.map((bbTrack, index) => { const patternNode = allPatternsArray[index]; const bbTrackName = bbTrack.getAttribute("name") || `Pattern ${index + 1}`; 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: trackName, samplePath: finalSamplePath, 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, }; }).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); } 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(); } export function generateMmpFile() { if (appState.originalXmlDoc) { modifyAndSaveExistingMmp(); } else { generateNewMmp(); } } 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() { 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(""); const mmpContent = ` ${tracksXml}

Feito com MMPCreator

]]>
`; downloadFile(mmpContent, "novo_projeto.mmp"); } function downloadFile(content, fileName) { const blob = new Blob([content], { type: "application/xml;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } 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(); await parseMmpContent(xmlContent); return true; } catch (error) { console.error("Erro ao carregar projeto do servidor:", error); alert(`Erro ao carregar projeto: ${error.message}`); return false; } }