Arrumando botões de volume e pan, criação de novos compassos.
Deploy / Deploy (push) Successful in 40s Details

This commit is contained in:
JotaChina 2025-08-29 18:27:39 -03:00
parent 3524b5cff6
commit cd3b236acf
8 changed files with 411 additions and 291 deletions

View File

@ -141,7 +141,6 @@ body.sidebar-hidden #sidebar-toggle {
padding: 8px 15px; padding: 8px 15px;
position: fixed; position: fixed;
top: 0; top: 0;
/* Removido width 100% para se adaptar ao padding do body */
left: 300px; left: 300px;
right: 0; right: 0;
z-index: 1000; z-index: 1000;
@ -269,6 +268,7 @@ body.sidebar-hidden .global-toolbar {
margin: 0 10px; margin: 0 10px;
padding-left: 10px; padding-left: 10px;
border-left: 1px solid var(--bg-toolbar); border-left: 1px solid var(--bg-toolbar);
flex-shrink: 0;
} }
.knob-container { .knob-container {
@ -305,32 +305,34 @@ body.sidebar-hidden .global-toolbar {
border-radius: 1px; border-radius: 1px;
} }
.step-sequencer { .step-sequencer-wrapper {
display: flex;
flex-grow: 1; flex-grow: 1;
gap: 4px;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden;
padding-bottom: 8px; padding-bottom: 8px;
} }
.step-sequencer::-webkit-scrollbar { .step-sequencer {
height: 8px; display: flex;
gap: 4px;
} }
.step-sequencer::-webkit-scrollbar-track { .step-sequencer-wrapper::-webkit-scrollbar {
height: 8px;
}
.step-sequencer-wrapper::-webkit-scrollbar-track {
background: var(--border-color); background: var(--border-color);
border-radius: 4px; border-radius: 4px;
} }
.step-sequencer-wrapper::-webkit-scrollbar-thumb {
.step-sequencer::-webkit-scrollbar-thumb {
background: var(--bg-toolbar); background: var(--bg-toolbar);
border-radius: 4px; border-radius: 4px;
} }
.step-sequencer-wrapper::-webkit-scrollbar-thumb:hover {
.step-sequencer::-webkit-scrollbar-thumb:hover {
background: #555; background: #555;
} }
.step-wrapper { .step-wrapper {
position: relative; position: relative;
} }
@ -341,17 +343,18 @@ body.sidebar-hidden .global-toolbar {
left: 1px; left: 1px;
font-size: .6rem; font-size: .6rem;
color: var(--text-dark); color: var(--text-dark);
user-select: none;
} }
.step { .step {
width: 28px; width: 28px;
aspect-ratio: 1 / 1; height: 28px;
background-color: #2a2a2a; background-color: #2a2a2a;
border: 1px solid #4a4a4a; border: 1px solid #4a4a4a;
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
transition: background-color .1s, transform 0.1s; transition: background-color .1s, transform 0.1s;
flex-shrink: 0; /* Impede que os steps encolham */ flex-shrink: 0;
} }
.step-dark { .step-dark {
@ -374,6 +377,7 @@ body.sidebar-hidden .global-toolbar {
box-shadow: inset 0 0 8px rgba(255, 255, 255, 0.8); box-shadow: inset 0 0 8px rgba(255, 255, 255, 0.8);
} }
/* =============================================== */ /* =============================================== */
/* CONTROLES E INPUTS /* CONTROLES E INPUTS
/* =============================================== */ /* =============================================== */
@ -518,7 +522,7 @@ body.sidebar-hidden .global-toolbar {
} }
/* =============================================== */ /* =============================================== */
/* MODAL (CAIXA DE DIÁLOGO) /* MODAL (CAIXA DE DIÁLOGO) - (REVISADO)
/* =============================================== */ /* =============================================== */
.modal-overlay { .modal-overlay {
position: fixed; position: fixed;
@ -531,6 +535,7 @@ body.sidebar-hidden .global-toolbar {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 1rem; /* Adiciona um respiro nas laterais */
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
transition: visibility 0s 0.3s, opacity 0.3s; transition: visibility 0s 0.3s, opacity 0.3s;
@ -551,6 +556,14 @@ body.sidebar-hidden .global-toolbar {
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;
position: relative; position: relative;
/* (NOVO) Usando Flexbox para organizar o conteúdo interno */
display: flex;
flex-direction: column;
gap: 1.5rem; /* Espaçamento consistente entre as seções */
/* (NOVO) Controle de altura para telas pequenas ou listas grandes */
max-height: 90vh;
} }
.modal-close { .modal-close {
@ -569,21 +582,53 @@ body.sidebar-hidden .global-toolbar {
} }
.modal-title { .modal-title {
margin-top: 0; margin: 0; /* Removido margin para usar o 'gap' do flexbox */
margin-bottom: 1.5rem; padding-bottom: 0.5rem;
border-bottom: 1px solid var(--bg-toolbar);
color: var(--text-light); color: var(--text-light);
text-align: center; text-align: center;
flex-shrink: 0; /* Impede que o título encolha */
} }
.modal-section { .modal-section {
margin-bottom: 1.5rem; margin: 0; /* Removido margin para usar o 'gap' do flexbox */
} }
.modal-section h3 { .modal-section h3 {
margin-top: 0; margin-top: 0;
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
border-bottom: 1px solid var(--bg-toolbar); font-size: 1rem;
padding-bottom: 0.5rem; color: var(--text-light);
}
/* (NOVO) Estilos para a lista de projetos do servidor */
#server-projects-list {
max-height: 250px; /* Altura máxima para a lista */
overflow-y: auto; /* Barra de rolagem SÓ para a lista */
background-color: var(--bg-toolbar);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.5rem;
min-height: 50px; /* Altura mínima para não colapsar se estiver vazio */
}
/* (REVISADO) Estilos para cada item na lista */
#server-projects-list .project-item {
background-color: var(--bg-editor);
padding: 10px 15px;
border-radius: 4px;
margin-bottom: 8px;
cursor: pointer;
transition: background-color 0.2s, color 0.2s;
border: 1px solid transparent;
}
#server-projects-list .project-item:last-child {
margin-bottom: 0;
}
#server-projects-list .project-item:hover {
background-color: var(--bg-body);
color: #fff;
border-color: var(--accent-green);
} }
.modal-button { .modal-button {
@ -596,6 +641,7 @@ body.sidebar-hidden .global-toolbar {
font-size: 1rem; font-size: 1rem;
transition: background-color 0.2s, border-color 0.2s; transition: background-color 0.2s, border-color 0.2s;
width: 100%; width: 100%;
text-align: center;
} }
.modal-button:hover { .modal-button:hover {
@ -603,101 +649,68 @@ body.sidebar-hidden .global-toolbar {
border-color: #333; border-color: #333;
} }
#server-projects-list .project-item {
background-color: var(--bg-editor);
padding: 10px 15px;
border-radius: 4px;
margin-bottom: 8px;
cursor: pointer;
transition: background-color 0.2s;
}
#server-projects-list .project-item:hover {
background-color: var(--bg-body);
color: #fff;
}
/* =============================================== */ /* =============================================== */
/* ESTILOS RESPONSIVOS /* ESTILOS RESPONSIVOS
/* =============================================== */ /* =============================================== */
/* Para telas menores como laptops pequenos e tablets grandes */
@media (max-width: 992px) { @media (max-width: 992px) {
.main-content { .main-content {
padding: 1.5rem; /* Reduz o padding */ padding: 1.5rem;
} }
.beat-editor { .beat-editor {
max-width: 100%; /* Permite que o editor use mais espaço */ max-width: 100%;
} }
} }
/* Para tablets e celulares */
@media (max-width: 768px) { @media (max-width: 768px) {
body { body {
padding-left: 0 !important; /* Remove o padding fixo, !important para garantir */ padding-left: 0 !important;
} }
.main-content { .main-content {
padding: 1rem; padding: 1rem;
} }
/* A sidebar agora se sobrepõe ao conteúdo e fica escondida por padrão */
.sample-browser { .sample-browser {
transform: translateX(-100%); transform: translateX(-100%);
width: 280px; /* Pode diminuir um pouco a largura */ width: 280px;
} }
/* Quando a sidebar for aberta, ela volta a posição original */
body:not(.sidebar-hidden) .sample-browser { body:not(.sidebar-hidden) .sample-browser {
transform: translateX(0); transform: translateX(0);
} }
#sidebar-toggle { #sidebar-toggle {
left: 5px; /* Posição fixa do botão */ left: 5px;
} }
/* A toolbar global agora ocupa 100% da largura */
.global-toolbar { .global-toolbar {
left: 0; left: 0;
padding-left: 45px; /* Espaço para o botão do menu */ padding-left: 45px;
} }
/* Quebra a linha dos itens da toolbar se não couberem */
.editor-toolbar, .editor-toolbar,
.control-group { .control-group {
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; gap: 10px;
} }
/* Reorganiza a track para um layout vertical */
.track-lane { .track-lane {
flex-direction: column; flex-direction: column;
align-items: stretch; /* Itens ocupam 100% da largura */ align-items: stretch;
gap: 15px; gap: 15px;
padding: 15px; padding: 15px;
} }
.track-info, .track-info,
.track-controls { .track-controls {
width: 100%; width: 100%;
} }
.track-controls { .track-controls {
border-left: none; border-left: none;
padding-left: 0; padding-left: 0;
justify-content: space-around; /* Distribui melhor os knobs */ justify-content: space-around;
} }
.step-sequencer-wrapper {
.step-sequencer {
width: 100%; width: 100%;
} }
/* (REVISADO) Ajuste do modal para telas pequenas */
/* Ajusta o modal para telas pequenas */
.modal-content { .modal-content {
max-width: 90vw; max-width: 95vw; /* Usa quase toda a largura da tela */
padding: 1.5rem 1rem; padding: 1rem 1.5rem;
gap: 1rem;
} }
} }

View File

@ -42,44 +42,41 @@ export function playMetronomeSound(isDownbeat) {
oscillator.stop(audioContext.currentTime + 0.05); oscillator.stop(audioContext.currentTime + 0.05);
} }
// (ALTERADO) Função reescrita para usar AudioBuffers pré-carregados // Função otimizada para usar AudioBuffers
export function playSample(filePath, trackId) { export function playSample(filePath, trackId) {
initializeAudioContext(); initializeAudioContext();
if (!filePath) return; if (!filePath) return;
const track = trackId ? appState.tracks.find((t) => t.id == trackId) : null; const track = trackId ? appState.tracks.find((t) => t.id == trackId) : null;
// Se a função for chamada sem um ID de track (ex: clique no browser de samples), // Se for uma prévia (sem trilha), usa o método antigo e rápido
// usa o método antigo como fallback para uma prévia rápida.
if (!track) { if (!track) {
const audio = new Audio(filePath); const audio = new Audio(filePath);
audio.play(); audio.play();
return; return;
} }
// Se a track não tiver o buffer de áudio pronto, avisa no console e não toca. // Se a trilha não tiver o buffer carregado, não toca
if (!track.audioBuffer) { if (!track.audioBuffer) {
console.warn(`Buffer para a track ${track.name} ainda não carregado.`); console.warn(`Buffer para a trilha ${track.name} ainda não carregado.`);
return; return;
} }
// 1. Cria uma fonte de áudio leve (BufferSource) // Cria uma fonte de áudio leve a partir do buffer
const source = audioContext.createBufferSource(); const source = audioContext.createBufferSource();
// 2. Conecta o buffer de áudio já decodificado
source.buffer = track.audioBuffer; source.buffer = track.audioBuffer;
// 3. Conecta na cadeia de áudio da track (respeitando volume e pan) // Conecta na cadeia de áudio da trilha (respeitando volume e pan)
if (track.gainNode) { if (track.gainNode) {
source.connect(track.gainNode); source.connect(track.gainNode);
} else { } else {
source.connect(mainGainNode); // Fallback source.connect(mainGainNode); // Fallback
} }
// 4. Toca o som imediatamente // Toca o som imediatamente
source.start(0); source.start(0);
} }
function tick() { function tick() {
const totalSteps = getTotalSteps(); const totalSteps = getTotalSteps();
if (totalSteps === 0) { if (totalSteps === 0) {

View File

@ -1,7 +1,7 @@
// js/file.js // js/file.js
import { appState } from "./state.js"; import { appState, loadAudioForTrack } from "./state.js";
import { getTotalSteps } from "./utils.js"; import { getTotalSteps } from "./utils.js";
import { renderApp } from "./ui.js"; import { renderApp, getSamplePathMap } from "./ui.js";
import { NOTE_LENGTH, TICKS_PER_BAR } from "./config.js"; import { NOTE_LENGTH, TICKS_PER_BAR } from "./config.js";
import { import {
initializeAudioContext, initializeAudioContext,
@ -26,14 +26,14 @@ export async function handleFileLoad(file) {
} else { } else {
xmlContent = await file.text(); xmlContent = await file.text();
} }
parseMmpContent(xmlContent); await parseMmpContent(xmlContent);
} catch (error) { } catch (error) {
console.error("Erro ao carregar o projeto:", error); console.error("Erro ao carregar o projeto:", error);
alert(`Erro ao carregar projeto: ${error.message}`); alert(`Erro ao carregar projeto: ${error.message}`);
} }
} }
export function parseMmpContent(xmlString) { export async function parseMmpContent(xmlString) {
initializeAudioContext(); initializeAudioContext();
const parser = new DOMParser(); const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "application/xml"); const xmlDoc = parser.parseFromString(xmlString, "application/xml");
@ -43,16 +43,18 @@ export function parseMmpContent(xmlString) {
const head = xmlDoc.querySelector("head"); const head = xmlDoc.querySelector("head");
if (head) { if (head) {
document.getElementById("bpm-input").value = document.getElementById("bpm-input").value = head.getAttribute("bpm") || 140;
head.getAttribute("bpm") || 140; document.getElementById("bars-input").value = head.getAttribute("num_bars") || 1;
document.getElementById("compasso-a-input").value = document.getElementById("compasso-a-input").value = head.getAttribute("timesig_numerator") || 4;
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 sampleTrackElements = xmlDoc.querySelectorAll(
'instrument[name="audiofileprocessor"]' 'instrument[name="audiofileprocessor"]'
); );
const pathMap = getSamplePathMap();
sampleTrackElements.forEach((instrumentNode) => { sampleTrackElements.forEach((instrumentNode) => {
const afpNode = instrumentNode.querySelector("audiofileprocessor"); const afpNode = instrumentNode.querySelector("audiofileprocessor");
const instrumentTrackNode = instrumentNode.parentElement; const instrumentTrackNode = instrumentNode.parentElement;
@ -61,27 +63,34 @@ export function parseMmpContent(xmlString) {
const audioContext = getAudioContext(); const audioContext = getAudioContext();
const mainGainNode = getMainGainNode(); const mainGainNode = getMainGainNode();
const totalSteps = parseInt(
trackNode.querySelector("pattern")?.getAttribute("steps") || const totalSteps = getTotalSteps();
getTotalSteps(),
10
);
const newSteps = new Array(totalSteps).fill(false); const newSteps = new Array(totalSteps).fill(false);
const ticksPerStep = totalSteps > 0 ? TICKS_PER_BAR / totalSteps : 0;
// ==================================================================
// (CORREÇÃO DEFINITIVA)
// 1. Simplificamos o cálculo: cada step de 1/16 vale 12 ticks.
const ticksPerStep = 12;
trackNode.querySelectorAll("pattern note").forEach((noteNode) => { // 2. Buscamos TODAS as notas da trilha, não importa em qual pattern elas estejam.
trackNode.querySelectorAll("note").forEach((noteNode) => {
// ==================================================================
const pos = parseInt(noteNode.getAttribute("pos"), 10); const pos = parseInt(noteNode.getAttribute("pos"), 10);
const stepIndex = Math.round(pos / ticksPerStep); const stepIndex = Math.round(pos / ticksPerStep);
if (stepIndex < totalSteps) { if (stepIndex < totalSteps) {
newSteps[stepIndex] = true; newSteps[stepIndex] = true;
} }
}); });
const srcAttribute = afpNode.getAttribute("src");
const filename = srcAttribute.split("/").pop();
const finalSamplePath = pathMap[filename] || `src/samples/${srcAttribute}`;
const newTrack = { const newTrack = {
id: Date.now() + Math.random(), id: Date.now() + Math.random(),
name: name: filename || trackNode.getAttribute("name"),
afpNode.getAttribute("src").split("/").pop() || samplePath: finalSamplePath,
trackNode.getAttribute("name"), audioBuffer: null,
samplePath: `samples/${afpNode.getAttribute("src")}`,
steps: newSteps, steps: newSteps,
volume: parseFloat(instrumentTrackNode.getAttribute("vol")) / 100, volume: parseFloat(instrumentTrackNode.getAttribute("vol")) / 100,
pan: parseFloat(instrumentTrackNode.getAttribute("pan")) / 100, pan: parseFloat(instrumentTrackNode.getAttribute("pan")) / 100,
@ -94,6 +103,15 @@ export function parseMmpContent(xmlString) {
newTrack.pannerNode.pan.value = newTrack.pan; newTrack.pannerNode.pan.value = newTrack.pan;
newTracks.push(newTrack); newTracks.push(newTrack);
}); });
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.tracks = newTracks;
renderApp(); renderApp();
console.log("Projeto carregado com sucesso!", appState); console.log("Projeto carregado com sucesso!", appState);
@ -112,6 +130,7 @@ function generateNewMmp() {
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;
const num_bars = document.getElementById("bars-input").value;
const tracksXml = appState.tracks const tracksXml = appState.tracks
.map((track) => createTrackXml(track)) .map((track) => createTrackXml(track))
.join(""); .join("");
@ -119,7 +138,7 @@ function generateNewMmp() {
const mmpContent = `<?xml version="1.0"?> const mmpContent = `<?xml version="1.0"?>
<!DOCTYPE lmms-project> <!DOCTYPE lmms-project>
<lmms-project version="1.0" type="song" creator="MMPCreator" creatorversion="1.0"> <lmms-project version="1.0" type="song" creator="MMPCreator" creatorversion="1.0">
<head mastervol="100" timesig_denominator="${sig_den}" bpm="${bpm}" timesig_numerator="${sig_num}" masterpitch="0"/> <head mastervol="100" timesig_denominator="${sig_den}" bpm="${bpm}" timesig_numerator="${sig_num}" masterpitch="0" num_bars="${num_bars}"/>
<song> <song>
<trackcontainer type="song"> <trackcontainer type="song">
<track type="1" solo="0" muted="0" name="Beat/Bassline 0"> <track type="1" solo="0" muted="0" name="Beat/Bassline 0">
@ -150,6 +169,7 @@ function modifyAndSaveExistingMmp() {
const head = xmlDoc.querySelector("head"); const head = xmlDoc.querySelector("head");
if (head) { if (head) {
head.setAttribute("bpm", document.getElementById("bpm-input").value); head.setAttribute("bpm", document.getElementById("bpm-input").value);
head.setAttribute("num_bars", document.getElementById("bars-input").value);
head.setAttribute( head.setAttribute(
"timesig_numerator", "timesig_numerator",
document.getElementById("compasso-a-input").value document.getElementById("compasso-a-input").value
@ -185,10 +205,10 @@ function modifyAndSaveExistingMmp() {
function createTrackXml(track) { function createTrackXml(track) {
if (!track.samplePath) return ""; if (!track.samplePath) return "";
const totalSteps = track.steps.length || getTotalSteps(); const totalSteps = track.steps.length || getTotalSteps();
const ticksPerStep = totalSteps > 0 ? TICKS_PER_BAR / totalSteps : 0; const ticksPerStep = 12; // Valor fixo de 12 ticks por step de 1/16
const lmmsVolume = Math.round(track.volume * 100); const lmmsVolume = Math.round(track.volume * 100);
const lmmsPan = Math.round(track.pan * 100); const lmmsPan = Math.round(track.pan * 100);
const sampleSrc = track.samplePath.replace("samples/", ""); const sampleSrc = track.samplePath.replace("src/samples/", "");
const notesXml = track.steps const notesXml = track.steps
.map((isActive, index) => { .map((isActive, index) => {
if (isActive) { if (isActive) {
@ -198,6 +218,26 @@ function createTrackXml(track) {
return ""; return "";
}) })
.join("\n "); .join("\n ");
// Cria um pattern para cada compasso (16 steps)
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 ` return `
<track type="0" solo="0" muted="0" name="${track.name}"> <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}"> <instrumenttrack vol="${lmmsVolume}" pitch="0" fxch="0" pitchrange="1" basenote="57" usemasterpitch="1" pan="${lmmsPan}">
@ -206,9 +246,7 @@ function createTrackXml(track) {
</instrument> </instrument>
<fxchain enabled="0" numofeffects="0"/> <fxchain enabled="0" numofeffects="0"/>
</instrumenttrack> </instrumenttrack>
<pattern type="0" pos="0" muted="0" steps="${totalSteps}" name="${track.name}"> ${patternsXml}
${notesXml}
</pattern>
</track>`; </track>`;
} }
@ -224,20 +262,17 @@ function downloadFile(content, fileName) {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
// (ALTERADO) Adiciona export na frente da função
export async function loadProjectFromServer(fileName) { export async function loadProjectFromServer(fileName) {
try { try {
const response = await fetch(`mmp/${fileName}`); const response = await fetch(`mmp/${fileName}`);
if (!response.ok) if (!response.ok)
throw new Error(`Não foi possível carregar o arquivo ${fileName}`); throw new Error(`Não foi possível carregar o arquivo ${fileName}`);
const xmlContent = await response.text(); const xmlContent = await response.text();
parseMmpContent(xmlContent); await parseMmpContent(xmlContent);
// Retorna true em caso de sucesso
return true; return true;
} catch (error) { } catch (error) {
console.error("Erro ao carregar projeto do servidor:", error); console.error("Erro ao carregar projeto do servidor:", error);
alert(`Erro ao carregar projeto: ${error.message}`); alert(`Erro ao carregar projeto: ${error.message}`);
// Retorna false em caso de falha
return false; return false;
} }
} }

View File

@ -36,6 +36,7 @@ document.addEventListener("DOMContentLoaded", () => {
const openModalCloseBtn = document.getElementById("open-modal-close-btn"); const openModalCloseBtn = document.getElementById("open-modal-close-btn");
const loadFromComputerBtn = document.getElementById("load-from-computer-btn"); const loadFromComputerBtn = document.getElementById("load-from-computer-btn");
const sidebarToggle = document.getElementById("sidebar-toggle"); const sidebarToggle = document.getElementById("sidebar-toggle");
const addBarBtn = document.getElementById("add-bar-btn");
newProjectBtn.addEventListener("click", () => { newProjectBtn.addEventListener("click", () => {
if ( if (
@ -51,18 +52,32 @@ document.addEventListener("DOMContentLoaded", () => {
metronomeEnabled: false, metronomeEnabled: false,
originalXmlDoc: null, originalXmlDoc: null,
}); });
// Reseta os inputs para o padrão
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;
renderApp(); renderApp();
}); });
addBarBtn.addEventListener("click", () => {
const barsInput = document.getElementById("bars-input");
if (barsInput) {
adjustValue(barsInput, 1);
}
});
openMmpBtn.addEventListener("click", showOpenProjectModal); openMmpBtn.addEventListener("click", showOpenProjectModal);
loadFromComputerBtn.addEventListener("click", () => mmpFileInput.click()); loadFromComputerBtn.addEventListener("click", () => mmpFileInput.click());
mmpFileInput.addEventListener("change", (event) => {
mmpFileInput.addEventListener("change", async (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (file) { if (file) {
handleFileLoad(file); await handleFileLoad(file);
closeOpenProjectModal(); closeOpenProjectModal();
} }
}); });
saveMmpBtn.addEventListener("click", generateMmpFile); saveMmpBtn.addEventListener("click", generateMmpFile);
addInstrumentBtn.addEventListener("click", addTrackToState); addInstrumentBtn.addEventListener("click", addTrackToState);
removeInstrumentBtn.addEventListener("click", removeLastTrackFromState); removeInstrumentBtn.addEventListener("click", removeLastTrackFromState);
@ -90,10 +105,10 @@ document.addEventListener("DOMContentLoaded", () => {
inputs.forEach((input) => { inputs.forEach((input) => {
input.addEventListener("input", (event) => { input.addEventListener("input", (event) => {
enforceNumericInput(event); enforceNumericInput(event);
if (appState.isPlaying && event.target.id.startsWith("compasso-")) { if (appState.isPlaying && (event.target.id.startsWith("compasso-") || event.target.id === 'bars-input')) {
stopPlayback(); stopPlayback();
} }
if (event.target.id.startsWith("compasso-")) { if (event.target.id.startsWith("compasso-") || event.target.id === 'bars-input') {
redrawSequencer(); redrawSequencer();
} }
}); });
@ -109,6 +124,7 @@ document.addEventListener("DOMContentLoaded", () => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
const targetId = button.dataset.target + "-input"; const targetId = button.dataset.target + "-input";
const targetInput = document.getElementById(targetId); const targetInput = document.getElementById(targetId);
const step = parseInt(button.dataset.step, 10) || 1;
if (targetInput) { if (targetInput) {
adjustValue(targetInput, step); adjustValue(targetInput, step);
} }
@ -118,4 +134,4 @@ document.addEventListener("DOMContentLoaded", () => {
// Inicia a aplicação // Inicia a aplicação
loadAndRenderSampleBrowser(); loadAndRenderSampleBrowser();
renderApp(); renderApp();
}); });

View File

@ -17,6 +17,29 @@ export let appState = {
originalXmlDoc: null, originalXmlDoc: null,
}; };
// ESSA É A FUNÇÃO QUE ESTÁ FALTANDO NO SEU ARQUIVO ATUAL
// Função auxiliar para carregar o buffer de áudio para uma track específica
export async function loadAudioForTrack(track) {
if (!track.samplePath) {
console.warn("Track sem samplePath, pulando o carregamento de áudio.");
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; // Marca como falha para não tentar tocar
}
return track;
}
export function addTrackToState() { export function addTrackToState() {
initializeAudioContext(); initializeAudioContext();
const audioContext = getAudioContext(); const audioContext = getAudioContext();
@ -26,7 +49,7 @@ export function addTrackToState() {
id: Date.now(), id: Date.now(),
name: "novo instrumento", name: "novo instrumento",
samplePath: null, samplePath: null,
audioBuffer: null, // (NOVO) Adicionado para armazenar o áudio decodificado audioBuffer: null,
steps: [], steps: [],
volume: DEFAULT_VOLUME, volume: DEFAULT_VOLUME,
pan: DEFAULT_PAN, pan: DEFAULT_PAN,
@ -47,33 +70,14 @@ export function removeLastTrackFromState() {
renderApp(); renderApp();
} }
// (ALTERADO) A função agora é 'async' para carregar e decodificar o áudio
export async function updateTrackSample(trackId, samplePath) { export async function updateTrackSample(trackId, samplePath) {
const track = appState.tracks.find((t) => t.id == trackId); const track = appState.tracks.find((t) => t.id == trackId);
if (track) { if (track) {
track.samplePath = samplePath; track.samplePath = samplePath;
track.name = samplePath.split("/").pop(); track.name = samplePath.split("/").pop();
track.audioBuffer = null; // Limpa o buffer antigo enquanto carrega o novo track.audioBuffer = null;
renderApp(); // Renderiza imediatamente para mostrar o novo nome renderApp();
await loadAudioForTrack(track); // Reutiliza a nova função aqui
// (NOVO) Lógica para carregar e decodificar o áudio em segundo plano
try {
const audioContext = getAudioContext();
if (!audioContext) initializeAudioContext(); // Garante que o contexto de áudio exista
const response = await fetch(samplePath);
const arrayBuffer = await response.arrayBuffer();
const decodedAudio = await audioContext.decodeAudioData(arrayBuffer);
track.audioBuffer = decodedAudio; // Armazena o buffer decodificado no estado da track
console.log(`Sample ${track.name} carregado e decodificado com sucesso.`);
} catch (error) {
console.error("Erro ao carregar ou decodificar o sample:", error);
track.samplePath = null;
track.name = "erro ao carregar";
renderApp(); // Re-renderiza para mostrar a mensagem de erro
}
} }
} }

View File

@ -8,7 +8,32 @@ import {
} from "./state.js"; } from "./state.js";
import { playSample } from "./audio.js"; import { playSample } from "./audio.js";
import { getTotalSteps } from "./utils.js"; import { getTotalSteps } from "./utils.js";
import { loadProjectFromServer } from "./file.js"; // (CORREÇÃO) Importa a função que faltava import { loadProjectFromServer } from "./file.js";
// Variável para armazenar o mapa de samples (nome do arquivo -> caminho completo)
let samplePathMap = {};
// Função para exportar o mapa de samples para outros módulos
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
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 // RENDERIZAÇÃO PRINCIPAL
export function renderApp() { export function renderApp() {
@ -116,94 +141,97 @@ export function redrawSequencer() {
// LÓGICA DE INTERAÇÃO DOS KNOBS // LÓGICA DE INTERAÇÃO DOS KNOBS
function addKnobInteraction(knobElement) { function addKnobInteraction(knobElement) {
const controlType = knobElement.dataset.control; const controlType = knobElement.dataset.control;
knobElement.addEventListener("mousedown", (e) => { knobElement.addEventListener("mousedown", (e) => {
if (e.button === 1) { if (e.button === 1) { // Middle mouse button
e.preventDefault();
const trackId = knobElement.dataset.trackId;
const defaultValue = controlType === "volume" ? 0.8 : 0.0;
if (controlType === "volume") {
updateTrackVolume(trackId, defaultValue);
} else {
updateTrackPan(trackId, defaultValue);
}
updateKnobVisual(knobElement, controlType);
}
});
knobElement.addEventListener("mousedown", (e) => {
if (e.button !== 0) return; // Apenas botão esquerdo
e.preventDefault(); e.preventDefault();
const trackId = knobElement.dataset.trackId; const trackId = knobElement.dataset.trackId;
const defaultValue = controlType === "volume" ? 0.8 : 0.0; const track = appState.tracks.find((t) => t.id == trackId);
if (controlType === "volume") { if (!track) return;
updateTrackVolume(trackId, defaultValue); const startY = e.clientY;
} else { const startValue = controlType === "volume" ? track.volume : track.pan;
updateTrackPan(trackId, defaultValue); document.body.classList.add("knob-dragging");
function onMouseMove(moveEvent) {
const deltaY = startY - moveEvent.clientY;
const sensitivity = controlType === "volume" ? 150 : 200;
const newValue = startValue + deltaY / sensitivity;
if (controlType === "volume") {
updateTrackVolume(trackId, newValue);
} else {
updateTrackPan(trackId, newValue);
}
updateKnobVisual(knobElement, controlType);
} }
} function onMouseUp() {
}); document.body.classList.remove("knob-dragging");
knobElement.addEventListener("mousedown", (e) => { document.removeEventListener("mousemove", onMouseMove);
if (e.button !== 0) return; document.removeEventListener("mouseup", onMouseUp);
e.preventDefault(); }
const trackId = knobElement.dataset.trackId; document.addEventListener("mousemove", onMouseMove);
const track = appState.tracks.find((t) => t.id == trackId); document.addEventListener("mouseup", onMouseUp);
if (!track) return; });
const startY = e.clientY; knobElement.addEventListener("wheel", (e) => {
const startValue = controlType === "volume" ? track.volume : track.pan; e.preventDefault();
document.body.classList.add("knob-dragging"); const trackId = knobElement.dataset.trackId;
function onMouseMove(moveEvent) { const track = appState.tracks.find((t) => t.id == trackId);
const deltaY = startY - moveEvent.clientY; if (!track) return;
const sensitivity = controlType === "volume" ? 150 : 200; const step = 0.05;
const newValue = startValue + deltaY / sensitivity; const direction = e.deltaY < 0 ? 1 : -1;
if (controlType === "volume") { if (controlType === "volume") {
const newValue = track.volume + direction * step;
updateTrackVolume(trackId, newValue); updateTrackVolume(trackId, newValue);
} else { } else {
const newValue = track.pan + direction * step;
updateTrackPan(trackId, newValue); updateTrackPan(trackId, newValue);
} }
} updateKnobVisual(knobElement, controlType);
function onMouseUp() { });
document.body.classList.remove("knob-dragging");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
}
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
knobElement.addEventListener("wheel", (e) => {
e.preventDefault();
const trackId = knobElement.dataset.trackId;
const track = appState.tracks.find((t) => t.id == trackId);
if (!track) return;
const step = 0.05;
const direction = e.deltaY < 0 ? 1 : -1;
if (controlType === "volume") {
const newValue = track.volume + direction * step;
updateTrackVolume(trackId, newValue);
} else {
const newValue = track.pan + direction * step;
updateTrackPan(trackId, newValue);
}
});
} }
function updateKnobVisual(knobElement, controlType) { function updateKnobVisual(knobElement, controlType) {
const trackId = knobElement.dataset.trackId; const trackId = knobElement.dataset.trackId;
const track = appState.tracks.find((t) => t.id == trackId); const track = appState.tracks.find((t) => t.id == trackId);
if (!track) return; if (!track) return;
const indicator = knobElement.querySelector(".knob-indicator"); const indicator = knobElement.querySelector(".knob-indicator");
if (!indicator) return; if (!indicator) return;
const minAngle = -135; const minAngle = -135;
const maxAngle = 135; const maxAngle = 135;
let percentage = 0.5; let percentage = 0.5;
let title = ""; let title = "";
if (controlType === "volume") { if (controlType === "volume") {
const value = track.volume; const value = track.volume;
const clampedValue = Math.max(0, Math.min(1.5, value)); const clampedValue = Math.max(0, Math.min(1.5, value));
percentage = clampedValue / 1.5; percentage = clampedValue / 1.5;
title = `Volume: ${Math.round(clampedValue * 100)}%`; title = `Volume: ${Math.round(clampedValue * 100)}%`;
} else { } else {
const value = track.pan; const value = track.pan;
const clampedValue = Math.max(-1, Math.min(1, value)); const clampedValue = Math.max(-1, Math.min(1, value));
percentage = (clampedValue + 1) / 2; percentage = (clampedValue + 1) / 2;
const panDisplay = Math.round(clampedValue * 100); const panDisplay = Math.round(clampedValue * 100);
title = `Pan: ${ title = `Pan: ${
panDisplay === 0 panDisplay === 0
? "Centro" ? "Centro"
: panDisplay < 0 : panDisplay < 0
? `${-panDisplay} L` ? `${-panDisplay} L`
: `${panDisplay} R` : `${panDisplay} R`
}`; }`;
} }
const angle = minAngle + percentage * (maxAngle - minAngle); const angle = minAngle + percentage * (maxAngle - minAngle);
indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`; indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`;
knobElement.title = title; knobElement.title = title;
} }
export function highlightStep(stepIndex, isActive) { export function highlightStep(stepIndex, isActive) {
@ -230,6 +258,11 @@ export async function loadAndRenderSampleBrowser() {
throw new Error("Arquivo samples-manifest.json não encontrado."); throw new Error("Arquivo samples-manifest.json não encontrado.");
} }
const fileTree = await response.json(); const fileTree = await response.json();
samplePathMap = {};
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) {
console.error("Erro ao carregar samples:", error); console.error("Erro ao carregar samples:", error);
@ -238,82 +271,82 @@ export async function loadAndRenderSampleBrowser() {
} }
function renderFileTree(tree, parentElement, currentPath) { function renderFileTree(tree, parentElement, currentPath) {
parentElement.innerHTML = ""; parentElement.innerHTML = "";
const ul = document.createElement("ul"); const ul = document.createElement("ul");
const sortedKeys = Object.keys(tree).sort((a, b) => { const sortedKeys = Object.keys(tree).sort((a, b) => {
const aIsFile = tree[a]._isFile; const aIsFile = tree[a]._isFile;
const bIsFile = tree[b]._isFile; const bIsFile = tree[b]._isFile;
if (aIsFile === bIsFile) return a.localeCompare(b); if (aIsFile === bIsFile) return a.localeCompare(b);
return aIsFile ? 1 : -1; return aIsFile ? 1 : -1;
}); });
for (const key of sortedKeys) { for (const key of sortedKeys) {
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}`;
if (node._isFile) { if (node._isFile) {
li.innerHTML = `<i class="fa-solid fa-file-audio"></i> ${key}`; li.innerHTML = `<i class="fa-solid fa-file-audio"></i> ${key}`;
li.setAttribute("draggable", true); li.setAttribute("draggable", true);
li.addEventListener("click", (e) => { li.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
playSample(newPath, null); playSample(newPath, null);
}); });
li.addEventListener("dragstart", (e) => { li.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("text/plain", newPath); e.dataTransfer.setData("text/plain", newPath);
e.dataTransfer.effectAllowed = "copy"; e.dataTransfer.effectAllowed = "copy";
}); });
ul.appendChild(li); ul.appendChild(li);
} else { } else {
li.className = "directory"; li.className = "directory";
li.innerHTML = `<i class="fa-solid fa-folder"></i> ${key}`; li.innerHTML = `<i class="fa-solid fa-folder"></i> ${key}`;
const nestedUl = document.createElement("ul"); const nestedUl = document.createElement("ul");
renderFileTree(node, nestedUl, newPath); renderFileTree(node, nestedUl, newPath);
li.appendChild(nestedUl); li.appendChild(nestedUl);
li.addEventListener("click", (e) => { li.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
li.classList.toggle("open"); li.classList.toggle("open");
}); });
ul.appendChild(li); ul.appendChild(li);
}
} }
} parentElement.appendChild(ul);
parentElement.appendChild(ul);
} }
export async function showOpenProjectModal() { export async function showOpenProjectModal() {
const openProjectModal = document.getElementById("open-project-modal"); const openProjectModal = document.getElementById("open-project-modal");
const serverProjectsList = document.getElementById("server-projects-list"); const serverProjectsList = document.getElementById("server-projects-list");
serverProjectsList.innerHTML = "<p>Carregando...</p>"; serverProjectsList.innerHTML = "<p>Carregando...</p>";
openProjectModal.classList.add("visible"); openProjectModal.classList.add("visible");
try { try {
const response = await fetch("metadata/mmp-manifest.json"); const response = await fetch("metadata/mmp-manifest.json");
if (!response.ok) if (!response.ok)
throw new Error("Arquivo mmp-manifest.json não encontrado."); throw new Error("Arquivo mmp-manifest.json não encontrado.");
const projects = await response.json(); const projects = await response.json();
serverProjectsList.innerHTML = ""; serverProjectsList.innerHTML = "";
if (projects.length === 0) { if (projects.length === 0) {
serverProjectsList.innerHTML = serverProjectsList.innerHTML =
'<p style="color:var(--text-dark);">Nenhum projeto encontrado no servidor.</p>'; '<p style="color:var(--text-dark);">Nenhum projeto encontrado no servidor.</p>';
} }
projects.forEach((projectName) => { projects.forEach((projectName) => {
const projectItem = document.createElement("div"); const projectItem = document.createElement("div");
projectItem.className = "project-item"; projectItem.className = "project-item";
projectItem.textContent = projectName; projectItem.textContent = projectName;
projectItem.addEventListener("click", async () => { projectItem.addEventListener("click", async () => {
const success = await loadProjectFromServer(projectName); const success = await loadProjectFromServer(projectName);
if (success) { if (success) {
closeOpenProjectModal(); closeOpenProjectModal();
} }
});
serverProjectsList.appendChild(projectItem);
}); });
serverProjectsList.appendChild(projectItem); } catch (error) {
}); console.error("Erro ao carregar lista de projetos:", error);
} catch (error) { serverProjectsList.innerHTML = `<p style="color:var(--accent-red);">${error.message}</p>`;
console.error("Erro ao carregar lista de projetos:", error); }
serverProjectsList.innerHTML = `<p style="color:var(--accent-red);">${error.message}</p>`;
}
} }
export function closeOpenProjectModal() { export function closeOpenProjectModal() {
const openProjectModal = document.getElementById("open-project-modal"); const openProjectModal = document.getElementById("open-project-modal");
openProjectModal.classList.remove("visible"); openProjectModal.classList.remove("visible");
} }

View File

@ -1,12 +1,16 @@
// js/utils.js // js/utils.js
export function getTotalSteps() { export function getTotalSteps() {
const barsInput = document.getElementById("bars-input");
const compassoAInput = document.getElementById("compasso-a-input"); const compassoAInput = document.getElementById("compasso-a-input");
const compassoBInput = document.getElementById("compasso-b-input"); const compassoBInput = document.getElementById("compasso-b-input");
const numberOfBars = parseInt(barsInput.value, 10) || 1;
const beatsPerBar = parseInt(compassoAInput.value, 10) || 4; const beatsPerBar = parseInt(compassoAInput.value, 10) || 4;
const noteValue = parseInt(compassoBInput.value, 10) || 4; const noteValue = parseInt(compassoBInput.value, 10) || 4;
const subdivisions = Math.round(16 / noteValue); const subdivisions = Math.round(16 / noteValue);
return beatsPerBar * subdivisions;
return numberOfBars * beatsPerBar * subdivisions;
} }
export function enforceNumericInput(event) { export function enforceNumericInput(event) {
@ -24,4 +28,4 @@ export function adjustValue(inputElement, step) {
// Dispara um evento 'input' para que outros listeners (como o que redesenha o sequenciador) sejam acionados. // Dispara um evento 'input' para que outros listeners (como o que redesenha o sequenciador) sejam acionados.
inputElement.dispatchEvent(new Event("input", { bubbles: true })); inputElement.dispatchEvent(new Event("input", { bubbles: true }));
} }

View File

@ -67,6 +67,23 @@
</div> </div>
<div class="label">ANDAMENTO/BPM</div> <div class="label">ANDAMENTO/BPM</div>
</div> </div>
<div class="info-display">
<div class="interactive-input-container">
<button class="adjust-btn" data-target="bars" data-step="-1">
-</button
><input
type="text"
class="value-input"
id="bars-input"
value="1"
data-min="1"
data-max="64"
/><button class="adjust-btn" data-target="bars" data-step="1">
+
</button>
</div>
<div class="label">COMPASSOS</div>
</div>
<div class="info-display"> <div class="info-display">
<div class="interactive-input-container"> <div class="interactive-input-container">
<div class="compasso-group"> <div class="compasso-group">
@ -119,10 +136,11 @@
</div> </div>
<div class="info-display"> <div class="info-display">
<div <div
id="timer-display"
class="interactive-input-container" class="interactive-input-container"
style="font-size: 0.7rem; color: var(--text-dark)" style="font-size: 0.7rem; color: var(--text-dark)"
> >
0:00:00 00:00:00
</div> </div>
<div class="label">MIN:SEC:MSEC</div> <div class="label">MIN:SEC:MSEC</div>
</div> </div>
@ -159,7 +177,7 @@
<i class="fa-solid fa-table-cells"></i <i class="fa-solid fa-table-cells"></i
><i class="fa-solid fa-bars-staggered"></i ><i class="fa-solid fa-bars-staggered"></i
><i class="fa-solid fa-wave-square enabled"></i ><i class="fa-solid fa-wave-square enabled"></i
><i class="fa-solid fa-plus"></i> ><i class="fa-solid fa-plus" id="add-bar-btn" title="Adicionar 1 Compasso"></i>
</div> </div>
<div class="zoom-controls"> <div class="zoom-controls">
<i class="fa-solid fa-minus" id="remove-instrument-btn"></i <i class="fa-solid fa-minus" id="remove-instrument-btn"></i
@ -198,4 +216,4 @@
<script src="assets/js/creations/main.js" type="module" defer></script> <script src="assets/js/creations/main.js" type="module" defer></script>
</body> </body>
</html> </html>