versão 2.0
This commit is contained in:
1 parent
d506332268
commit
fe735bf0a6
16 files changed
+1735
-1626
No files matched your search
+20
-302
@@ -1,316 +1,34 @@
|
||||
// js/audio.js
|
||||
import { appState } from "./state.js";
|
||||
import { highlightStep, updateAudioEditorUI, updatePlayheadVisual, resetPlayheadVisual } from "./ui.js";
|
||||
import { getTotalSteps } from "./utils.js";
|
||||
import { PIXELS_PER_STEP } from "./config.js";
|
||||
|
||||
let audioContext;
|
||||
let mainGainNode;
|
||||
let masterPannerNode;
|
||||
|
||||
const timerDisplay = document.getElementById('timer-display');
|
||||
// O contexto de áudio agora será gerenciado principalmente pelo Tone.js.
|
||||
// Esta função garante que ele seja iniciado por uma interação do usuário.
|
||||
export function initializeAudioContext() {
|
||||
if (Tone.context.state !== 'running') {
|
||||
Tone.start();
|
||||
console.log("AudioContext iniciado com Tone.js");
|
||||
}
|
||||
}
|
||||
|
||||
// Funções de acesso ao contexto global do Tone.js
|
||||
export function getAudioContext() {
|
||||
return audioContext;
|
||||
return Tone.context;
|
||||
}
|
||||
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();
|
||||
}
|
||||
return Tone.Destination;
|
||||
}
|
||||
|
||||
// Funções para controlar o volume e pan master
|
||||
export function updateMasterVolume(volume) {
|
||||
if (mainGainNode) {
|
||||
mainGainNode.gain.setValueAtTime(volume, audioContext.currentTime);
|
||||
// Tone.Destination.volume.value é em decibéis. Convertemos de linear (0-1.5) para dB.
|
||||
if (volume === 0) {
|
||||
Tone.Destination.volume.value = -Infinity;
|
||||
} else {
|
||||
Tone.Destination.volume.value = Tone.gainToDb(volume);
|
||||
}
|
||||
}
|
||||
|
||||
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(mainGainNode);
|
||||
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) {
|
||||
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 || !appState.isPlaying) {
|
||||
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 % (stepsPerBeat * 4) === 0);
|
||||
}
|
||||
}
|
||||
|
||||
appState.tracks.forEach((track) => {
|
||||
if (!track.patterns || track.patterns.length === 0) return;
|
||||
|
||||
const activePattern = track.patterns[appState.activePatternIndex];
|
||||
|
||||
if (activePattern && activePattern.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();
|
||||
|
||||
if (appState.currentStep === 0) {
|
||||
rewindPlayback();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
document.querySelectorAll('.step.playing').forEach(s => s.classList.remove('playing'));
|
||||
|
||||
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 lastStep = appState.currentStep > 0 ? appState.currentStep - 1 : getTotalSteps() - 1;
|
||||
appState.currentStep = 0;
|
||||
if (!appState.isPlaying) {
|
||||
if (timerDisplay) timerDisplay.textContent = '00:00:00';
|
||||
highlightStep(lastStep, false);
|
||||
}
|
||||
}
|
||||
|
||||
export function togglePlayback() {
|
||||
initializeAudioContext();
|
||||
if (appState.isPlaying) {
|
||||
stopPlayback();
|
||||
} else {
|
||||
appState.currentStep = 0;
|
||||
startPlayback();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function startAudioEditorPlayback() {
|
||||
if (appState.isAudioEditorPlaying || appState.audioTracks.length === 0) return;
|
||||
initializeAudioContext();
|
||||
|
||||
appState.isAudioEditorPlaying = true;
|
||||
appState.activeAudioSources = [];
|
||||
updateAudioEditorUI();
|
||||
|
||||
const startTime = audioContext.currentTime;
|
||||
appState.audioEditorStartTime = startTime;
|
||||
|
||||
appState.audioTracks.forEach(track => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
if (appState.activeAudioSources.length > 0) {
|
||||
if (appState.audioEditorAnimationId) {
|
||||
cancelAnimationFrame(appState.audioEditorAnimationId);
|
||||
}
|
||||
animationLoop();
|
||||
} else {
|
||||
appState.isAudioEditorPlaying = false;
|
||||
updateAudioEditorUI();
|
||||
}
|
||||
}
|
||||
|
||||
export function stopAudioEditorPlayback() {
|
||||
if (!appState.isAudioEditorPlaying) 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
|
||||
);
|
||||
|
||||
// --- 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();
|
||||
}
|
||||
|
||||
export function seekAudioEditor(newTime) {
|
||||
const wasPlaying = appState.isAudioEditorPlaying;
|
||||
if (wasPlaying) {
|
||||
stopAudioEditorPlayback();
|
||||
}
|
||||
appState.audioEditorPlaybackTime = newTime;
|
||||
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
||||
const stepsPerSecond = (bpm / 60) * 4;
|
||||
const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP;
|
||||
const newPositionPx = newTime * pixelsPerSecond;
|
||||
updatePlayheadVisual(newPositionPx);
|
||||
if (wasPlaying) {
|
||||
startAudioEditorPlayback();
|
||||
}
|
||||
}
|
||||
|
||||
export function restartAudioEditorIfPlaying() {
|
||||
if (appState.isAudioEditorPlaying) {
|
||||
stopAudioEditorPlayback();
|
||||
startAudioEditorPlayback();
|
||||
}
|
||||
// A panorimização master em Tone.js geralmente requer um nó Panner dedicado.
|
||||
// Por enquanto, esta função servirá como um placeholder para futuras implementações.
|
||||
console.log("Master Pan ainda não implementado com Tone.js");
|
||||
}
|
||||
Reference in new issue
Block a user