tentando obter playlist real do projeto mmp
Deploy / Deploy (push) Successful in 1m57s
Details
Deploy / Deploy (push) Successful in 1m57s
Details
This commit is contained in:
parent
72cea97123
commit
57ec600307
|
|
@ -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 <bbtco>
|
||||
// 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 <bbtco>)
|
||||
// Eles são filhos diretos de <track type="1">
|
||||
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) => {
|
||||
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());
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
// 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(
|
||||
`<root>${tracksXml}</root>`,
|
||||
"application/xml"
|
||||
);
|
||||
const tempDoc = new DOMParser().parseFromString(`<root>${tracksXml}</root>`, "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 ||
|
||||
`<kicker><env amt="0" attack="0.01" hold="0.1" decay="0.1" release="0.1" sustain="0.5" sync_mode="0"/></kicker>`;
|
||||
const instrXml = track.instrumentXml || `<kicker><env amt="0" attack="0.01" hold="0.1" decay="0.1" release="0.1" sustain="0.5" sync_mode="0"/></kicker>`;
|
||||
|
||||
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 `<note vol="${note.vol}" len="${note.len}" pos="${note.pos}" pan="${note.pan}" key="${note.key}"/>`;
|
||||
})
|
||||
.map((note) => `<note vol="${note.vol}" len="${note.len}" pos="${note.pos}" pan="${note.pan}" key="${note.key}"/>`)
|
||||
.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 `<pattern type="0" pos="${pattern.pos}" muted="0" steps="${pattern.steps.length}" name="${pattern.name}">
|
||||
return `<pattern type="0" pos="${pattern.pos}" muted="0" steps="${pattern.steps ? pattern.steps.length : 16}" name="${pattern.name}">
|
||||
${patternNotesXml}
|
||||
</pattern>`;
|
||||
})
|
||||
|
|
@ -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("");
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue