diff --git a/assets/js/creations/audio.js b/assets/js/creations/audio.js index b4d7337..42850ea 100644 --- a/assets/js/creations/audio.js +++ b/assets/js/creations/audio.js @@ -5,6 +5,7 @@ import { getTotalSteps } from "./utils.js"; let audioContext; let mainGainNode; +let masterPannerNode; const timerDisplay = document.getElementById('timer-display'); @@ -19,13 +20,29 @@ export function initializeAudioContext() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); mainGainNode = audioContext.createGain(); - mainGainNode.connect(audioContext.destination); + masterPannerNode = audioContext.createStereoPanner(); + + // Roteamento: Gain Master -> Panner Master -> Saída + mainGainNode.connect(masterPannerNode); + masterPannerNode.connect(audioContext.destination); } if (audioContext.state === "suspended") { audioContext.resume(); } } +export function updateMasterVolume(volume) { + if (mainGainNode) { + mainGainNode.gain.setValueAtTime(volume, audioContext.currentTime); + } +} + +export function updateMasterPan(pan) { + if (masterPannerNode) { + masterPannerNode.pan.setValueAtTime(pan, audioContext.currentTime); + } +} + function formatTime(milliseconds) { const totalSeconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0'); diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index 1089ad3..612864d 100644 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -9,6 +9,8 @@ import { stopPlayback, rewindPlayback, initializeAudioContext, + updateMasterVolume, + updateMasterPan, } from "./audio.js"; import { handleFileLoad, generateMmpFile } from "./file.js"; import { @@ -19,6 +21,7 @@ import { closeOpenProjectModal, } from "./ui.js"; import { adjustValue, enforceNumericInput } from "./utils.js"; +import { DEFAULT_PAN, DEFAULT_VOLUME } from "./config.js"; // --- INICIALIZAÇÃO E EVENTOS FINAIS --- document.addEventListener("DOMContentLoaded", () => { @@ -37,6 +40,9 @@ document.addEventListener("DOMContentLoaded", () => { const loadFromComputerBtn = document.getElementById("load-from-computer-btn"); const sidebarToggle = document.getElementById("sidebar-toggle"); const addBarBtn = document.getElementById("add-bar-btn"); + + const masterVolumeKnob = document.getElementById("master-volume-knob"); + const masterPanKnob = document.getElementById("master-pan-knob"); newProjectBtn.addEventListener("click", () => { if ( @@ -51,6 +57,8 @@ document.addEventListener("DOMContentLoaded", () => { currentStep: 0, metronomeEnabled: false, originalXmlDoc: null, + masterVolume: DEFAULT_VOLUME, + masterPan: DEFAULT_PAN }); // Reseta os inputs para o padrão document.getElementById('bpm-input').value = 140; @@ -58,6 +66,7 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById('compasso-a-input').value = 4; document.getElementById('compasso-b-input').value = 4; renderApp(); + setupMasterKnobs(); // Re-inicializa os knobs master }); addBarBtn.addEventListener("click", () => { @@ -67,9 +76,95 @@ document.addEventListener("DOMContentLoaded", () => { } }); + function setupMasterKnobs() { + function updateMasterKnobVisual(knobElement, controlType) { + const indicator = knobElement.querySelector(".knob-indicator"); + if (!indicator) return; + + const minAngle = -135; + const maxAngle = 135; + let percentage = 0.5; + let title = ""; + + if (controlType === "volume") { + const value = appState.masterVolume; + percentage = value / 1.5; + title = `Volume Master: ${Math.round(value * 100)}%`; + } else { // pan + const value = appState.masterPan; + percentage = (value + 1) / 2; + const panDisplay = Math.round(value * 100); + title = `Pan Master: ${ panDisplay === 0 ? "Centro" : panDisplay < 0 ? `${-panDisplay} L` : `${panDisplay} R` }`; + } + + const angle = minAngle + percentage * (maxAngle - minAngle); + indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`; + knobElement.title = title; + } + + function addMasterKnobInteraction(knobElement, controlType) { + knobElement.addEventListener("wheel", (e) => { + e.preventDefault(); + const step = 0.05; + const direction = e.deltaY < 0 ? 1 : -1; + if (controlType === "volume") { + const newValue = appState.masterVolume + direction * step; + const clampedValue = Math.max(0, Math.min(1.5, newValue)); + appState.masterVolume = clampedValue; + updateMasterVolume(clampedValue); + } else { + const newValue = appState.masterPan + direction * step; + const clampedValue = Math.max(-1, Math.min(1, newValue)); + appState.masterPan = clampedValue; + updateMasterPan(clampedValue); + } + updateMasterKnobVisual(knobElement, controlType); + }); + + knobElement.addEventListener("mousedown", (e) => { + if (e.button !== 0) return; + e.preventDefault(); + + const startY = e.clientY; + const startValue = controlType === "volume" ? appState.masterVolume : appState.masterPan; + 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") { + const clampedValue = Math.max(0, Math.min(1.5, newValue)); + appState.masterVolume = clampedValue; + updateMasterVolume(clampedValue); + } else { + const clampedValue = Math.max(-1, Math.min(1, newValue)); + appState.masterPan = clampedValue; + updateMasterPan(clampedValue); + } + updateMasterKnobVisual(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); + }); + } + + addMasterKnobInteraction(masterVolumeKnob, "volume"); + updateMasterKnobVisual(masterVolumeKnob, "volume"); + + addMasterKnobInteraction(masterPanKnob, "pan"); + updateMasterKnobVisual(masterPanKnob, "pan"); + } + openMmpBtn.addEventListener("click", showOpenProjectModal); loadFromComputerBtn.addEventListener("click", () => mmpFileInput.click()); - mmpFileInput.addEventListener("change", async (event) => { const file = event.target.files[0]; if (file) { @@ -77,7 +172,6 @@ document.addEventListener("DOMContentLoaded", () => { closeOpenProjectModal(); } }); - saveMmpBtn.addEventListener("click", generateMmpFile); addInstrumentBtn.addEventListener("click", addTrackToState); removeInstrumentBtn.addEventListener("click", removeLastTrackFromState); @@ -100,7 +194,6 @@ document.addEventListener("DOMContentLoaded", () => { ? "fa-solid fa-caret-right" : "fa-solid fa-caret-left"; }); - const inputs = document.querySelectorAll(".value-input"); inputs.forEach((input) => { input.addEventListener("input", (event) => { @@ -118,7 +211,6 @@ document.addEventListener("DOMContentLoaded", () => { adjustValue(event.target, step); }); }); - const buttons = document.querySelectorAll(".adjust-btn"); buttons.forEach((button) => { button.addEventListener("click", () => { @@ -134,4 +226,5 @@ document.addEventListener("DOMContentLoaded", () => { // Inicia a aplicação loadAndRenderSampleBrowser(); renderApp(); + setupMasterKnobs(); }); \ No newline at end of file diff --git a/assets/js/creations/state.js b/assets/js/creations/state.js index 5f1ee81..2945a76 100644 --- a/assets/js/creations/state.js +++ b/assets/js/creations/state.js @@ -15,10 +15,10 @@ export let appState = { currentStep: 0, metronomeEnabled: false, originalXmlDoc: null, + masterVolume: DEFAULT_VOLUME, + masterPan: DEFAULT_PAN, }; -// 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."); @@ -35,7 +35,7 @@ export async function loadAudioForTrack(track) { 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 + track.audioBuffer = null; } return track; } @@ -77,7 +77,7 @@ export async function updateTrackSample(trackId, samplePath) { track.name = samplePath.split("/").pop(); track.audioBuffer = null; renderApp(); - await loadAudioForTrack(track); // Reutiliza a nova função aqui + await loadAudioForTrack(track); } } diff --git a/creation.html b/creation.html index 350f5b9..c457f88 100644 --- a/creation.html +++ b/creation.html @@ -149,6 +149,21 @@
+ +
+
+
+
+
+ VOL MASTER +
+
+
+
+
+ PAN MASTER +
+