// js/audio.js import { appState } from "./state.js"; import { highlightStep } from "./ui.js"; import { getTotalSteps } from "./utils.js"; let audioContext; let mainGainNode; let masterPannerNode; const timerDisplay = document.getElementById('timer-display'); export function getAudioContext() { return audioContext; } export function getMainGainNode() { return mainGainNode; } export function initializeAudioContext() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); mainGainNode = audioContext.createGain(); masterPannerNode = audioContext.createStereoPanner(); 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'); const seconds = (totalSeconds % 60).toString().padStart(2, '0'); const centiseconds = Math.floor((milliseconds % 1000) / 10).toString().padStart(2, '0'); return `${minutes}:${seconds}:${centiseconds}`; } export function playMetronomeSound(isDownbeat) { initializeAudioContext(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); const frequency = isDownbeat ? 1000 : 800; oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); oscillator.type = "sine"; gainNode.gain.setValueAtTime(1, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime( 0.00001, audioContext.currentTime + 0.05 ); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.05); } export function playSample(filePath, trackId) { initializeAudioContext(); if (!filePath) return; const track = trackId ? appState.tracks.find((t) => t.id == trackId) : null; if (!track || !track.audioBuffer) { // Se não houver buffer (ex: preview do sample browser), toca como um áudio simples const audio = new Audio(filePath); audio.play(); return; } const source = audioContext.createBufferSource(); source.buffer = track.audioBuffer; if (track.gainNode) { source.connect(track.gainNode); } else { source.connect(mainGainNode); } source.start(0); } function tick() { const totalSteps = getTotalSteps(); if (totalSteps === 0) { stopPlayback(); return; } const lastStepIndex = appState.currentStep === 0 ? totalSteps - 1 : appState.currentStep - 1; highlightStep(lastStepIndex, false); const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120; const stepInterval = (60 * 1000) / (bpm * 4); const currentTime = appState.currentStep * stepInterval; if (timerDisplay) { timerDisplay.textContent = formatTime(currentTime); } if (appState.metronomeEnabled) { const noteValue = parseInt(document.getElementById("compasso-b-input").value, 10) || 4; const stepsPerBeat = 16 / noteValue; if (appState.currentStep % stepsPerBeat === 0) { playMetronomeSound(appState.currentStep === 0); } } // --- INÍCIO DA CORREÇÃO --- appState.tracks.forEach((track) => { // 1. Verifica se a faixa tem patterns if (!track.patterns || track.patterns.length === 0) return; // 2. Pega o pattern que está ativo para esta faixa const activePattern = track.patterns[track.activePatternIndex]; // 3. Verifica se o pattern existe e se o step atual está ativo NELE if (activePattern && activePattern.steps[appState.currentStep] && track.samplePath) { playSample(track.samplePath, track.id); } }); // --- FIM DA CORREÇÃO --- highlightStep(appState.currentStep, true); appState.currentStep = (appState.currentStep + 1) % totalSteps; } export function startPlayback() { if (appState.isPlaying || appState.tracks.length === 0) return; initializeAudioContext(); const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120; const stepInterval = (60 * 1000) / (bpm * 4); if (appState.playbackIntervalId) clearInterval(appState.playbackIntervalId); appState.isPlaying = true; document.getElementById("play-btn").classList.remove("fa-play"); document.getElementById("play-btn").classList.add("fa-pause"); tick(); appState.playbackIntervalId = setInterval(tick, stepInterval); } export function stopPlayback() { if(appState.playbackIntervalId) { clearInterval(appState.playbackIntervalId); } appState.playbackIntervalId = null; appState.isPlaying = false; highlightStep(appState.currentStep - 1, false); highlightStep(appState.currentStep, false); // Garante que o último step "playing" seja limpo appState.currentStep = 0; if (timerDisplay) timerDisplay.textContent = '00:00:00'; const playBtn = document.getElementById("play-btn"); if (playBtn) { playBtn.classList.remove("fa-pause"); playBtn.classList.add("fa-play"); } } export function rewindPlayback() { const previousStep = appState.currentStep; appState.currentStep = 0; if (!appState.isPlaying) { if (timerDisplay) timerDisplay.textContent = '00:00:00'; highlightStep(previousStep - 1, false); highlightStep(previousStep, false); } } export function togglePlayback() { initializeAudioContext(); // Garante que o contexto de áudio foi iniciado por um gesto do usuário if (appState.isPlaying) { // Pausa a reprodução, mas não reseta clearInterval(appState.playbackIntervalId); appState.playbackIntervalId = null; appState.isPlaying = false; document.getElementById("play-btn").classList.remove("fa-pause"); document.getElementById("play-btn").classList.add("fa-play"); } else { startPlayback(); } }