Arrumando botões de volume e pan, criação de novos compassos.
Deploy / Deploy (push) Successful in 40s
Details
Deploy / Deploy (push) Successful in 40s
Details
This commit is contained in:
parent
3524b5cff6
commit
cd3b236acf
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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();
|
||||||
});
|
});
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
|
@ -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 }));
|
||||||
}
|
}
|
|
@ -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>
|
Loading…
Reference in New Issue