// js/pattern_audio.js import * as Tone from "https://esm.sh/tone"; import { appState } from "../state.js"; import { highlightStep } from "./pattern_ui.js"; import { getTotalSteps } from "../utils.js"; import { initializeAudioContext } from "../audio.js"; const timerDisplay = document.getElementById('timer-display'); 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 synth = new Tone.Synth().toDestination(); const freq = isDownbeat ? 1000 : 800; synth.triggerAttackRelease(freq, "8n", Tone.now()); } // Dispara o sample de uma track, garantindo que o player esteja roteado corretamente export function playSample(filePath, trackId) { initializeAudioContext(); const track = trackId ? appState.pattern.tracks.find((t) => t.id == trackId) : null; // Se a faixa existe e tem um player pré-carregado if (track && track.player) { if (track.player.loaded) { // Ajusta volume/pan sempre que tocar (robustez a alterações em tempo real) if (track.volumeNode) { track.volumeNode.volume.value = track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume); } if (track.pannerNode) { track.pannerNode.pan.value = track.pan ?? 0; } // Garante conexão: player -> volumeNode (não usar mais gainNode) try { track.player.disconnect(); } catch {} if (track.volumeNode) { track.player.connect(track.volumeNode); } // Dispara imediatamente track.player.start(Tone.now()); } else { console.warn(`Player da trilha "${track.name}" ainda não carregado — pulando este tick.`); } } // Fallback para preview de sample sem trackId else if (!trackId && filePath) { const previewPlayer = new Tone.Player(filePath).toDestination(); previewPlayer.autostart = true; } } function tick() { if (!appState.global.isPlaying) { stopPlayback(); return; } const totalSteps = getTotalSteps(); const lastStepIndex = appState.global.currentStep === 0 ? totalSteps - 1 : appState.global.currentStep - 1; highlightStep(lastStepIndex, false); const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120; const stepInterval = (60 * 1000) / (bpm * 4); const currentTime = appState.global.currentStep * stepInterval; if (timerDisplay) { timerDisplay.textContent = formatTime(currentTime); } // Metrônomo if (appState.global.metronomeEnabled) { const noteValue = parseInt(document.getElementById("compasso-b-input").value, 10) || 4; const stepsPerBeat = 16 / noteValue; if (appState.global.currentStep % stepsPerBeat === 0) { playMetronomeSound(appState.global.currentStep % (stepsPerBeat * 4) === 0); } } // Percorre tracks e toca o step atual se ativo appState.pattern.tracks.forEach((track) => { if (!track.patterns || track.patterns.length === 0) return; // IMPORTANTE: usar o pattern ativo da PRÓPRIA TRILHA const activePattern = track.patterns[track.activePatternIndex]; if (activePattern && activePattern.steps[appState.global.currentStep] && track.samplePath) { playSample(track.samplePath, track.id); } }); highlightStep(appState.global.currentStep, true); appState.global.currentStep = (appState.global.currentStep + 1) % totalSteps; } export function startPlayback() { if (appState.global.isPlaying || appState.pattern.tracks.length === 0) return; initializeAudioContext(); if (appState.global.currentStep === 0) { rewindPlayback(); } const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120; Tone.Transport.bpm.value = bpm; const stepInterval = (60 * 1000) / (bpm * 4); if (appState.global.playbackIntervalId) clearInterval(appState.global.playbackIntervalId); appState.global.isPlaying = true; const playBtn = document.getElementById("play-btn"); if (playBtn) { playBtn.classList.remove("fa-play"); playBtn.classList.add("fa-pause"); } tick(); appState.global.playbackIntervalId = setInterval(tick, stepInterval); } export function stopPlayback() { if (appState.global.playbackIntervalId) { clearInterval(appState.global.playbackIntervalId); } appState.global.playbackIntervalId = null; appState.global.isPlaying = false; document.querySelectorAll('.step.playing').forEach(s => s.classList.remove('playing')); appState.global.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 lastStep = appState.global.currentStep > 0 ? appState.global.currentStep - 1 : getTotalSteps() - 1; appState.global.currentStep = 0; if (!appState.global.isPlaying) { if (timerDisplay) timerDisplay.textContent = '00:00:00'; highlightStep(lastStep, false); } } export function togglePlayback() { initializeAudioContext(); if (appState.global.isPlaying) { stopPlayback(); } else { appState.global.currentStep = 0; startPlayback(); } }