//TODO ver Tone.js // js/audio.js import { appState } from "./state.js"; import { highlightStep } from "./ui.js"; import { getTotalSteps } from "./utils.js"; let audioContext; let mainGainNode; export function getAudioContext() { return audioContext; } export function getMainGainNode() { return mainGainNode; } export function initializeAudioContext() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); mainGainNode = audioContext.createGain(); mainGainNode.connect(audioContext.destination); } if (audioContext.state === "suspended") { audioContext.resume(); } } 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; const audio = new Audio(filePath); const source = audioContext.createMediaElementSource(audio); if (track && track.gainNode) { source.connect(track.gainNode); } else { source.connect(mainGainNode); } audio.play(); } function tick() { const totalSteps = getTotalSteps(); if (totalSteps === 0) { stopPlayback(); return; } const lastStepIndex = appState.currentStep === 0 ? totalSteps - 1 : appState.currentStep - 1; highlightStep(lastStepIndex, false); 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); } } appState.tracks.forEach((track) => { if (track.steps[appState.currentStep] && track.samplePath) { playSample(track.samplePath, track.id); } }); 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() { clearInterval(appState.playbackIntervalId); appState.playbackIntervalId = null; appState.isPlaying = false; highlightStep(appState.currentStep - 1, false); appState.currentStep = 0; document.getElementById("play-btn").classList.remove("fa-pause"); document.getElementById("play-btn").classList.add("fa-play"); } export function rewindPlayback() { appState.currentStep = 0; if (!appState.isPlaying) { document .querySelectorAll(".step.playing") .forEach((s) => s.classList.remove("playing")); } } export function togglePlayback() { if (appState.isPlaying) { stopPlayback(); } else { startPlayback(); } }