patterns funcionais, alguns nomes estão ao contrário. mas o conteúdo agora está correto e fidedigno ao projeto.
Deploy / Deploy (push) Successful in 1m7s Details

This commit is contained in:
JotaChina 2025-10-03 18:02:36 -03:00
parent fb95d52534
commit e415093a74
5 changed files with 365 additions and 214 deletions

View File

@ -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();
}

View File

@ -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 `<note vol="100" len="${NOTE_LENGTH}" pos="${notePos}" pan="0" key="57"/>`;
}
return "";
}).join("\n ");
return `<pattern type="0" pos="${pattern.pos}" muted="0" steps="${pattern.steps.length}" name="${pattern.name}">
${patternNotes}
</pattern>`;
}).join('\n ');
return `
<track type="0" solo="0" muted="0" name="${track.name}">
<instrumenttrack vol="${lmmsVolume}" pitch="0" fxch="0" pitchrange="1" basenote="57" usemasterpitch="1" pan="${lmmsPan}">
<instrument name="${track.instrumentName}">
${track.instrumentXml}
</instrument>
<fxchain enabled="0" numofeffects="0"/>
</instrumenttrack>
${patternsXml}
</track>`;
}
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(
`<root>${tracksXml}</root>`,
"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() {
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
</style></head><body>
<p>Feito com MMPCreator no https://alice.ufsj.edu.br/MMPSearch/creator</p>
<p>Feito com MMPCreator</p>
</body></html>]]></projectnotes>
</song>
</lmms-project>`;
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(
`<root>${tracksXml}</root>`,
"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 `<note vol="100" len="${NOTE_LENGTH}" pos="${notePos}" pan="0" key="57"/>`;
}
return "";
}).join("\n ");
patternsXml += `<pattern type="0" pos="${i * TICKS_PER_BAR}" muted="0" steps="${stepsPerBar}" name="Pattern ${i+1}">
${patternNotes}
</pattern>`
}
return `
<track type="0" solo="0" muted="0" name="${track.name}">
<instrumenttrack vol="${lmmsVolume}" pitch="0" fxch="0" pitchrange="1" basenote="57" usemasterpitch="1" pan="${lmmsPan}">
<instrument name="audiofileprocessor">
<audiofileprocessor eframe="1" stutter="0" looped="0" interp="1" reversed="0" lframe="0" src="${sampleSrc}" amp="100" sframe="0"/>
</instrument>
<fxchain enabled="0" numofeffects="0"/>
</instrumenttrack>
${patternsXml}
</track>`;
}
function downloadFile(content, fileName) {
const blob = new Blob([content], { type: "application/xml;charset=utf-8" });
const url = URL.createObjectURL(blob);

View File

@ -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);
}
}
}

View File

@ -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 = `
<div class="track-info"><i class="fa-solid fa-gear"></i><div class="track-mute"></div><span class="track-name">${trackData.name}</span></div>
<div class="track-controls">
<div class="knob-container">
<div class="knob" data-control="volume" data-track-id="${trackData.id}">
<div class="knob-indicator"></div>
</div>
<span>VOL</span>
</div>
<div class="knob-container">
<div class="knob" data-control="pan" data-track-id="${trackData.id}">
<div class="knob-indicator"></div>
</div>
<span>PAN</span>
</div>
</div>
<div class="step-sequencer-wrapper"></div>
`;
if (trackData.id === appState.activeTrackId) {
trackLane.classList.add('active-track');
}
trackLane.addEventListener("dragover", (e) => {
e.preventDefault();
trackLane.classList.add("drag-over");
trackLane.innerHTML = `
<div class="track-info">
<i class="fa-solid fa-gear"></i>
<div class="track-mute"></div>
<span class="track-name">${trackData.name}</span>
</div>
<div class="track-controls">
<div class="knob-container">
<div class="knob" data-control="volume" data-track-id="${trackData.id}">
<div class="knob-indicator"></div>
</div>
<span>VOL</span>
</div>
<div class="knob-container">
<div class="knob" data-control="pan" data-track-id="${trackData.id}">
<div class="knob-indicator"></div>
</div>
<span>PAN</span>
</div>
</div>
<div class="step-sequencer-wrapper"></div>
`;
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}`;

View File

@ -181,10 +181,10 @@
<i class="fa-solid fa-stop" id="stop-btn" title="Stop"></i>
</div>
<div class="pattern-manager">
<select
id="pattern-selector"
class="pattern-selector-dropdown"
></select>
<h2 id="beat-bassline-title"></h2>
<select id="global-pattern-selector" class="pattern-selector" disabled>
<option>Selecione uma faixa</option>
</select>
<button id="add-pattern-btn" class="pattern-btn">+</button>
<button id="remove-pattern-btn" class="pattern-btn">-</button>
</div>