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
Deploy / Deploy (push) Successful in 1m7s
Details
This commit is contained in:
parent
fb95d52534
commit
e415093a74
|
@ -22,7 +22,6 @@ export function initializeAudioContext() {
|
||||||
mainGainNode = audioContext.createGain();
|
mainGainNode = audioContext.createGain();
|
||||||
masterPannerNode = audioContext.createStereoPanner();
|
masterPannerNode = audioContext.createStereoPanner();
|
||||||
|
|
||||||
// Roteamento: Gain Master -> Panner Master -> Saída
|
|
||||||
mainGainNode.connect(masterPannerNode);
|
mainGainNode.connect(masterPannerNode);
|
||||||
masterPannerNode.connect(audioContext.destination);
|
masterPannerNode.connect(audioContext.destination);
|
||||||
}
|
}
|
||||||
|
@ -75,17 +74,13 @@ export function playSample(filePath, trackId) {
|
||||||
|
|
||||||
const track = trackId ? appState.tracks.find((t) => t.id == trackId) : null;
|
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);
|
const audio = new Audio(filePath);
|
||||||
audio.play();
|
audio.play();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!track.audioBuffer) {
|
|
||||||
console.warn(`Buffer para a trilha ${track.name} ainda não carregado.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = audioContext.createBufferSource();
|
const source = audioContext.createBufferSource();
|
||||||
source.buffer = track.audioBuffer;
|
source.buffer = track.audioBuffer;
|
||||||
|
|
||||||
|
@ -124,11 +119,20 @@ function tick() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- INÍCIO DA CORREÇÃO ---
|
||||||
appState.tracks.forEach((track) => {
|
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);
|
playSample(track.samplePath, track.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// --- FIM DA CORREÇÃO ---
|
||||||
|
|
||||||
highlightStep(appState.currentStep, true);
|
highlightStep(appState.currentStep, true);
|
||||||
appState.currentStep = (appState.currentStep + 1) % totalSteps;
|
appState.currentStep = (appState.currentStep + 1) % totalSteps;
|
||||||
|
@ -151,31 +155,43 @@ export function startPlayback() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopPlayback() {
|
export function stopPlayback() {
|
||||||
clearInterval(appState.playbackIntervalId);
|
if(appState.playbackIntervalId) {
|
||||||
|
clearInterval(appState.playbackIntervalId);
|
||||||
|
}
|
||||||
appState.playbackIntervalId = null;
|
appState.playbackIntervalId = null;
|
||||||
appState.isPlaying = false;
|
appState.isPlaying = false;
|
||||||
highlightStep(appState.currentStep - 1, false);
|
highlightStep(appState.currentStep - 1, false);
|
||||||
|
highlightStep(appState.currentStep, false); // Garante que o último step "playing" seja limpo
|
||||||
appState.currentStep = 0;
|
appState.currentStep = 0;
|
||||||
|
|
||||||
if (timerDisplay) timerDisplay.textContent = '00:00:00';
|
if (timerDisplay) timerDisplay.textContent = '00:00:00';
|
||||||
|
|
||||||
document.getElementById("play-btn").classList.remove("fa-pause");
|
const playBtn = document.getElementById("play-btn");
|
||||||
document.getElementById("play-btn").classList.add("fa-play");
|
if (playBtn) {
|
||||||
|
playBtn.classList.remove("fa-pause");
|
||||||
|
playBtn.classList.add("fa-play");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rewindPlayback() {
|
export function rewindPlayback() {
|
||||||
|
const previousStep = appState.currentStep;
|
||||||
appState.currentStep = 0;
|
appState.currentStep = 0;
|
||||||
if (!appState.isPlaying) {
|
if (!appState.isPlaying) {
|
||||||
if (timerDisplay) timerDisplay.textContent = '00:00:00';
|
if (timerDisplay) timerDisplay.textContent = '00:00:00';
|
||||||
document
|
highlightStep(previousStep - 1, false);
|
||||||
.querySelectorAll(".step.playing")
|
highlightStep(previousStep, false);
|
||||||
.forEach((s) => s.classList.remove("playing"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function togglePlayback() {
|
export function togglePlayback() {
|
||||||
|
initializeAudioContext(); // Garante que o contexto de áudio foi iniciado por um gesto do usuário
|
||||||
if (appState.isPlaying) {
|
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 {
|
} else {
|
||||||
startPlayback();
|
startPlayback();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { appState, loadAudioForTrack } from "./state.js";
|
import { appState, loadAudioForTrack } from "./state.js";
|
||||||
import { getTotalSteps } from "./utils.js";
|
import { getTotalSteps } from "./utils.js";
|
||||||
import { renderApp, getSamplePathMap } from "./ui.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 {
|
import {
|
||||||
initializeAudioContext,
|
initializeAudioContext,
|
||||||
getAudioContext,
|
getAudioContext,
|
||||||
|
@ -19,9 +19,7 @@ export async function handleFileLoad(file) {
|
||||||
name.toLowerCase().endsWith(".mmp")
|
name.toLowerCase().endsWith(".mmp")
|
||||||
);
|
);
|
||||||
if (!projectFile)
|
if (!projectFile)
|
||||||
throw new Error(
|
throw new Error("Não foi possível encontrar um arquivo .mmp dentro do .mmpz");
|
||||||
"Não foi possível encontrar um arquivo .mmp dentro do .mmpz"
|
|
||||||
);
|
|
||||||
xmlContent = await zip.files[projectFile].async("string");
|
xmlContent = await zip.files[projectFile].async("string");
|
||||||
} else {
|
} else {
|
||||||
xmlContent = await file.text();
|
xmlContent = await file.text();
|
||||||
|
@ -39,77 +37,159 @@ export async function parseMmpContent(xmlString) {
|
||||||
const xmlDoc = parser.parseFromString(xmlString, "application/xml");
|
const xmlDoc = parser.parseFromString(xmlString, "application/xml");
|
||||||
|
|
||||||
appState.originalXmlDoc = xmlDoc;
|
appState.originalXmlDoc = xmlDoc;
|
||||||
const newTracks = [];
|
let newTracks = [];
|
||||||
|
|
||||||
const head = xmlDoc.querySelector("head");
|
const head = xmlDoc.querySelector("head");
|
||||||
if (head) {
|
if (head) {
|
||||||
document.getElementById("bpm-input").value = head.getAttribute("bpm") || 140;
|
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-a-input").value = head.getAttribute("timesig_numerator") || 4;
|
||||||
document.getElementById("compasso-b-input").value = head.getAttribute("timesig_denominator") || 4;
|
document.getElementById("compasso-b-input").value = head.getAttribute("timesig_denominator") || 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleTrackElements = xmlDoc.querySelectorAll(
|
const allBBTrackNodes = Array.from(xmlDoc.querySelectorAll('song > trackcontainer[type="song"] > track[type="1"]'));
|
||||||
'instrument[name="audiofileprocessor"]'
|
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();
|
const pathMap = getSamplePathMap();
|
||||||
|
|
||||||
sampleTrackElements.forEach((instrumentNode) => {
|
newTracks = Array.from(instrumentTracks).map(trackNode => {
|
||||||
const afpNode = instrumentNode.querySelector("audiofileprocessor");
|
const instrumentNode = trackNode.querySelector("instrument");
|
||||||
const instrumentTrackNode = instrumentNode.parentElement;
|
const instrumentTrackNode = trackNode.querySelector("instrumenttrack");
|
||||||
const trackNode = instrumentTrackNode.parentElement;
|
if (!instrumentNode || !instrumentTrackNode) return null;
|
||||||
if (!afpNode || !instrumentTrackNode || !trackNode) return;
|
|
||||||
|
|
||||||
const audioContext = getAudioContext();
|
|
||||||
const mainGainNode = getMainGainNode();
|
|
||||||
|
|
||||||
const totalSteps = getTotalSteps();
|
const trackName = trackNode.getAttribute("name");
|
||||||
const newSteps = new Array(totalSteps).fill(false);
|
|
||||||
|
|
||||||
const ticksPerStep = 12;
|
if (instrumentNode.getAttribute("name") === 'tripleoscillator') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
trackNode.querySelectorAll("note").forEach((noteNode) => {
|
const allPatternsNodeList = trackNode.querySelectorAll("pattern");
|
||||||
const pos = parseInt(noteNode.getAttribute("pos"), 10);
|
// A lista de CONTEÚDO dos patterns é ordenada de forma DECRESCENTE para corresponder.
|
||||||
const stepIndex = Math.round(pos / ticksPerStep);
|
const allPatternsArray = Array.from(allPatternsNodeList).sort((a, b) => {
|
||||||
if (stepIndex < totalSteps) {
|
const posA = parseInt(a.getAttribute('pos'), 10) || 0;
|
||||||
newSteps[stepIndex] = true;
|
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 patterns = sortedBBTrackNameNodes.map((bbTrack, index) => {
|
||||||
const filename = srcAttribute.split("/").pop();
|
const patternNode = allPatternsArray[index];
|
||||||
const finalSamplePath = pathMap[filename] || `src/samples/${srcAttribute}`;
|
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(),
|
id: Date.now() + Math.random(),
|
||||||
name: filename || trackNode.getAttribute("name"),
|
name: trackName,
|
||||||
samplePath: finalSamplePath,
|
samplePath: finalSamplePath,
|
||||||
audioBuffer: null,
|
patterns: patterns,
|
||||||
steps: newSteps,
|
activePatternIndex: firstPatternWithNotesIndex !== -1 ? firstPatternWithNotesIndex : 0,
|
||||||
volume: parseFloat(instrumentTrackNode.getAttribute("vol")) / 100,
|
volume: !isNaN(volFromFile) ? volFromFile / 100 : DEFAULT_VOLUME,
|
||||||
pan: parseFloat(instrumentTrackNode.getAttribute("pan")) / 100,
|
pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN,
|
||||||
gainNode: audioContext.createGain(),
|
instrumentName: instrumentNode.getAttribute("name"),
|
||||||
pannerNode: audioContext.createStereoPanner(),
|
instrumentXml: instrumentNode.innerHTML,
|
||||||
};
|
};
|
||||||
newTrack.gainNode.connect(newTrack.pannerNode);
|
}).filter(track => track !== null);
|
||||||
newTrack.pannerNode.connect(mainGainNode);
|
|
||||||
newTrack.gainNode.gain.value = newTrack.volume;
|
let isFirstTrackWithNotes = true;
|
||||||
newTrack.pannerNode.pan.value = newTrack.pan;
|
newTracks.forEach(track => {
|
||||||
newTracks.push(newTrack);
|
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 {
|
try {
|
||||||
const trackLoadPromises = newTracks.map(track => loadAudioForTrack(track));
|
const trackLoadPromises = newTracks.map(track => loadAudioForTrack(track));
|
||||||
await Promise.all(trackLoadPromises);
|
await Promise.all(trackLoadPromises);
|
||||||
console.log("Todos os áudios do projeto foram carregados.");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ocorreu um erro ao carregar os áudios do projeto:", error);
|
console.error("Ocorreu um erro ao carregar os áudios do projeto:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
appState.tracks = newTracks;
|
appState.tracks = newTracks;
|
||||||
|
appState.activeTrackId = appState.tracks[0]?.id || null;
|
||||||
renderApp();
|
renderApp();
|
||||||
console.log("Projeto carregado com sucesso!", appState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateMmpFile() {
|
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() {
|
function generateNewMmp() {
|
||||||
console.log("Gerando novo arquivo .mmp do zero...");
|
|
||||||
const bpm = document.getElementById("bpm-input").value;
|
const bpm = document.getElementById("bpm-input").value;
|
||||||
const sig_num = document.getElementById("compasso-a-input").value;
|
const sig_num = document.getElementById("compasso-a-input").value;
|
||||||
const sig_den = document.getElementById("compasso-b-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">
|
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||||
p, li { white-space: pre-wrap; }
|
p, li { white-space: pre-wrap; }
|
||||||
</style></head><body>
|
</style></head><body>
|
||||||
<p>Feito com MMPCreator no https://alice.ufsj.edu.br/MMPSearch/creator</p>
|
<p>Feito com MMPCreator</p>
|
||||||
</body></html>]]></projectnotes>
|
</body></html>]]></projectnotes>
|
||||||
</song>
|
</song>
|
||||||
</lmms-project>`;
|
</lmms-project>`;
|
||||||
downloadFile(mmpContent, "novo_projeto.mmp");
|
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) {
|
function downloadFile(content, fileName) {
|
||||||
const blob = new Blob([content], { type: "application/xml;charset=utf-8" });
|
const blob = new Blob([content], { type: "application/xml;charset=utf-8" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
|
@ -6,10 +6,12 @@ import {
|
||||||
getMainGainNode,
|
getMainGainNode,
|
||||||
} from "./audio.js";
|
} from "./audio.js";
|
||||||
import { renderApp } from "./ui.js";
|
import { renderApp } from "./ui.js";
|
||||||
|
import { getTotalSteps } from "./utils.js";
|
||||||
|
|
||||||
// O "cérebro" da aplicação
|
|
||||||
export let appState = {
|
export let appState = {
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
activeTrackId: null,
|
||||||
|
activePatternIndex: 0, // <-- VOLTOU A SER GLOBAL
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
playbackIntervalId: null,
|
playbackIntervalId: null,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
|
@ -20,19 +22,14 @@ export let appState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadAudioForTrack(track) {
|
export async function loadAudioForTrack(track) {
|
||||||
if (!track.samplePath) {
|
if (!track.samplePath) return track;
|
||||||
console.warn("Track sem samplePath, pulando o carregamento de áudio.");
|
|
||||||
return track;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const audioContext = getAudioContext();
|
const audioContext = getAudioContext();
|
||||||
if (!audioContext) initializeAudioContext();
|
if (!audioContext) initializeAudioContext();
|
||||||
|
|
||||||
const response = await fetch(track.samplePath);
|
const response = await fetch(track.samplePath);
|
||||||
if (!response.ok) throw new Error(`Erro ao buscar o sample: ${response.statusText}`);
|
if (!response.ok) throw new Error(`Erro ao buscar o sample: ${response.statusText}`);
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
track.audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
track.audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||||
console.log(`Áudio carregado para a trilha: ${track.name}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Falha ao carregar áudio para a trilha ${track.name}:`, error);
|
console.error(`Falha ao carregar áudio para a trilha ${track.name}:`, error);
|
||||||
track.audioBuffer = null;
|
track.audioBuffer = null;
|
||||||
|
@ -44,13 +41,18 @@ export function addTrackToState() {
|
||||||
initializeAudioContext();
|
initializeAudioContext();
|
||||||
const audioContext = getAudioContext();
|
const audioContext = getAudioContext();
|
||||||
const mainGainNode = getMainGainNode();
|
const mainGainNode = getMainGainNode();
|
||||||
|
const totalSteps = getTotalSteps();
|
||||||
|
const referenceTrack = appState.tracks[0];
|
||||||
|
|
||||||
const newTrack = {
|
const newTrack = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: "novo instrumento",
|
name: "novo instrumento",
|
||||||
samplePath: null,
|
samplePath: null,
|
||||||
audioBuffer: 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,
|
volume: DEFAULT_VOLUME,
|
||||||
pan: DEFAULT_PAN,
|
pan: DEFAULT_PAN,
|
||||||
gainNode: audioContext.createGain(),
|
gainNode: audioContext.createGain(),
|
||||||
|
@ -62,12 +64,20 @@ export function addTrackToState() {
|
||||||
newTrack.pannerNode.pan.value = newTrack.pan;
|
newTrack.pannerNode.pan.value = newTrack.pan;
|
||||||
|
|
||||||
appState.tracks.push(newTrack);
|
appState.tracks.push(newTrack);
|
||||||
|
if (!appState.activeTrackId) {
|
||||||
|
appState.activeTrackId = newTrack.id;
|
||||||
|
}
|
||||||
renderApp();
|
renderApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeLastTrackFromState() {
|
export function removeLastTrackFromState() {
|
||||||
appState.tracks.pop();
|
if (appState.tracks.length > 0) {
|
||||||
renderApp();
|
const removedTrack = appState.tracks.pop();
|
||||||
|
if (appState.activeTrackId === removedTrack.id) {
|
||||||
|
appState.activeTrackId = appState.tracks[0]?.id || null;
|
||||||
|
}
|
||||||
|
renderApp();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateTrackSample(trackId, samplePath) {
|
export async function updateTrackSample(trackId, samplePath) {
|
||||||
|
@ -76,41 +86,43 @@ export async function updateTrackSample(trackId, samplePath) {
|
||||||
track.samplePath = samplePath;
|
track.samplePath = samplePath;
|
||||||
track.name = samplePath.split("/").pop();
|
track.name = samplePath.split("/").pop();
|
||||||
track.audioBuffer = null;
|
track.audioBuffer = null;
|
||||||
renderApp();
|
|
||||||
await loadAudioForTrack(track);
|
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) {
|
export function toggleStepState(trackId, stepIndex) {
|
||||||
const track = appState.tracks.find((t) => t.id == trackId);
|
const track = appState.tracks.find((t) => t.id == trackId);
|
||||||
if (track) {
|
if (track && track.patterns && track.patterns.length > 0) {
|
||||||
track.steps[stepIndex] = !track.steps[stepIndex];
|
// 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) {
|
export function updateTrackVolume(trackId, volume) {
|
||||||
const track = appState.tracks.find((t) => t.id == trackId);
|
const track = appState.tracks.find((t) => t.id == trackId);
|
||||||
const audioContext = getAudioContext();
|
|
||||||
if (track) {
|
if (track) {
|
||||||
const clampedVolume = Math.max(0, Math.min(1.5, volume));
|
const clampedVolume = Math.max(0, Math.min(1.5, volume));
|
||||||
track.volume = clampedVolume;
|
track.volume = clampedVolume;
|
||||||
if (track.gainNode) {
|
if (track.gainNode) {
|
||||||
track.gainNode.gain.setValueAtTime(
|
track.gainNode.gain.setValueAtTime(clampedVolume, getAudioContext().currentTime);
|
||||||
clampedVolume,
|
|
||||||
audioContext.currentTime
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateTrackPan(trackId, pan) {
|
export function updateTrackPan(trackId, pan) {
|
||||||
const track = appState.tracks.find((t) => t.id == trackId);
|
const track = appState.tracks.find((t) => t.id == trackId);
|
||||||
const audioContext = getAudioContext();
|
|
||||||
if (track) {
|
if (track) {
|
||||||
const clampedPan = Math.max(-1, Math.min(1, pan));
|
const clampedPan = Math.max(-1, Math.min(1, pan));
|
||||||
track.pan = clampedPan;
|
track.pan = clampedPan;
|
||||||
if (track.pannerNode) {
|
if (track.pannerNode) {
|
||||||
track.pannerNode.pan.setValueAtTime(clampedPan, audioContext.currentTime);
|
track.pannerNode.pan.setValueAtTime(clampedPan, getAudioContext().currentTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,66 +10,111 @@ import { playSample } from "./audio.js";
|
||||||
import { getTotalSteps } from "./utils.js";
|
import { getTotalSteps } from "./utils.js";
|
||||||
import { loadProjectFromServer } from "./file.js";
|
import { loadProjectFromServer } from "./file.js";
|
||||||
|
|
||||||
// Variável para armazenar o mapa de samples (nome do arquivo -> caminho completo)
|
|
||||||
let samplePathMap = {};
|
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() {
|
export function getSamplePathMap() {
|
||||||
return samplePathMap;
|
return samplePathMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Função recursiva para construir o mapa de samples a partir do manifest
|
|
||||||
function buildSamplePathMap(tree, currentPath) {
|
function buildSamplePathMap(tree, currentPath) {
|
||||||
for (const key in tree) {
|
for (const key in tree) {
|
||||||
if (key === "_isFile") continue; // Ignora a propriedade de metadados
|
if (key === "_isFile") continue;
|
||||||
|
|
||||||
const node = tree[key];
|
const node = tree[key];
|
||||||
const newPath = `${currentPath}/${key}`;
|
const newPath = `${currentPath}/${key}`;
|
||||||
if (node._isFile) {
|
if (node._isFile) {
|
||||||
// Se for um arquivo, adiciona ao mapa
|
|
||||||
samplePathMap[key] = newPath;
|
samplePathMap[key] = newPath;
|
||||||
} else {
|
} else {
|
||||||
// Se for um diretório, continua a busca recursivamente
|
|
||||||
buildSamplePathMap(node, newPath);
|
buildSamplePathMap(node, newPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RENDERIZAÇÃO PRINCIPAL
|
|
||||||
export function renderApp() {
|
export function renderApp() {
|
||||||
const trackContainer = document.getElementById("track-container");
|
const trackContainer = document.getElementById("track-container");
|
||||||
trackContainer.innerHTML = "";
|
trackContainer.innerHTML = "";
|
||||||
|
|
||||||
appState.tracks.forEach((trackData) => {
|
appState.tracks.forEach((trackData) => {
|
||||||
const trackLane = document.createElement("div");
|
const trackLane = document.createElement("div");
|
||||||
trackLane.className = "track-lane";
|
trackLane.className = "track-lane";
|
||||||
trackLane.dataset.trackId = trackData.id;
|
trackLane.dataset.trackId = trackData.id;
|
||||||
|
|
||||||
trackLane.innerHTML = `
|
if (trackData.id === appState.activeTrackId) {
|
||||||
<div class="track-info"><i class="fa-solid fa-gear"></i><div class="track-mute"></div><span class="track-name">${trackData.name}</span></div>
|
trackLane.classList.add('active-track');
|
||||||
<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("dragover", (e) => {
|
trackLane.innerHTML = `
|
||||||
e.preventDefault();
|
<div class="track-info">
|
||||||
trackLane.classList.add("drag-over");
|
<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) => {
|
trackLane.addEventListener("drop", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
trackLane.classList.remove("drag-over");
|
trackLane.classList.remove("drag-over");
|
||||||
|
@ -80,25 +125,20 @@ export function renderApp() {
|
||||||
});
|
});
|
||||||
|
|
||||||
trackContainer.appendChild(trackLane);
|
trackContainer.appendChild(trackLane);
|
||||||
|
|
||||||
const volumeKnob = trackLane.querySelector(".knob[data-control='volume']");
|
const volumeKnob = trackLane.querySelector(".knob[data-control='volume']");
|
||||||
addKnobInteraction(volumeKnob);
|
addKnobInteraction(volumeKnob);
|
||||||
updateKnobVisual(volumeKnob, "volume");
|
updateKnobVisual(volumeKnob, "volume");
|
||||||
|
|
||||||
const panKnob = trackLane.querySelector(".knob[data-control='pan']");
|
const panKnob = trackLane.querySelector(".knob[data-control='pan']");
|
||||||
addKnobInteraction(panKnob);
|
addKnobInteraction(panKnob);
|
||||||
updateKnobVisual(panKnob, "pan");
|
updateKnobVisual(panKnob, "pan");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateGlobalPatternSelector();
|
||||||
redrawSequencer();
|
redrawSequencer();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function redrawSequencer() {
|
export function redrawSequencer() {
|
||||||
const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4;
|
const totalGridSteps = getTotalSteps();
|
||||||
const noteValue = parseInt(document.getElementById("compasso-b-input").value, 10) || 4;
|
|
||||||
const subdivisions = Math.round(16 / noteValue);
|
|
||||||
const stepsPerBar = beatsPerBar * subdivisions;
|
|
||||||
const totalSteps = getTotalSteps();
|
|
||||||
|
|
||||||
document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => {
|
document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => {
|
||||||
let sequencerContainer = wrapper.querySelector(".step-sequencer");
|
let sequencerContainer = wrapper.querySelector(".step-sequencer");
|
||||||
if (!sequencerContainer) {
|
if (!sequencerContainer) {
|
||||||
|
@ -111,37 +151,45 @@ export function redrawSequencer() {
|
||||||
const trackId = parentTrackElement.dataset.trackId;
|
const trackId = parentTrackElement.dataset.trackId;
|
||||||
const trackData = appState.tracks.find((t) => t.id == trackId);
|
const trackData = appState.tracks.find((t) => t.id == trackId);
|
||||||
|
|
||||||
if (trackData && trackData.steps.length !== totalSteps) {
|
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
|
||||||
const newStepsState = new Array(totalSteps).fill(false);
|
sequencerContainer.innerHTML = "";
|
||||||
for (let i = 0; i < Math.min(trackData.steps.length, totalSteps); i++) {
|
return;
|
||||||
newStepsState[i] = trackData.steps[i];
|
|
||||||
}
|
|
||||||
trackData.steps = newStepsState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 = "";
|
sequencerContainer.innerHTML = "";
|
||||||
for (let i = 0; i < totalSteps; i++) {
|
for (let i = 0; i < totalGridSteps; i++) {
|
||||||
const stepWrapper = document.createElement("div");
|
const stepWrapper = document.createElement("div");
|
||||||
stepWrapper.className = "step-wrapper";
|
stepWrapper.className = "step-wrapper";
|
||||||
|
|
||||||
const stepElement = document.createElement("div");
|
const stepElement = document.createElement("div");
|
||||||
stepElement.className = "step";
|
stepElement.className = "step";
|
||||||
if (trackData && trackData.steps[i] === true) {
|
|
||||||
|
if (patternSteps[i] === true) {
|
||||||
stepElement.classList.add("active");
|
stepElement.classList.add("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
stepElement.addEventListener("click", () => {
|
stepElement.addEventListener("click", () => {
|
||||||
toggleStepState(trackData.id, i);
|
toggleStepState(trackData.id, i);
|
||||||
stepElement.classList.toggle("active");
|
stepElement.classList.toggle("active");
|
||||||
if (trackData && trackData.samplePath) {
|
if (trackData && trackData.samplePath) {
|
||||||
playSample(trackData.samplePath, trackData.id);
|
playSample(trackData.samplePath, trackData.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4;
|
||||||
const groupIndex = Math.floor(i / beatsPerBar);
|
const groupIndex = Math.floor(i / beatsPerBar);
|
||||||
if (groupIndex % 2 === 0) {
|
if (groupIndex % 2 === 0) {
|
||||||
stepElement.classList.add("step-dark");
|
stepElement.classList.add("step-dark");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stepsPerBar = 16;
|
||||||
if (i > 0 && i % stepsPerBar === 0) {
|
if (i > 0 && i % stepsPerBar === 0) {
|
||||||
const marker = document.createElement("div");
|
const marker = document.createElement("div");
|
||||||
marker.className = "step-marker";
|
marker.className = "step-marker";
|
||||||
|
@ -275,7 +323,6 @@ export async function loadAndRenderSampleBrowser() {
|
||||||
|
|
||||||
samplePathMap = {};
|
samplePathMap = {};
|
||||||
buildSamplePathMap(fileTree, "src/samples");
|
buildSamplePathMap(fileTree, "src/samples");
|
||||||
console.log("Mapa de samples construído:", samplePathMap);
|
|
||||||
|
|
||||||
renderFileTree(fileTree, browserContent, "src/samples");
|
renderFileTree(fileTree, browserContent, "src/samples");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -294,6 +341,7 @@ function renderFileTree(tree, parentElement, currentPath) {
|
||||||
return aIsFile ? 1 : -1;
|
return aIsFile ? 1 : -1;
|
||||||
});
|
});
|
||||||
for (const key of sortedKeys) {
|
for (const key of sortedKeys) {
|
||||||
|
if (key === '_isFile') continue;
|
||||||
const node = tree[key];
|
const node = tree[key];
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
const newPath = `${currentPath}/${key}`;
|
const newPath = `${currentPath}/${key}`;
|
||||||
|
|
|
@ -181,10 +181,10 @@
|
||||||
<i class="fa-solid fa-stop" id="stop-btn" title="Stop"></i>
|
<i class="fa-solid fa-stop" id="stop-btn" title="Stop"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="pattern-manager">
|
<div class="pattern-manager">
|
||||||
<select
|
<h2 id="beat-bassline-title"></h2>
|
||||||
id="pattern-selector"
|
<select id="global-pattern-selector" class="pattern-selector" disabled>
|
||||||
class="pattern-selector-dropdown"
|
<option>Selecione uma faixa</option>
|
||||||
></select>
|
</select>
|
||||||
<button id="add-pattern-btn" class="pattern-btn">+</button>
|
<button id="add-pattern-btn" class="pattern-btn">+</button>
|
||||||
<button id="remove-pattern-btn" class="pattern-btn">-</button>
|
<button id="remove-pattern-btn" class="pattern-btn">-</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue