167 lines
5.4 KiB
JavaScript
167 lines
5.4 KiB
JavaScript
// 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();
|
|
}
|
|
}
|