diff --git a/assets/css/style.css b/assets/css/style.css index a3489e4..cdf12bd 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -488,4 +488,14 @@ body.sidebar-hidden .global-toolbar { z-index: 10; pointer-events: none; /* Impede que a agulha intercepte cliques do mouse */ transition: background-color 0.3s; /* Efeito suave ao parar */ +} + +/* Estilo para o botão de loop na barra de ferramentas do editor de áudio */ +#audio-editor-loop-btn { + color: var(--text-dark); /* Cor padrão quando está desligado */ + transition: color 0.2s; +} + +#audio-editor-loop-btn.active { + color: var(--accent-green); /* Cor de destaque quando está ligado */ } \ No newline at end of file diff --git a/assets/js/creations/audio.js b/assets/js/creations/audio.js index ce17269..7bbfde1 100644 --- a/assets/js/creations/audio.js +++ b/assets/js/creations/audio.js @@ -31,7 +31,6 @@ export function initializeAudioContext() { } } -// ... (funções de master volume/pan, formatTime, metronome, sample player, tick, etc. permanecem iguais)... export function updateMasterVolume(volume) { if (mainGainNode) { mainGainNode.gain.setValueAtTime(volume, audioContext.currentTime); @@ -195,23 +194,33 @@ export function togglePlayback() { function animationLoop() { if (!appState.isAudioEditorPlaying || !audioContext) return; + const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120; const stepsPerSecond = (bpm / 60) * 4; const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP; - const totalElapsedTime = (audioContext.currentTime - appState.audioEditorStartTime) + appState.audioEditorPlaybackTime; - const newPositionPx = totalElapsedTime * pixelsPerSecond; - const maxDuration = appState.audioTracks.reduce((max, track) => (track.audioBuffer && track.audioBuffer.duration > max) ? track.audioBuffer.duration : max, 0); - if (totalElapsedTime >= maxDuration && maxDuration > 0) { - stopAudioEditorPlayback(); - appState.audioEditorPlaybackTime = 0; - resetPlayheadVisual(); - return; + + let totalElapsedTime = (audioContext.currentTime - appState.audioEditorStartTime) + appState.audioEditorPlaybackTime; + + const maxDuration = appState.audioTracks.reduce((max, track) => + (track.audioBuffer && track.audioBuffer.duration > max) ? track.audioBuffer.duration : max, 0 + ); + + if (appState.isAudioEditorLoopEnabled && maxDuration > 0) { + totalElapsedTime = totalElapsedTime % maxDuration; + } else { + if (totalElapsedTime >= maxDuration && maxDuration > 0) { + stopAudioEditorPlayback(); + appState.audioEditorPlaybackTime = 0; + resetPlayheadVisual(); + return; + } } + + const newPositionPx = totalElapsedTime * pixelsPerSecond; updatePlayheadVisual(newPositionPx); appState.audioEditorAnimationId = requestAnimationFrame(animationLoop); } -// --- LÓGICA DE REPRODUÇÃO ATUALIZADA --- export function startAudioEditorPlayback() { if (appState.isAudioEditorPlaying || appState.audioTracks.length === 0) return; initializeAudioContext(); @@ -223,25 +232,13 @@ export function startAudioEditorPlayback() { const startTime = audioContext.currentTime; appState.audioEditorStartTime = startTime; - // Verifica se existe alguma faixa no modo "solo" - const isAnyTrackSoloed = appState.audioTracks.some(t => t.isSoloed); - appState.audioTracks.forEach(track => { - // Condições para tocar: - // 1. A faixa deve ter um buffer de áudio. - // 2. A faixa não pode estar mutada. - const canPlay = track.audioBuffer && !track.isMuted; - - // 3. Lógica de solo: - // - Se houver alguma faixa solada, esta faixa TAMBÉM deve estar solada. - // - Se NENHUMA faixa estiver solada, todas podem tocar. - const shouldPlay = isAnyTrackSoloed ? track.isSoloed : true; - - if (canPlay && shouldPlay) { + if (track.audioBuffer && !track.isMuted && track.isSoloed) { if (appState.audioEditorPlaybackTime >= track.audioBuffer.duration) return; const source = audioContext.createBufferSource(); source.buffer = track.audioBuffer; + source.loop = appState.isAudioEditorLoopEnabled; source.connect(track.gainNode); source.start(startTime, appState.audioEditorPlaybackTime); appState.activeAudioSources.push(source); @@ -261,17 +258,35 @@ export function startAudioEditorPlayback() { export function stopAudioEditorPlayback() { if (!appState.isAudioEditorPlaying) return; - const elapsedTime = (audioContext.currentTime - appState.audioEditorStartTime); - appState.audioEditorPlaybackTime += elapsedTime; + + let totalElapsedTime = (audioContext.currentTime - appState.audioEditorStartTime) + appState.audioEditorPlaybackTime; + + const maxDuration = appState.audioTracks.reduce((max, track) => + (track.audioBuffer && track.audioBuffer.duration > max) ? track.audioBuffer.duration : max, 0 + ); + + // --- CORREÇÃO FINAL E ROBUSTA --- + // Sempre aplica o módulo ao salvar o tempo. + // Se não estava em loop, totalElapsedTime < maxDuration, e o módulo não faz nada. + // Se estava em loop, ele corrige o valor para a posição visual correta. + if (maxDuration > 0) { + appState.audioEditorPlaybackTime = totalElapsedTime % maxDuration; + } else { + appState.audioEditorPlaybackTime = totalElapsedTime; + } + // --- FIM DA CORREÇÃO --- + if (appState.audioEditorAnimationId) { cancelAnimationFrame(appState.audioEditorAnimationId); appState.audioEditorAnimationId = null; } + appState.activeAudioSources.forEach(source => { try { source.stop(0); } catch (e) { /* Ignora erros */ } }); + appState.activeAudioSources = []; appState.isAudioEditorPlaying = false; updateAudioEditorUI(); @@ -291,4 +306,11 @@ export function seekAudioEditor(newTime) { if (wasPlaying) { startAudioEditorPlayback(); } +} + +export function restartAudioEditorIfPlaying() { + if (appState.isAudioEditorPlaying) { + stopAudioEditorPlayback(); + startAudioEditorPlayback(); + } } \ No newline at end of file diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index fb7b049..0395c23 100644 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -13,6 +13,7 @@ import { updateMasterPan, startAudioEditorPlayback, stopAudioEditorPlayback, + restartAudioEditorIfPlaying, } from "./audio.js"; import { handleFileLoad, generateMmpFile } from "./file.js"; import { @@ -21,7 +22,7 @@ import { loadAndRenderSampleBrowser, showOpenProjectModal, closeOpenProjectModal, - handleSampleUpload, // Importa handleSampleUpload, embora não seja mais usado diretamente + handleSampleUpload, } from "./ui.js"; import { adjustValue, enforceNumericInput } from "./utils.js"; import { DEFAULT_PAN, DEFAULT_VOLUME } from "./config.js"; @@ -37,6 +38,7 @@ document.addEventListener("DOMContentLoaded", () => { const stopBtn = document.getElementById("stop-btn"); const audioEditorPlayBtn = document.getElementById("audio-editor-play-btn"); const audioEditorStopBtn = document.getElementById("audio-editor-stop-btn"); + const audioEditorLoopBtn = document.getElementById("audio-editor-loop-btn"); const rewindBtn = document.getElementById("rewind-btn"); const metronomeBtn = document.getElementById("metronome-btn"); const mmpFileInput = document.getElementById("mmp-file-input"); @@ -57,6 +59,7 @@ document.addEventListener("DOMContentLoaded", () => { return; Object.assign(appState, { tracks: [], + audioTracks: [], activeTrackId: null, isPlaying: false, playbackIntervalId: null, @@ -166,8 +169,6 @@ document.addEventListener("DOMContentLoaded", () => { uploadSampleBtn.addEventListener("click", () => sampleFileInput.click()); - // --- INÍCIO DA CORREÇÃO --- - // Lógica de upload de sample para o servidor Flask sampleFileInput.addEventListener("change", async (event) => { const file = event.target.files[0]; if (!file) return; @@ -176,7 +177,6 @@ document.addEventListener("DOMContentLoaded", () => { formData.append("sampleFile", file); try { - // ATENÇÃO: Verifique se a URL e a porta estão corretas para o seu servidor Flask const response = await fetch('http://localhost:5000/upload-sample', { method: 'POST', body: formData, @@ -186,7 +186,6 @@ document.addEventListener("DOMContentLoaded", () => { if (response.ok) { alert("Sample enviado com sucesso!"); - // Recarrega a lista de samples para exibir o novo arquivo await loadAndRenderSampleBrowser(); } else { throw new Error(result.error || "Erro desconhecido no servidor."); @@ -197,9 +196,8 @@ document.addEventListener("DOMContentLoaded", () => { alert(`Falha no upload: ${error.message}`); } - event.target.value = null; // Limpa o input para permitir o mesmo arquivo de novo + event.target.value = null; }); - // --- FIM DA CORREÇÃO --- saveMmpBtn.addEventListener("click", generateMmpFile); addInstrumentBtn.addEventListener("click", addTrackToState); @@ -252,7 +250,6 @@ document.addEventListener("DOMContentLoaded", () => { }); }); - // Listeners para os controles do editor de áudio audioEditorPlayBtn.addEventListener("click", () => { if (appState.isAudioEditorPlaying) { stopAudioEditorPlayback(); @@ -263,6 +260,12 @@ document.addEventListener("DOMContentLoaded", () => { audioEditorStopBtn.addEventListener("click", stopAudioEditorPlayback); + audioEditorLoopBtn.addEventListener("click", () => { + appState.isAudioEditorLoopEnabled = !appState.isAudioEditorLoopEnabled; + audioEditorLoopBtn.classList.toggle("active", appState.isAudioEditorLoopEnabled); + restartAudioEditorIfPlaying(); + }); + loadAndRenderSampleBrowser(); renderApp(); setupMasterKnobs(); diff --git a/assets/js/creations/state.js b/assets/js/creations/state.js index 1418dcc..b2abe24 100644 --- a/assets/js/creations/state.js +++ b/assets/js/creations/state.js @@ -18,7 +18,8 @@ export let appState = { activeAudioSources: [], audioEditorStartTime: 0, audioEditorAnimationId: null, - audioEditorPlaybackTime: 0, + audioEditorPlaybackTime: 0, + isAudioEditorLoopEnabled: false, // <-- ADICIONADO: Estado para controlar o loop playbackIntervalId: null, currentStep: 0, metronomeEnabled: false, diff --git a/creation.html b/creation.html index a2d7824..a9dbea9 100644 --- a/creation.html +++ b/creation.html @@ -125,10 +125,11 @@