versão 2.0.1 - Correção do grid (Editor de Samples), loop, delete, resize, trim e streching funcionais
Deploy / Deploy (push) Successful in 1m2s
Details
Deploy / Deploy (push) Successful in 1m2s
Details
This commit is contained in:
parent
dc32ba2225
commit
6903839643
|
|
@ -246,6 +246,7 @@ body.sidebar-hidden .sample-browser {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
/* Estas variáveis são agora definidas dinamicamente por audio_ui.js */
|
||||||
--step-width: 32px;
|
--step-width: 32px;
|
||||||
--beat-width: 128px;
|
--beat-width: 128px;
|
||||||
--bar-width: 512px;
|
--bar-width: 512px;
|
||||||
|
|
@ -305,7 +306,8 @@ body.sidebar-hidden .sample-browser {
|
||||||
box-shadow: 0 3px 8px rgba(0,0,0,0.5);
|
box-shadow: 0 3px 8px rgba(0,0,0,0.5);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 8px;
|
/* --- CORREÇÃO: Remove o padding horizontal --- */
|
||||||
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
@ -342,13 +344,22 @@ body.sidebar-hidden .sample-browser {
|
||||||
.loop-handle { position: absolute; top: 0; bottom: 0; width: 10px; cursor: ew-resize; }
|
.loop-handle { position: absolute; top: 0; bottom: 0; width: 10px; cursor: ew-resize; }
|
||||||
.loop-handle.left { left: -5px; }
|
.loop-handle.left { left: -5px; }
|
||||||
.loop-handle.right { right: -5px; }
|
.loop-handle.right { right: -5px; }
|
||||||
#slice-tool-btn.active { color: var(--accent-blue); }
|
|
||||||
|
/* --- ESTILOS ADICIONADOS --- */
|
||||||
|
#slice-tool-btn.active,
|
||||||
|
#resize-tool-trim.active,
|
||||||
|
#resize-tool-stretch.active {
|
||||||
|
color: var(--accent-blue);
|
||||||
|
background-color: var(--bg-editor);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
#audio-editor-loop-btn.active { color: var(--accent-green); }
|
#audio-editor-loop-btn.active { color: var(--accent-green); }
|
||||||
|
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
/* COMPONENTES GERAIS (KNOBS, BOTÕES, INPUTS)
|
/* COMPONENTES GERAIS (KNOBS, BOTÕES, INPUTS)
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
.knob-container { text-align: center; font-size: .7rem; color: var(--text-dark); }
|
.knob-container { text-align: center; font-size: .7rem; color: var(--text-dark); }
|
||||||
|
.knob-container { text-align: center; font-size: .7rem; color: var(--text-dark); }
|
||||||
.knob { width: 28px; height: 28px; background-color: var(--bg-toolbar); border-radius: 50%; border: 1px solid var(--border-color); margin-bottom: 2px; cursor: grab; box-shadow: inset 0 0 4px #222; position: relative; }
|
.knob { width: 28px; height: 28px; background-color: var(--bg-toolbar); border-radius: 50%; border: 1px solid var(--border-color); margin-bottom: 2px; cursor: grab; box-shadow: inset 0 0 4px #222; position: relative; }
|
||||||
.knob:active { cursor: grabbing; }
|
.knob:active { cursor: grabbing; }
|
||||||
.knob-indicator { width: 2px; height: 8px; background-color: var(--text-light); position: absolute; top: 2px; left: 50%; transform-origin: bottom center; transform: translateX(-50%) rotate(0deg); border-radius: 1px; }
|
.knob-indicator { width: 2px; height: 8px; background-color: var(--text-light); position: absolute; top: 2px; left: 50%; transform-origin: bottom center; transform: translateX(-50%) rotate(0deg); border-radius: 1px; }
|
||||||
|
|
|
||||||
|
|
@ -1,154 +1,304 @@
|
||||||
// js/audio_audio.js
|
// js/audio/audio_audio.js
|
||||||
import { appState } from "../state.js";
|
import { appState } from "../state.js";
|
||||||
import { updateAudioEditorUI, updatePlayheadVisual, resetPlayheadVisual } from "./audio_ui.js";
|
import { updateAudioEditorUI, updatePlayheadVisual, resetPlayheadVisual } from "./audio_ui.js";
|
||||||
import { PIXELS_PER_STEP } from "../config.js";
|
import { initializeAudioContext, getAudioContext } from "../audio.js";
|
||||||
import { initializeAudioContext } from "../audio.js";
|
|
||||||
import { getPixelsPerSecond } from "../utils.js";
|
import { getPixelsPerSecond } from "../utils.js";
|
||||||
|
|
||||||
function animationLoop() {
|
// --- Configurações do Scheduler ---
|
||||||
if (!appState.global.isAudioEditorPlaying) return;
|
const LOOKAHEAD_INTERVAL_MS = 25.0;
|
||||||
|
const SCHEDULE_AHEAD_TIME_SEC = 0.5; // 500ms
|
||||||
|
|
||||||
const pixelsPerSecond = getPixelsPerSecond();
|
// --- Estado Interno do Engine ---
|
||||||
const totalElapsedTime = Tone.Transport.seconds;
|
let audioCtx = null;
|
||||||
|
let isPlaying = false;
|
||||||
|
let schedulerIntervalId = null;
|
||||||
|
let animationFrameId = null;
|
||||||
|
|
||||||
|
// Sincronização de Tempo
|
||||||
|
let startTime = 0;
|
||||||
|
let seekTime = 0;
|
||||||
|
let logicalPlaybackTime = 0;
|
||||||
|
|
||||||
|
// Configurações de Loop
|
||||||
|
let isLoopActive = false;
|
||||||
|
let loopStartTimeSec = 0;
|
||||||
|
let loopEndTimeSec = 8;
|
||||||
|
|
||||||
|
const runtimeClipState = new Map();
|
||||||
|
const scheduledNodes = new Map();
|
||||||
|
let nextEventId = 0;
|
||||||
|
|
||||||
|
const callbacks = {
|
||||||
|
onClipScheduled: null,
|
||||||
|
onClipPlayed: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Funções Auxiliares de Tempo (sem alterações) ---
|
||||||
|
function _getBpm() {
|
||||||
|
const bpmInput = document.getElementById("bpm-input");
|
||||||
|
return parseFloat(bpmInput.value) || 120;
|
||||||
|
}
|
||||||
|
function _getSecondsPerBeat() { return 60.0 / _getBpm(); }
|
||||||
|
function _convertBeatToSeconds(beat) { return beat * _getSecondsPerBeat(); }
|
||||||
|
function _convertSecondsToBeat(seconds) { return seconds / _getSecondsPerBeat(); }
|
||||||
|
|
||||||
|
|
||||||
|
function _initContext() {
|
||||||
|
if (!audioCtx) {
|
||||||
|
initializeAudioContext();
|
||||||
|
audioCtx = getAudioContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Lógica Principal do Scheduler (sem alterações) ---
|
||||||
|
|
||||||
|
function _scheduleClip(clip, absolutePlayTime, durationSec) {
|
||||||
|
if (!clip.buffer) {
|
||||||
|
console.warn(`Clip ${clip.id} não possui áudio buffer carregado.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!clip.gainNode || !clip.pannerNode) {
|
||||||
|
console.warn(`Clip ${clip.id} não possui gainNode ou pannerNode.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = new Tone.BufferSource(clip.buffer);
|
||||||
|
source.connect(clip.gainNode);
|
||||||
|
|
||||||
|
// --- CORREÇÃO: Aplica o pitch (que pode ser de stretch ou wheel) ---
|
||||||
|
if (clip.pitch && clip.pitch !== 0) {
|
||||||
|
source.playbackRate.value = Math.pow(2, clip.pitch / 12);
|
||||||
|
} else {
|
||||||
|
source.playbackRate.value = 1.0; // Garante que o modo 'trim' toque normal
|
||||||
|
}
|
||||||
|
// --- FIM DA CORREÇÃO ---
|
||||||
|
|
||||||
|
const eventId = nextEventId++;
|
||||||
|
const clipOffset = clip.offsetInSeconds || clip.offset || 0;
|
||||||
|
source.start(absolutePlayTime, clipOffset, durationSec);
|
||||||
|
scheduledNodes.set(eventId, { sourceNode: source, clipId: clip.id });
|
||||||
|
|
||||||
|
if (callbacks.onClipScheduled) {
|
||||||
|
callbacks.onClipScheduled(clip);
|
||||||
|
}
|
||||||
|
|
||||||
|
source.onended = () => {
|
||||||
|
_handleClipEnd(eventId, clip.id);
|
||||||
|
source.dispose();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _handleClipEnd(eventId, clipId) {
|
||||||
|
scheduledNodes.delete(eventId);
|
||||||
|
runtimeClipState.delete(clipId);
|
||||||
|
|
||||||
|
if (callbacks.onClipPlayed) {
|
||||||
|
const clip = appState.audio.clips.find(c => c.id == clipId);
|
||||||
|
if(clip) callbacks.onClipPlayed(clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _schedulerTick() {
|
||||||
|
if (!isPlaying || !audioCtx) return;
|
||||||
|
|
||||||
|
const now = audioCtx.currentTime;
|
||||||
|
const logicalTime = (now - startTime) + seekTime;
|
||||||
|
const scheduleWindowStartSec = logicalTime;
|
||||||
|
const scheduleWindowEndSec = logicalTime + SCHEDULE_AHEAD_TIME_SEC;
|
||||||
|
|
||||||
|
for (const clip of appState.audio.clips) {
|
||||||
|
const clipRuntime = runtimeClipState.get(clip.id) || { isScheduled: false };
|
||||||
|
|
||||||
|
if (clipRuntime.isScheduled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!clip.buffer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipStartTimeSec = clip.startTimeInSeconds;
|
||||||
|
const clipDurationSec = clip.durationInSeconds;
|
||||||
|
|
||||||
|
if (typeof clipStartTimeSec === 'undefined' || typeof clipDurationSec === 'undefined') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let occurrenceStartTimeSec = clipStartTimeSec;
|
||||||
|
|
||||||
|
if (isLoopActive) {
|
||||||
|
const loopDuration = loopEndTimeSec - loopStartTimeSec;
|
||||||
|
if (loopDuration <= 0) continue;
|
||||||
|
if (occurrenceStartTimeSec < loopStartTimeSec && logicalTime >= loopStartTimeSec) {
|
||||||
|
const offsetFromLoopStart = (occurrenceStartTimeSec - loopStartTimeSec) % loopDuration;
|
||||||
|
occurrenceStartTimeSec = loopStartTimeSec + (offsetFromLoopStart < 0 ? offsetFromLoopStart + loopDuration : offsetFromLoopStart);
|
||||||
|
}
|
||||||
|
if (occurrenceStartTimeSec < logicalTime) {
|
||||||
|
const loopsMissed = Math.floor((logicalTime - occurrenceStartTimeSec) / loopDuration) + 1;
|
||||||
|
occurrenceStartTimeSec += loopsMissed * loopDuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
occurrenceStartTimeSec >= scheduleWindowStartSec &&
|
||||||
|
occurrenceStartTimeSec < scheduleWindowEndSec
|
||||||
|
) {
|
||||||
|
const absolutePlayTime = startTime + (occurrenceStartTimeSec - seekTime);
|
||||||
|
_scheduleClip(clip, absolutePlayTime, clipDurationSec);
|
||||||
|
clipRuntime.isScheduled = true;
|
||||||
|
runtimeClipState.set(clip.id, clipRuntime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Loop de Animação (sem alterações) ---
|
||||||
|
function _animationLoop() {
|
||||||
|
if (!isPlaying) {
|
||||||
|
animationFrameId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = audioCtx.currentTime;
|
||||||
|
let newLogicalTime = (now - startTime) + seekTime;
|
||||||
|
if (isLoopActive) {
|
||||||
|
if (newLogicalTime >= loopEndTimeSec) {
|
||||||
|
const loopDuration = loopEndTimeSec - loopStartTimeSec;
|
||||||
|
newLogicalTime = loopStartTimeSec + ((newLogicalTime - loopStartTimeSec) % loopDuration);
|
||||||
|
startTime = now;
|
||||||
|
seekTime = newLogicalTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logicalPlaybackTime = newLogicalTime;
|
||||||
|
if (!isLoopActive) {
|
||||||
let maxTime = 0;
|
let maxTime = 0;
|
||||||
appState.audio.clips.forEach(clip => {
|
appState.audio.clips.forEach(clip => {
|
||||||
const endTime = clip.startTime + clip.duration;
|
const clipStartTime = clip.startTimeInSeconds || 0;
|
||||||
|
const clipDuration = clip.durationInSeconds || 0;
|
||||||
|
const endTime = clipStartTime + clipDuration;
|
||||||
if (endTime > maxTime) maxTime = endTime;
|
if (endTime > maxTime) maxTime = endTime;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!appState.global.isLoopActive && totalElapsedTime >= maxTime && maxTime > 0) {
|
if (maxTime > 0 && logicalPlaybackTime >= maxTime) {
|
||||||
stopAudioEditorPlayback();
|
stopAudioEditorPlayback(true); // Rebobina no fim
|
||||||
resetPlayheadVisual();
|
resetPlayheadVisual();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const newPositionPx = totalElapsedTime * pixelsPerSecond;
|
const pixelsPerSecond = getPixelsPerSecond();
|
||||||
updatePlayheadVisual(newPositionPx);
|
const newPositionPx = logicalPlaybackTime * pixelsPerSecond;
|
||||||
|
updatePlayheadVisual(newPositionPx);
|
||||||
// ##### CORREÇÃO 1 #####
|
animationFrameId = requestAnimationFrame(_animationLoop);
|
||||||
// Salva o ID da animação para que o stop possa cancelá-lo
|
|
||||||
appState.audio.audioEditorAnimationId = requestAnimationFrame(animationLoop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- API Pública ---
|
||||||
|
|
||||||
export function updateTransportLoop() {
|
export function updateTransportLoop() {
|
||||||
Tone.Transport.loop = appState.global.isLoopActive;
|
isLoopActive = appState.global.isLoopActive;
|
||||||
Tone.Transport.loopStart = appState.global.loopStartTime;
|
loopStartTimeSec = appState.global.loopStartTime;
|
||||||
Tone.Transport.loopEnd = appState.global.loopEndTime;
|
loopEndTimeSec = appState.global.loopEndTime;
|
||||||
|
|
||||||
|
runtimeClipState.clear();
|
||||||
|
|
||||||
|
scheduledNodes.forEach(nodeData => {
|
||||||
|
// --- CORREÇÃO BUG 1: Remove a linha 'onended = null' ---
|
||||||
|
nodeData.sourceNode.stop(0);
|
||||||
|
nodeData.sourceNode.dispose();
|
||||||
|
});
|
||||||
|
scheduledNodes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startAudioEditorPlayback() {
|
export function startAudioEditorPlayback() {
|
||||||
if (appState.global.isAudioEditorPlaying) return;
|
if (isPlaying) return;
|
||||||
initializeAudioContext();
|
|
||||||
Tone.Transport.cancel(); // Limpa eventos agendados anteriormente
|
|
||||||
|
|
||||||
updateTransportLoop(); // Isso deve definir Tone.Transport.loop = true e Tone.Transport.loopEnd
|
_initContext();
|
||||||
|
if (audioCtx.state === 'suspended') {
|
||||||
// 1. Pegue a duração total do loop que a função acima definiu
|
audioCtx.resume();
|
||||||
const loopInterval = Tone.Transport.loopEnd;
|
|
||||||
|
|
||||||
// Se loopEnd não foi definido (ex: 0 ou undefined), o loop não funcionará.
|
|
||||||
if (!loopInterval || loopInterval === 0) {
|
|
||||||
console.error("LoopEnd não está definido no Tone.Transport! O áudio não repetirá.");
|
|
||||||
// Você pode querer definir um padrão aqui, mas o ideal é
|
|
||||||
// garantir que 'updateTransportLoop' esteja definindo 'loopEnd' corretamente.
|
|
||||||
// ex: const loopInterval = "1m"; (se for um compasso por padrão)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
appState.audio.clips.forEach(clip => {
|
isPlaying = true;
|
||||||
if (!clip.player || !clip.player.loaded) return;
|
// --- CORREÇÃO BUG 2: Atualiza o estado global ---
|
||||||
|
|
||||||
// 2. CORREÇÃO: Use scheduleRepeat no lugar de scheduleOnce
|
|
||||||
Tone.Transport.scheduleRepeat((time) => {
|
|
||||||
// Sua lógica de parâmetros está correta
|
|
||||||
clip.gainNode.gain.value = Tone.gainToDb(clip.volume);
|
|
||||||
clip.pannerNode.pan.value = clip.pan;
|
|
||||||
clip.player.playbackRate = Math.pow(2, clip.pitch / 12);
|
|
||||||
|
|
||||||
// Inicia o player no tempo agendado
|
|
||||||
clip.player.start(time, clip.offset, clip.duration);
|
|
||||||
|
|
||||||
},
|
|
||||||
loopInterval, // <--- O intervalo de repetição (ex: "4m", "8m")
|
|
||||||
clip.startTime // <--- Onde o clip começa dentro da linha do tempo
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. ADIÇÃO CRÍTICA: Inicie o transporte e atualize o estado
|
|
||||||
Tone.Transport.start();
|
|
||||||
appState.global.isAudioEditorPlaying = true;
|
appState.global.isAudioEditorPlaying = true;
|
||||||
|
|
||||||
// 4. (CORRIGIDO) Atualize a UI do botão de play
|
startTime = audioCtx.currentTime;
|
||||||
const playBtn = document.getElementById("audio-editor-play-btn");
|
|
||||||
if (playBtn) {
|
|
||||||
playBtn.classList.add("active");
|
|
||||||
// Verifica se o ícone existe antes de tentar mudá-lo
|
|
||||||
const icon = playBtn.querySelector('i');
|
|
||||||
if (icon) {
|
|
||||||
icon.className = 'fa-solid fa-pause';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ##### CORREÇÃO 2 #####
|
updateTransportLoop();
|
||||||
// Inicia o loop de animação da agulha
|
|
||||||
animationLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stopAudioEditorPlayback() {
|
console.log("%cIniciando Playback...", "color: #3498db;");
|
||||||
if (!appState.global.isAudioEditorPlaying) return;
|
|
||||||
Tone.Transport.stop();
|
|
||||||
|
|
||||||
appState.audio.clips.forEach(clip => {
|
_schedulerTick();
|
||||||
if (clip.player && clip.player.state === 'started') {
|
schedulerIntervalId = setInterval(_schedulerTick, LOOKAHEAD_INTERVAL_MS);
|
||||||
clip.player.stop();
|
animationFrameId = requestAnimationFrame(_animationLoop);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
appState.audio.audioEditorPlaybackTime = Tone.Transport.seconds;
|
|
||||||
|
|
||||||
// Esta lógica agora funcionará corretamente graças à Correção 1
|
|
||||||
if (appState.audio.audioEditorAnimationId) {
|
|
||||||
cancelAnimationFrame(appState.audio.audioEditorAnimationId);
|
|
||||||
appState.audio.audioEditorAnimationId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// (CORRIGIDO) Atualiza a UI do botão de play
|
|
||||||
const playBtn = document.getElementById("audio-editor-play-btn");
|
|
||||||
if (playBtn) {
|
|
||||||
playBtn.classList.remove("active");
|
|
||||||
// Verifica se o ícone existe antes de tentar mudá-lo
|
|
||||||
const icon = playBtn.querySelector('i');
|
|
||||||
if (icon) {
|
|
||||||
icon.className = 'fa-solid fa-play'; // Muda de volta para "play"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
appState.global.isAudioEditorPlaying = false;
|
|
||||||
updateAudioEditorUI();
|
updateAudioEditorUI();
|
||||||
|
const playBtn = document.getElementById("audio-editor-play-btn");
|
||||||
|
if (playBtn) {
|
||||||
|
playBtn.className = 'fa-solid fa-pause';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function seekAudioEditor(newTime) {
|
export function stopAudioEditorPlayback(rewind = false) {
|
||||||
const wasPlaying = appState.global.isAudioEditorPlaying;
|
if (!isPlaying) return;
|
||||||
if (wasPlaying) {
|
|
||||||
stopAudioEditorPlayback();
|
|
||||||
}
|
|
||||||
|
|
||||||
appState.audio.audioEditorPlaybackTime = newTime;
|
isPlaying = false;
|
||||||
Tone.Transport.seconds = newTime;
|
// --- CORREÇÃO BUG 2: Atualiza o estado global ---
|
||||||
|
appState.global.isAudioEditorPlaying = false;
|
||||||
|
|
||||||
const pixelsPerSecond = getPixelsPerSecond();
|
console.log(`%cParando Playback... (Rewind: ${rewind})`, "color: #d9534f;");
|
||||||
const newPositionPx = newTime * pixelsPerSecond;
|
|
||||||
updatePlayheadVisual(newPositionPx);
|
|
||||||
|
|
||||||
if (wasPlaying) {
|
clearInterval(schedulerIntervalId);
|
||||||
startAudioEditorPlayback();
|
schedulerIntervalId = null;
|
||||||
}
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
animationFrameId = null;
|
||||||
|
|
||||||
|
seekTime = logicalPlaybackTime;
|
||||||
|
logicalPlaybackTime = 0;
|
||||||
|
|
||||||
|
if (rewind) {
|
||||||
|
seekTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledNodes.forEach(nodeData => {
|
||||||
|
// --- CORREÇÃO BUG 1: Remove a linha 'onended = null' ---
|
||||||
|
nodeData.sourceNode.stop(0);
|
||||||
|
nodeData.sourceNode.dispose();
|
||||||
|
});
|
||||||
|
scheduledNodes.clear();
|
||||||
|
runtimeClipState.clear();
|
||||||
|
|
||||||
|
updateAudioEditorUI();
|
||||||
|
const playBtn = document.getElementById("audio-editor-play-btn");
|
||||||
|
if (playBtn) {
|
||||||
|
playBtn.className = 'fa-solid fa-play';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rewind) {
|
||||||
|
resetPlayheadVisual();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restartAudioEditorIfPlaying() {
|
export function restartAudioEditorIfPlaying() {
|
||||||
if (appState.global.isAudioEditorPlaying) {
|
if (isPlaying) {
|
||||||
appState.audio.audioEditorPlaybackTime = Tone.Transport.seconds;
|
stopAudioEditorPlayback(false); // Pausa
|
||||||
stopAudioEditorPlayback();
|
startAudioEditorPlayback();
|
||||||
startAudioEditorPlayback();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function seekAudioEditor(newTime) {
|
||||||
|
const wasPlaying = isPlaying;
|
||||||
|
if (wasPlaying) {
|
||||||
|
stopAudioEditorPlayback(false); // Pausa
|
||||||
|
}
|
||||||
|
seekTime = newTime;
|
||||||
|
logicalPlaybackTime = newTime;
|
||||||
|
const pixelsPerSecond = getPixelsPerSecond();
|
||||||
|
const newPositionPx = newTime * pixelsPerSecond;
|
||||||
|
updatePlayheadVisual(newPositionPx);
|
||||||
|
if (wasPlaying) {
|
||||||
|
startAudioEditorPlayback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerCallbacks(newCallbacks) {
|
||||||
|
if (newCallbacks.onClipScheduled) {
|
||||||
|
callbacks.onClipScheduled = newCallbacks.onClipScheduled;
|
||||||
|
}
|
||||||
|
if (newCallbacks.onClipPlayed) {
|
||||||
|
callbacks.onClipPlayed = newCallbacks.onClipPlayed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
import { DEFAULT_VOLUME, DEFAULT_PAN } from "../config.js";
|
import { DEFAULT_VOLUME, DEFAULT_PAN } from "../config.js";
|
||||||
import { renderAudioEditor } from "./audio_ui.js";
|
import { renderAudioEditor } from "./audio_ui.js";
|
||||||
import { getMainGainNode } from "../audio.js";
|
import { getMainGainNode } from "../audio.js";
|
||||||
|
import { getAudioContext } from "../audio.js";
|
||||||
|
|
||||||
const initialState = {
|
export let audioState = {
|
||||||
tracks: [],
|
tracks: [],
|
||||||
clips: [],
|
clips: [],
|
||||||
audioEditorStartTime: 0,
|
audioEditorStartTime: 0,
|
||||||
|
|
@ -12,33 +13,47 @@ const initialState = {
|
||||||
isAudioEditorLoopEnabled: false,
|
isAudioEditorLoopEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export let audioState = { ...initialState };
|
|
||||||
|
|
||||||
export function initializeAudioState() {
|
export function initializeAudioState() {
|
||||||
audioState.clips.forEach(clip => {
|
audioState.clips.forEach(clip => {
|
||||||
if (clip.player) clip.player.dispose();
|
|
||||||
if (clip.pannerNode) clip.pannerNode.dispose();
|
if (clip.pannerNode) clip.pannerNode.dispose();
|
||||||
if (clip.gainNode) clip.gainNode.dispose();
|
if (clip.gainNode) clip.gainNode.dispose();
|
||||||
});
|
});
|
||||||
Object.assign(audioState, initialState, { tracks: [], clips: [] });
|
Object.assign(audioState, {
|
||||||
|
tracks: [],
|
||||||
|
clips: [],
|
||||||
|
audioEditorStartTime: 0,
|
||||||
|
audioEditorAnimationId: null,
|
||||||
|
audioEditorPlaybackTime: 0,
|
||||||
|
isAudioEditorLoopEnabled: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadAudioForClip(clip) {
|
export async function loadAudioForClip(clip) {
|
||||||
if (!clip.sourcePath) return clip;
|
if (!clip.sourcePath) return clip;
|
||||||
|
|
||||||
|
const audioCtx = getAudioContext();
|
||||||
|
if (!audioCtx) {
|
||||||
|
console.error("AudioContext não disponível para carregar áudio.");
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Cria o player e o conecta à cadeia de áudio do clipe
|
const response = await fetch(clip.sourcePath);
|
||||||
clip.player = new Tone.Player();
|
if (!response.ok) throw new Error(`Falha ao buscar áudio: ${clip.sourcePath}`);
|
||||||
clip.player.chain(clip.gainNode, clip.pannerNode, getMainGainNode());
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
|
||||||
|
|
||||||
// Carrega o áudio e espera a conclusão
|
clip.buffer = audioBuffer;
|
||||||
await clip.player.load(clip.sourcePath);
|
|
||||||
|
|
||||||
if (clip.duration === 0) {
|
// --- CORREÇÃO: Salva a duração original ---
|
||||||
clip.duration = clip.player.buffer.duration;
|
if (clip.durationInSeconds === 0) {
|
||||||
|
clip.durationInSeconds = audioBuffer.duration;
|
||||||
}
|
}
|
||||||
|
// Salva a duração real do buffer para cálculos de stretch
|
||||||
|
clip.originalDuration = audioBuffer.duration;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Falha ao carregar áudio para o clipe ${clip.name}:`, error);
|
console.error(`Falha ao carregar áudio para o clipe ${clip.name}:`, error);
|
||||||
clip.player = null;
|
|
||||||
}
|
}
|
||||||
return clip;
|
return clip;
|
||||||
}
|
}
|
||||||
|
|
@ -49,19 +64,26 @@ export function addAudioClipToTimeline(samplePath, trackId = 1, startTime = 0) {
|
||||||
trackId: trackId,
|
trackId: trackId,
|
||||||
sourcePath: samplePath,
|
sourcePath: samplePath,
|
||||||
name: samplePath.split('/').pop(),
|
name: samplePath.split('/').pop(),
|
||||||
player: null,
|
|
||||||
startTime: startTime,
|
startTimeInSeconds: startTime,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
duration: 0,
|
durationInSeconds: 0,
|
||||||
|
originalDuration: 0, // Será preenchido pelo loadAudioForClip
|
||||||
|
|
||||||
pitch: 0,
|
pitch: 0,
|
||||||
volume: DEFAULT_VOLUME,
|
volume: DEFAULT_VOLUME,
|
||||||
pan: DEFAULT_PAN,
|
pan: DEFAULT_PAN,
|
||||||
isSoloed: true,
|
|
||||||
// --- ADICIONADO: Nós de áudio para cada clipe ---
|
|
||||||
gainNode: new Tone.Gain(Tone.gainToDb(DEFAULT_VOLUME)),
|
gainNode: new Tone.Gain(Tone.gainToDb(DEFAULT_VOLUME)),
|
||||||
pannerNode: new Tone.Panner(DEFAULT_PAN),
|
pannerNode: new Tone.Panner(DEFAULT_PAN),
|
||||||
|
|
||||||
|
buffer: null,
|
||||||
|
player: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
newClip.gainNode.connect(newClip.pannerNode);
|
||||||
|
newClip.pannerNode.connect(getMainGainNode());
|
||||||
|
|
||||||
audioState.clips.push(newClip);
|
audioState.clips.push(newClip);
|
||||||
|
|
||||||
loadAudioForClip(newClip).then(() => {
|
loadAudioForClip(newClip).then(() => {
|
||||||
|
|
@ -78,35 +100,52 @@ export function updateAudioClipProperties(clipId, properties) {
|
||||||
|
|
||||||
export function sliceAudioClip(clipId, sliceTimeInTimeline) {
|
export function sliceAudioClip(clipId, sliceTimeInTimeline) {
|
||||||
const originalClip = audioState.clips.find(c => c.id == clipId);
|
const originalClip = audioState.clips.find(c => c.id == clipId);
|
||||||
if (!originalClip || sliceTimeInTimeline <= originalClip.startTime || sliceTimeInTimeline >= originalClip.startTime + originalClip.duration) {
|
|
||||||
|
if (!originalClip ||
|
||||||
|
sliceTimeInTimeline <= originalClip.startTimeInSeconds ||
|
||||||
|
sliceTimeInTimeline >= (originalClip.startTimeInSeconds + originalClip.durationInSeconds)) {
|
||||||
|
console.warn("Corte inválido: fora dos limites do clipe.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cutPointInClip = sliceTimeInTimeline - originalClip.startTime;
|
const originalOffset = originalClip.offset || 0;
|
||||||
|
const cutPointInClip = sliceTimeInTimeline - originalClip.startTimeInSeconds;
|
||||||
|
|
||||||
const newClip = {
|
const newClip = {
|
||||||
id: Date.now() + Math.random(),
|
id: Date.now() + Math.random(),
|
||||||
trackId: originalClip.trackId,
|
trackId: originalClip.trackId,
|
||||||
sourcePath: originalClip.sourcePath,
|
sourcePath: originalClip.sourcePath,
|
||||||
name: originalClip.name,
|
name: originalClip.name,
|
||||||
player: originalClip.player,
|
buffer: originalClip.buffer,
|
||||||
startTime: sliceTimeInTimeline,
|
|
||||||
offset: originalClip.offset + cutPointInClip,
|
startTimeInSeconds: sliceTimeInTimeline,
|
||||||
duration: originalClip.duration - cutPointInClip,
|
offset: originalOffset + cutPointInClip,
|
||||||
|
durationInSeconds: originalClip.durationInSeconds - cutPointInClip,
|
||||||
|
|
||||||
|
// --- CORREÇÃO: Propaga a duração original ---
|
||||||
|
originalDuration: originalClip.originalDuration,
|
||||||
|
|
||||||
pitch: originalClip.pitch,
|
pitch: originalClip.pitch,
|
||||||
volume: originalClip.volume,
|
volume: originalClip.volume,
|
||||||
pan: originalClip.pan,
|
pan: originalClip.pan,
|
||||||
isSoloed: false,
|
|
||||||
gainNode: new Tone.Gain(Tone.gainToDb(originalClip.volume)),
|
gainNode: new Tone.Gain(Tone.gainToDb(originalClip.volume)),
|
||||||
pannerNode: new Tone.Panner(originalClip.pan),
|
pannerNode: new Tone.Panner(originalClip.pan),
|
||||||
|
|
||||||
|
player: null
|
||||||
};
|
};
|
||||||
newClip.player.chain(newClip.gainNode, newClip.pannerNode, getMainGainNode());
|
|
||||||
|
|
||||||
|
newClip.gainNode.connect(newClip.pannerNode);
|
||||||
|
newClip.pannerNode.connect(getMainGainNode());
|
||||||
|
|
||||||
|
originalClip.durationInSeconds = cutPointInClip;
|
||||||
|
|
||||||
originalClip.duration = cutPointInClip;
|
|
||||||
audioState.clips.push(newClip);
|
audioState.clips.push(newClip);
|
||||||
|
|
||||||
|
console.log("Clipe dividido. Original:", originalClip, "Novo:", newClip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... (resto do arquivo 'audio_state.js' sem alterações) ...
|
||||||
export function updateClipVolume(clipId, volume) {
|
export function updateClipVolume(clipId, volume) {
|
||||||
const clip = audioState.clips.find((c) => c.id == clipId);
|
const clip = audioState.clips.find((c) => c.id == clipId);
|
||||||
if (clip) {
|
if (clip) {
|
||||||
|
|
@ -132,5 +171,27 @@ export function updateClipPan(clipId, pan) {
|
||||||
export function addAudioTrackLane() {
|
export function addAudioTrackLane() {
|
||||||
const newTrackName = `Pista de Áudio ${audioState.tracks.length + 1}`;
|
const newTrackName = `Pista de Áudio ${audioState.tracks.length + 1}`;
|
||||||
audioState.tracks.push({ id: Date.now(), name: newTrackName });
|
audioState.tracks.push({ id: Date.now(), name: newTrackName });
|
||||||
// A UI será re-renderizada a partir do main.js
|
}
|
||||||
|
|
||||||
|
export function removeAudioClip(clipId) {
|
||||||
|
const clipIndex = audioState.clips.findIndex(c => c.id == clipId);
|
||||||
|
if (clipIndex === -1) return false; // Retorna false se não encontrou
|
||||||
|
|
||||||
|
const clip = audioState.clips[clipIndex];
|
||||||
|
|
||||||
|
// 1. Limpa os nós de áudio do Tone.js
|
||||||
|
if (clip.gainNode) {
|
||||||
|
clip.gainNode.disconnect();
|
||||||
|
clip.gainNode.dispose();
|
||||||
|
}
|
||||||
|
if (clip.pannerNode) {
|
||||||
|
clip.pannerNode.disconnect();
|
||||||
|
clip.pannerNode.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Remove o clipe do array de estado
|
||||||
|
audioState.clips.splice(clipIndex, 1);
|
||||||
|
|
||||||
|
// 3. Retorna true para o chamador (Controller)
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -7,15 +7,15 @@ import {
|
||||||
} from "./audio_state.js";
|
} from "./audio_state.js";
|
||||||
import { seekAudioEditor, restartAudioEditorIfPlaying, updateTransportLoop } from "./audio_audio.js";
|
import { seekAudioEditor, restartAudioEditorIfPlaying, updateTransportLoop } from "./audio_audio.js";
|
||||||
import { drawWaveform } from "../waveform.js";
|
import { drawWaveform } from "../waveform.js";
|
||||||
import { PIXELS_PER_BAR, ZOOM_LEVELS } from "../config.js";
|
import { PIXELS_PER_BAR, PIXELS_PER_STEP, ZOOM_LEVELS } from "../config.js";
|
||||||
import { getPixelsPerSecond } from "../utils.js";
|
import { getPixelsPerSecond, quantizeTime, getBeatsPerBar, getSecondsPerStep } from "../utils.js";
|
||||||
|
|
||||||
export function renderAudioEditor() {
|
export function renderAudioEditor() {
|
||||||
const audioEditor = document.querySelector('.audio-editor');
|
const audioEditor = document.querySelector('.audio-editor');
|
||||||
const existingTrackContainer = document.getElementById('audio-track-container');
|
const existingTrackContainer = document.getElementById('audio-track-container');
|
||||||
if (!audioEditor || !existingTrackContainer) return;
|
if (!audioEditor || !existingTrackContainer) return;
|
||||||
|
|
||||||
// --- CRIAÇÃO E RENDERIZAÇÃO DA RÉGUA (AGORA COM WRAPPER E SPACER) ---
|
// --- CRIAÇÃO E RENDERIZAÇÃO DA RÉGUA ---
|
||||||
let rulerWrapper = audioEditor.querySelector('.ruler-wrapper');
|
let rulerWrapper = audioEditor.querySelector('.ruler-wrapper');
|
||||||
if (!rulerWrapper) {
|
if (!rulerWrapper) {
|
||||||
rulerWrapper = document.createElement('div');
|
rulerWrapper = document.createElement('div');
|
||||||
|
|
@ -28,32 +28,35 @@ export function renderAudioEditor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ruler = rulerWrapper.querySelector('.timeline-ruler');
|
const ruler = rulerWrapper.querySelector('.timeline-ruler');
|
||||||
ruler.innerHTML = ''; // Limpa a régua para redesenhar
|
ruler.innerHTML = '';
|
||||||
|
|
||||||
const pixelsPerSecond = getPixelsPerSecond();
|
const pixelsPerSecond = getPixelsPerSecond();
|
||||||
|
|
||||||
let maxTime = appState.global.loopEndTime;
|
let maxTime = appState.global.loopEndTime;
|
||||||
appState.audio.clips.forEach(clip => {
|
appState.audio.clips.forEach(clip => {
|
||||||
const endTime = clip.startTime + clip.duration;
|
const endTime = (clip.startTimeInSeconds || 0) + (clip.durationInSeconds || 0);
|
||||||
if (endTime > maxTime) maxTime = endTime;
|
if (endTime > maxTime) maxTime = endTime;
|
||||||
});
|
});
|
||||||
|
|
||||||
const containerWidth = existingTrackContainer.offsetWidth;
|
const containerWidth = existingTrackContainer.offsetWidth;
|
||||||
const contentWidth = maxTime * pixelsPerSecond;
|
const contentWidth = maxTime * pixelsPerSecond;
|
||||||
const totalWidth = Math.max(contentWidth, containerWidth, 2000); // Garante uma largura mínima
|
const totalWidth = Math.max(contentWidth, containerWidth, 2000);
|
||||||
|
|
||||||
ruler.style.width = `${totalWidth}px`;
|
ruler.style.width = `${totalWidth}px`;
|
||||||
|
|
||||||
const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex];
|
const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex];
|
||||||
const scaledBarWidth = PIXELS_PER_BAR * zoomFactor;
|
const beatsPerBar = getBeatsPerBar();
|
||||||
|
const stepWidthPx = PIXELS_PER_STEP * zoomFactor;
|
||||||
|
const beatWidthPx = stepWidthPx * 4;
|
||||||
|
const barWidthPx = beatWidthPx * beatsPerBar;
|
||||||
|
|
||||||
if (scaledBarWidth > 0) {
|
if (barWidthPx > 0) {
|
||||||
const numberOfBars = Math.ceil(totalWidth / scaledBarWidth);
|
const numberOfBars = Math.ceil(totalWidth / barWidthPx);
|
||||||
for (let i = 1; i <= numberOfBars; i++) {
|
for (let i = 1; i <= numberOfBars; i++) {
|
||||||
const marker = document.createElement('div');
|
const marker = document.createElement('div');
|
||||||
marker.className = 'ruler-marker';
|
marker.className = 'ruler-marker';
|
||||||
marker.textContent = i;
|
marker.textContent = i;
|
||||||
marker.style.left = `${(i - 1) * scaledBarWidth}px`;
|
marker.style.left = `${(i - 1) * barWidthPx}px`;
|
||||||
ruler.appendChild(marker);
|
ruler.appendChild(marker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,13 +69,13 @@ export function renderAudioEditor() {
|
||||||
loopRegion.classList.toggle("visible", appState.global.isLoopActive);
|
loopRegion.classList.toggle("visible", appState.global.isLoopActive);
|
||||||
ruler.appendChild(loopRegion);
|
ruler.appendChild(loopRegion);
|
||||||
|
|
||||||
// --- LISTENER DA RÉGUA PARA INTERAÇÕES (LOOP E SEEK) ---
|
// --- LISTENER DA RÉGUA (sem alterações) ---
|
||||||
ruler.addEventListener('mousedown', (e) => {
|
ruler.addEventListener('mousedown', (e) => {
|
||||||
const currentPixelsPerSecond = getPixelsPerSecond();
|
const currentPixelsPerSecond = getPixelsPerSecond();
|
||||||
const loopHandle = e.target.closest('.loop-handle');
|
const loopHandle = e.target.closest('.loop-handle');
|
||||||
const loopRegionBody = e.target.closest('#loop-region:not(.loop-handle)');
|
const loopRegionBody = e.target.closest('#loop-region:not(.loop-handle)');
|
||||||
|
|
||||||
if (loopHandle) {
|
if (loopHandle) { /* ... lógica de loop ... */
|
||||||
e.preventDefault(); e.stopPropagation();
|
e.preventDefault(); e.stopPropagation();
|
||||||
const handleType = loopHandle.classList.contains('left') ? 'left' : 'right';
|
const handleType = loopHandle.classList.contains('left') ? 'left' : 'right';
|
||||||
const initialMouseX = e.clientX;
|
const initialMouseX = e.clientX;
|
||||||
|
|
@ -87,35 +90,28 @@ export function renderAudioEditor() {
|
||||||
|
|
||||||
if (handleType === 'left') {
|
if (handleType === 'left') {
|
||||||
newStart = Math.max(0, initialStart + deltaTime);
|
newStart = Math.max(0, initialStart + deltaTime);
|
||||||
newStart = Math.min(newStart, appState.global.loopEndTime - 0.1); // Não deixa passar do fim
|
newStart = Math.min(newStart, appState.global.loopEndTime - 0.1);
|
||||||
appState.global.loopStartTime = newStart;
|
appState.global.loopStartTime = newStart;
|
||||||
} else {
|
} else {
|
||||||
newEnd = Math.max(appState.global.loopStartTime + 0.1, initialEnd + deltaTime); // Não deixa ser antes do início
|
newEnd = Math.max(appState.global.loopStartTime + 0.1, initialEnd + deltaTime);
|
||||||
appState.global.loopEndTime = newEnd;
|
appState.global.loopEndTime = newEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTransportLoop();
|
updateTransportLoop();
|
||||||
|
|
||||||
// ### CORREÇÃO DE PERFORMANCE 1 ###
|
|
||||||
// Remove a chamada para renderAudioEditor()
|
|
||||||
// Em vez disso, atualiza o estilo do elemento 'loopRegion' diretamente
|
|
||||||
loopRegion.style.left = `${newStart * currentPixelsPerSecond}px`;
|
loopRegion.style.left = `${newStart * currentPixelsPerSecond}px`;
|
||||||
loopRegion.style.width = `${(newEnd - newStart) * currentPixelsPerSecond}px`;
|
loopRegion.style.width = `${(newEnd - newStart) * currentPixelsPerSecond}px`;
|
||||||
// ### FIM DA CORREÇÃO 1 ###
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = () => {
|
const onMouseUp = () => {
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
document.removeEventListener('mouseup', onMouseUp);
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
// Opcional: chamar renderAudioEditor() UMA VEZ no final para garantir a sincronia
|
|
||||||
renderAudioEditor();
|
renderAudioEditor();
|
||||||
};
|
};
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
document.addEventListener('mouseup', onMouseUp);
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (loopRegionBody) { /* ... lógica de mover loop ... */
|
||||||
if (loopRegionBody) {
|
|
||||||
e.preventDefault(); e.stopPropagation();
|
e.preventDefault(); e.stopPropagation();
|
||||||
const initialMouseX = e.clientX;
|
const initialMouseX = e.clientX;
|
||||||
const initialStart = appState.global.loopStartTime;
|
const initialStart = appState.global.loopStartTime;
|
||||||
|
|
@ -127,23 +123,15 @@ export function renderAudioEditor() {
|
||||||
const deltaTime = deltaX / currentPixelsPerSecond;
|
const deltaTime = deltaX / currentPixelsPerSecond;
|
||||||
let newStart = Math.max(0, initialStart + deltaTime);
|
let newStart = Math.max(0, initialStart + deltaTime);
|
||||||
let newEnd = newStart + initialDuration;
|
let newEnd = newStart + initialDuration;
|
||||||
|
|
||||||
appState.global.loopStartTime = newStart;
|
appState.global.loopStartTime = newStart;
|
||||||
appState.global.loopEndTime = newEnd;
|
appState.global.loopEndTime = newEnd;
|
||||||
|
|
||||||
updateTransportLoop();
|
updateTransportLoop();
|
||||||
|
loopRegion.style.left = `${newStart * currentPixelsPerSecond}px`;
|
||||||
// ### CORREÇÃO DE PERFORMANCE 2 ###
|
|
||||||
// Remove a chamada para renderAudioEditor()
|
|
||||||
// Atualiza apenas a posição 'left' do elemento
|
|
||||||
loopRegion.style.left = `${newStart * currentPixelsPerSecond}px`;
|
|
||||||
// ### FIM DA CORREÇÃO 2 ###
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = () => {
|
const onMouseUp = () => {
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
document.removeEventListener('mouseup', onMouseUp);
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
// Opcional: chamar renderAudioEditor() UMA VEZ no final
|
|
||||||
renderAudioEditor();
|
renderAudioEditor();
|
||||||
};
|
};
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
@ -151,9 +139,8 @@ export function renderAudioEditor() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se o clique não foi em um handle ou no corpo do loop, faz o "seek"
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const handleSeek = (event) => {
|
const handleSeek = (event) => { /* ... lógica de seek ... */
|
||||||
const rect = ruler.getBoundingClientRect();
|
const rect = ruler.getBoundingClientRect();
|
||||||
const scrollLeft = ruler.scrollLeft;
|
const scrollLeft = ruler.scrollLeft;
|
||||||
const clickX = event.clientX - rect.left;
|
const clickX = event.clientX - rect.left;
|
||||||
|
|
@ -168,7 +155,7 @@ export function renderAudioEditor() {
|
||||||
document.addEventListener('mouseup', onMouseUpSeek);
|
document.addEventListener('mouseup', onMouseUpSeek);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- RECRIAÇÃO DO CONTAINER DE PISTAS PARA EVITAR LISTENERS DUPLICADOS ---
|
// --- RECRIAÇÃO DO CONTAINER DE PISTAS (sem alterações) ---
|
||||||
const newTrackContainer = existingTrackContainer.cloneNode(false);
|
const newTrackContainer = existingTrackContainer.cloneNode(false);
|
||||||
audioEditor.replaceChild(newTrackContainer, existingTrackContainer);
|
audioEditor.replaceChild(newTrackContainer, existingTrackContainer);
|
||||||
|
|
||||||
|
|
@ -176,7 +163,7 @@ export function renderAudioEditor() {
|
||||||
appState.audio.tracks.push({ id: Date.now(), name: "Pista de Áudio 1" });
|
appState.audio.tracks.push({ id: Date.now(), name: "Pista de Áudio 1" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- RENDERIZAÇÃO DAS PISTAS INDIVIDUAIS ---
|
// --- RENDERIZAÇÃO DAS PISTAS INDIVIDUAIS (sem alterações) ---
|
||||||
appState.audio.tracks.forEach(trackData => {
|
appState.audio.tracks.forEach(trackData => {
|
||||||
const audioTrackLane = document.createElement('div');
|
const audioTrackLane = document.createElement('div');
|
||||||
audioTrackLane.className = 'audio-track-lane';
|
audioTrackLane.className = 'audio-track-lane';
|
||||||
|
|
@ -210,6 +197,7 @@ export function renderAudioEditor() {
|
||||||
|
|
||||||
timelineContainer.addEventListener("dragover", (e) => { e.preventDefault(); audioTrackLane.classList.add('drag-over'); });
|
timelineContainer.addEventListener("dragover", (e) => { e.preventDefault(); audioTrackLane.classList.add('drag-over'); });
|
||||||
timelineContainer.addEventListener("dragleave", () => audioTrackLane.classList.remove('drag-over'));
|
timelineContainer.addEventListener("dragleave", () => audioTrackLane.classList.remove('drag-over'));
|
||||||
|
|
||||||
timelineContainer.addEventListener("drop", (e) => {
|
timelineContainer.addEventListener("drop", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
audioTrackLane.classList.remove('drag-over');
|
audioTrackLane.classList.remove('drag-over');
|
||||||
|
|
@ -217,25 +205,37 @@ export function renderAudioEditor() {
|
||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
const rect = timelineContainer.getBoundingClientRect();
|
const rect = timelineContainer.getBoundingClientRect();
|
||||||
const dropX = e.clientX - rect.left + timelineContainer.scrollLeft;
|
const dropX = e.clientX - rect.left + timelineContainer.scrollLeft;
|
||||||
const startTimeInSeconds = dropX / pixelsPerSecond;
|
let startTimeInSeconds = dropX / pixelsPerSecond;
|
||||||
|
startTimeInSeconds = quantizeTime(startTimeInSeconds);
|
||||||
addAudioClipToTimeline(filePath, trackData.id, startTimeInSeconds);
|
addAudioClipToTimeline(filePath, trackData.id, startTimeInSeconds);
|
||||||
});
|
});
|
||||||
|
|
||||||
const grid = timelineContainer.querySelector('.spectrogram-view-grid');
|
const grid = timelineContainer.querySelector('.spectrogram-view-grid');
|
||||||
grid.style.setProperty('--bar-width', `${scaledBarWidth}px`);
|
grid.style.setProperty('--step-width', `${stepWidthPx}px`);
|
||||||
grid.style.setProperty('--four-bar-width', `${scaledBarWidth * 4}px`);
|
grid.style.setProperty('--beat-width', `${beatWidthPx}px`);
|
||||||
|
grid.style.setProperty('--bar-width', `${barWidthPx}px`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- RENDERIZAÇÃO DOS CLIPS ---
|
// --- RENDERIZAÇÃO DOS CLIPS (COM MODIFICAÇÃO PARA SELEÇÃO) ---
|
||||||
appState.audio.clips.forEach(clip => {
|
appState.audio.clips.forEach(clip => {
|
||||||
const parentGrid = newTrackContainer.querySelector(`.audio-track-lane[data-track-id="${clip.trackId}"] .spectrogram-view-grid`);
|
const parentGrid = newTrackContainer.querySelector(`.audio-track-lane[data-track-id="${clip.trackId}"] .spectrogram-view-grid`);
|
||||||
if (!parentGrid) return;
|
if (!parentGrid) return;
|
||||||
|
|
||||||
const clipElement = document.createElement('div');
|
const clipElement = document.createElement('div');
|
||||||
clipElement.className = 'timeline-clip';
|
clipElement.className = 'timeline-clip';
|
||||||
clipElement.dataset.clipId = clip.id;
|
clipElement.dataset.clipId = clip.id;
|
||||||
clipElement.style.left = `${clip.startTime * pixelsPerSecond}px`;
|
|
||||||
clipElement.style.width = `${clip.duration * pixelsPerSecond}px`;
|
// --- INÍCIO DA MODIFICAÇÃO ---
|
||||||
let pitchStr = clip.pitch > 0 ? `+${clip.pitch}` : `${clip.pitch}`;
|
// Adiciona a classe 'selected' se o ID corresponder ao estado global
|
||||||
|
if (clip.id === appState.global.selectedClipId) {
|
||||||
|
clipElement.classList.add('selected');
|
||||||
|
}
|
||||||
|
// --- FIM DA MODIFICAÇÃO ---
|
||||||
|
|
||||||
|
clipElement.style.left = `${(clip.startTimeInSeconds || 0) * pixelsPerSecond}px`;
|
||||||
|
clipElement.style.width = `${(clip.durationInSeconds || 0) * pixelsPerSecond}px`;
|
||||||
|
|
||||||
|
let pitchStr = clip.pitch > 0 ? `+${clip.pitch.toFixed(1)}` : `${clip.pitch.toFixed(1)}`;
|
||||||
if (clip.pitch === 0) pitchStr = '';
|
if (clip.pitch === 0) pitchStr = '';
|
||||||
clipElement.innerHTML = `
|
clipElement.innerHTML = `
|
||||||
<div class="clip-resize-handle left"></div>
|
<div class="clip-resize-handle left"></div>
|
||||||
|
|
@ -244,13 +244,31 @@ export function renderAudioEditor() {
|
||||||
<div class="clip-resize-handle right"></div>
|
<div class="clip-resize-handle right"></div>
|
||||||
`;
|
`;
|
||||||
parentGrid.appendChild(clipElement);
|
parentGrid.appendChild(clipElement);
|
||||||
if (clip.player && clip.player.loaded) {
|
|
||||||
|
if (clip.buffer) {
|
||||||
const canvas = clipElement.querySelector('.waveform-canvas-clip');
|
const canvas = clipElement.querySelector('.waveform-canvas-clip');
|
||||||
canvas.width = clip.duration * pixelsPerSecond;
|
const canvasWidth = (clip.durationInSeconds || 0) * pixelsPerSecond;
|
||||||
canvas.height = 40;
|
if (canvasWidth > 0) {
|
||||||
const audioBuffer = clip.player.buffer.get();
|
canvas.width = canvasWidth;
|
||||||
drawWaveform(canvas, audioBuffer, 'var(--accent-green)', clip.offset, clip.duration);
|
canvas.height = 40;
|
||||||
|
const audioBuffer = clip.buffer;
|
||||||
|
|
||||||
|
// --- INÍCIO DA CORREÇÃO ---
|
||||||
|
// Verifica se o clipe está esticado (pitch != 0) ou aparado (pitch == 0).
|
||||||
|
const isStretched = clip.pitch !== 0;
|
||||||
|
|
||||||
|
// Se for 'stretch', devemos usar a duração original do buffer como fonte.
|
||||||
|
// Se for 'trim', usamos a duração e offset atuais do clipe.
|
||||||
|
const sourceOffset = isStretched ? 0 : (clip.offset || 0);
|
||||||
|
const sourceDuration = isStretched ? clip.originalDuration : clip.durationInSeconds;
|
||||||
|
|
||||||
|
// Chama drawWaveform para desenhar o segmento-fonte (source)
|
||||||
|
// dentro do canvas (que já tem a largura de destino).
|
||||||
|
drawWaveform(canvas, audioBuffer, 'var(--accent-green)', sourceOffset, sourceDuration);
|
||||||
|
// --- FIM DA CORREÇÃO ---
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clipElement.addEventListener('wheel', (e) => {
|
clipElement.addEventListener('wheel', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const clipToUpdate = appState.audio.clips.find(c => c.id == clipElement.dataset.clipId);
|
const clipToUpdate = appState.audio.clips.find(c => c.id == clipElement.dataset.clipId);
|
||||||
|
|
@ -264,7 +282,7 @@ export function renderAudioEditor() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- SINCRONIZAÇÃO DE SCROLL ENTRE A RÉGUA E AS PISTAS ---
|
// --- SINCRONIZAÇÃO DE SCROLL (sem alterações) ---
|
||||||
newTrackContainer.addEventListener('scroll', () => {
|
newTrackContainer.addEventListener('scroll', () => {
|
||||||
const scrollPos = newTrackContainer.scrollLeft;
|
const scrollPos = newTrackContainer.scrollLeft;
|
||||||
if (ruler.scrollLeft !== scrollPos) {
|
if (ruler.scrollLeft !== scrollPos) {
|
||||||
|
|
@ -272,18 +290,212 @@ export function renderAudioEditor() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- EVENT LISTENER PRINCIPAL PARA INTERAÇÕES (MOVER, REDIMENSIONAR, ETC.) ---
|
// --- EVENT LISTENER PRINCIPAL (COM MODIFICAÇÃO PARA SELEÇÃO) ---
|
||||||
newTrackContainer.addEventListener('mousedown', (e) => {
|
newTrackContainer.addEventListener('mousedown', (e) => {
|
||||||
const currentPixelsPerSecond = getPixelsPerSecond();
|
// --- INÍCIO DA MODIFICAÇÃO ---
|
||||||
const handle = e.target.closest('.clip-resize-handle');
|
// Esconde o menu de contexto se estiver aberto
|
||||||
|
const menu = document.getElementById('timeline-context-menu');
|
||||||
|
if (menu) menu.style.display = 'none';
|
||||||
|
|
||||||
const clipElement = e.target.closest('.timeline-clip');
|
const clipElement = e.target.closest('.timeline-clip');
|
||||||
|
|
||||||
if (appState.global.sliceToolActive && clipElement) { /* ... lógica de corte ... */ return; }
|
// Desseleciona se clicar fora de um clipe (e não for clique direito)
|
||||||
if (handle) { /* ... lógica de redimensionamento de clipe ... */ return; }
|
if (!clipElement && e.button !== 2) {
|
||||||
|
if (appState.global.selectedClipId) {
|
||||||
|
appState.global.selectedClipId = null;
|
||||||
|
|
||||||
|
// Remove a classe de todos os clipes (para resposta visual imediata)
|
||||||
|
newTrackContainer.querySelectorAll('.timeline-clip.selected').forEach(c => {
|
||||||
|
c.classList.remove('selected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- FIM DA MODIFICAÇÃO ---
|
||||||
|
|
||||||
|
const currentPixelsPerSecond = getPixelsPerSecond();
|
||||||
|
const handle = e.target.closest('.clip-resize-handle');
|
||||||
|
// const clipElement = e.target.closest('.timeline-clip'); // <-- Já definido acima
|
||||||
|
|
||||||
|
if (appState.global.sliceToolActive && clipElement) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const clipId = clipElement.dataset.clipId;
|
||||||
|
const timelineContainer = clipElement.closest('.timeline-container');
|
||||||
|
const rect = timelineContainer.getBoundingClientRect();
|
||||||
|
const clickX = e.clientX - rect.left;
|
||||||
|
const absoluteX = clickX + timelineContainer.scrollLeft;
|
||||||
|
let sliceTimeInTimeline = absoluteX / currentPixelsPerSecond;
|
||||||
|
sliceTimeInTimeline = quantizeTime(sliceTimeInTimeline);
|
||||||
|
sliceAudioClip(clipId, sliceTimeInTimeline);
|
||||||
|
renderAudioEditor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CORREÇÃO: LÓGICA DE REDIMENSIONAMENTO DIVIDIDA ---
|
||||||
|
if (handle) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const clipId = clipElement.dataset.clipId;
|
||||||
|
const clip = appState.audio.clips.find(c => c.id == clipId);
|
||||||
|
if (!clip || !clip.buffer) return;
|
||||||
|
|
||||||
|
const handleType = handle.classList.contains('left') ? 'left' : 'right';
|
||||||
|
const initialMouseX = e.clientX;
|
||||||
|
const secondsPerStep = getSecondsPerStep();
|
||||||
|
|
||||||
|
const initialLeftPx = clipElement.offsetLeft;
|
||||||
|
const initialWidthPx = clipElement.offsetWidth;
|
||||||
|
|
||||||
|
const initialStartTime = clip.startTimeInSeconds;
|
||||||
|
const initialDuration = clip.durationInSeconds;
|
||||||
|
const initialOffset = clip.offset || 0;
|
||||||
|
const initialPitch = clip.pitch || 0;
|
||||||
|
const initialOriginalDuration = clip.originalDuration || clip.buffer.duration;
|
||||||
|
|
||||||
|
// O tempo "zero" absoluto do buffer (seu início)
|
||||||
|
const bufferStartTime = initialStartTime - initialOffset;
|
||||||
|
|
||||||
|
const onMouseMove = (moveEvent) => {
|
||||||
|
const deltaX = moveEvent.clientX - initialMouseX;
|
||||||
|
|
||||||
|
// --- MODO 2: TRIMMING ---
|
||||||
|
if (appState.global.resizeMode === 'trim') {
|
||||||
|
if (handleType === 'right') {
|
||||||
|
let newWidthPx = initialWidthPx + deltaX;
|
||||||
|
let newDuration = newWidthPx / currentPixelsPerSecond;
|
||||||
|
let newEndTime = quantizeTime(initialStartTime + newDuration);
|
||||||
|
|
||||||
|
newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime);
|
||||||
|
|
||||||
|
const maxEndTime = bufferStartTime + initialOriginalDuration;
|
||||||
|
newEndTime = Math.min(newEndTime, maxEndTime);
|
||||||
|
|
||||||
|
clipElement.style.width = `${(newEndTime - initialStartTime) * currentPixelsPerSecond}px`;
|
||||||
|
|
||||||
|
} else if (handleType === 'left') {
|
||||||
|
let newLeftPx = initialLeftPx + deltaX;
|
||||||
|
let newStartTime = newLeftPx / currentPixelsPerSecond;
|
||||||
|
newStartTime = quantizeTime(newStartTime);
|
||||||
|
|
||||||
|
const minStartTime = (initialStartTime + initialDuration) - secondsPerStep;
|
||||||
|
newStartTime = Math.min(newStartTime, minStartTime);
|
||||||
|
|
||||||
|
newStartTime = Math.max(bufferStartTime, newStartTime);
|
||||||
|
|
||||||
|
const newLeftFinalPx = newStartTime * currentPixelsPerSecond;
|
||||||
|
const newWidthFinalPx = ((initialStartTime + initialDuration) - newStartTime) * currentPixelsPerSecond;
|
||||||
|
clipElement.style.left = `${newLeftFinalPx}px`;
|
||||||
|
clipElement.style.width = `${newWidthFinalPx}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- MODO 1: STRETCHING ---
|
||||||
|
else if (appState.global.resizeMode === 'stretch') {
|
||||||
|
if (handleType === 'right') {
|
||||||
|
let newWidthPx = initialWidthPx + deltaX;
|
||||||
|
let newDuration = newWidthPx / currentPixelsPerSecond;
|
||||||
|
let newEndTime = quantizeTime(initialStartTime + newDuration);
|
||||||
|
|
||||||
|
newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime);
|
||||||
|
|
||||||
|
clipElement.style.width = `${(newEndTime - initialStartTime) * currentPixelsPerSecond}px`;
|
||||||
|
|
||||||
|
} else if (handleType === 'left') {
|
||||||
|
let newLeftPx = initialLeftPx + deltaX;
|
||||||
|
let newStartTime = newLeftPx / currentPixelsPerSecond;
|
||||||
|
newStartTime = quantizeTime(newStartTime);
|
||||||
|
|
||||||
|
const minStartTime = (initialStartTime + initialDuration) - secondsPerStep;
|
||||||
|
newStartTime = Math.min(newStartTime, minStartTime);
|
||||||
|
|
||||||
|
const newLeftFinalPx = newStartTime * currentPixelsPerSecond;
|
||||||
|
const newWidthFinalPx = ((initialStartTime + initialDuration) - newStartTime) * currentPixelsPerSecond;
|
||||||
|
clipElement.style.left = `${newLeftFinalPx}px`;
|
||||||
|
clipElement.style.width = `${newWidthFinalPx}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = (upEvent) => {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
|
||||||
|
const finalLeftPx = clipElement.offsetLeft;
|
||||||
|
const finalWidthPx = clipElement.offsetWidth;
|
||||||
|
|
||||||
|
const newStartTime = finalLeftPx / currentPixelsPerSecond;
|
||||||
|
const newDuration = finalWidthPx / currentPixelsPerSecond;
|
||||||
|
|
||||||
|
// --- MODO 2: TRIMMING ---
|
||||||
|
if (appState.global.resizeMode === 'trim') {
|
||||||
|
const newOffset = newStartTime - bufferStartTime;
|
||||||
|
|
||||||
|
if (handleType === 'right') {
|
||||||
|
updateAudioClipProperties(clipId, {
|
||||||
|
durationInSeconds: newDuration,
|
||||||
|
pitch: 0 // Reseta o pitch
|
||||||
|
});
|
||||||
|
} else if (handleType === 'left') {
|
||||||
|
updateAudioClipProperties(clipId, {
|
||||||
|
startTimeInSeconds: newStartTime,
|
||||||
|
durationInSeconds: newDuration,
|
||||||
|
offset: newOffset,
|
||||||
|
pitch: 0 // Reseta o pitch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- MODO 1: STRETCHING ---
|
||||||
|
else if (appState.global.resizeMode === 'stretch') {
|
||||||
|
// Calcula o novo pitch baseado na mudança de duração
|
||||||
|
// Usa a duração *do buffer* (originalDuration) como base
|
||||||
|
const newPlaybackRate = initialOriginalDuration / newDuration;
|
||||||
|
const newPitch = 12 * Math.log2(newPlaybackRate);
|
||||||
|
|
||||||
|
if (handleType === 'right') {
|
||||||
|
updateAudioClipProperties(clipId, {
|
||||||
|
durationInSeconds: newDuration,
|
||||||
|
pitch: newPitch,
|
||||||
|
offset: 0 // Stretch sempre reseta o offset
|
||||||
|
});
|
||||||
|
} else if (handleType === 'left') {
|
||||||
|
updateAudioClipProperties(clipId, {
|
||||||
|
startTimeInSeconds: newStartTime,
|
||||||
|
durationInSeconds: newDuration,
|
||||||
|
pitch: newPitch,
|
||||||
|
offset: 0 // Stretch sempre reseta o offset
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restartAudioEditorIfPlaying();
|
||||||
|
renderAudioEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// --- FIM DA CORREÇÃO ---
|
||||||
|
|
||||||
|
|
||||||
if (clipElement) {
|
if (clipElement) {
|
||||||
e.preventDefault();
|
// --- INÍCIO DA MODIFICAÇÃO (SELEÇÃO NO DRAG) ---
|
||||||
const clipId = clipElement.dataset.clipId;
|
const clipId = clipElement.dataset.clipId;
|
||||||
|
// Se o clipe clicado não for o já selecionado, atualiza a seleção
|
||||||
|
if (appState.global.selectedClipId !== clipId) {
|
||||||
|
appState.global.selectedClipId = clipId; // Define o estado global
|
||||||
|
|
||||||
|
// Atualiza visualmente
|
||||||
|
newTrackContainer.querySelectorAll('.timeline-clip.selected').forEach(c => {
|
||||||
|
c.classList.remove('selected');
|
||||||
|
});
|
||||||
|
clipElement.classList.add('selected');
|
||||||
|
}
|
||||||
|
// --- FIM DA MODIFICAÇÃO ---
|
||||||
|
|
||||||
|
// Lógica de 'drag' (sem alterações)
|
||||||
|
e.preventDefault();
|
||||||
|
// const clipId = clipElement.dataset.clipId; // <-- Já definido acima
|
||||||
const clickOffsetInClip = e.clientX - clipElement.getBoundingClientRect().left;
|
const clickOffsetInClip = e.clientX - clipElement.getBoundingClientRect().left;
|
||||||
clipElement.classList.add('dragging');
|
clipElement.classList.add('dragging');
|
||||||
let lastOverLane = clipElement.closest('.audio-track-lane');
|
let lastOverLane = clipElement.closest('.audio-track-lane');
|
||||||
|
|
@ -312,9 +524,10 @@ export function renderAudioEditor() {
|
||||||
const newLeftPx = (upEvent.clientX - wrapperRect.left) - clickOffsetInClip + timelineContainer.scrollLeft;
|
const newLeftPx = (upEvent.clientX - wrapperRect.left) - clickOffsetInClip + timelineContainer.scrollLeft;
|
||||||
|
|
||||||
const constrainedLeftPx = Math.max(0, newLeftPx);
|
const constrainedLeftPx = Math.max(0, newLeftPx);
|
||||||
const newStartTime = constrainedLeftPx / currentPixelsPerSecond;
|
let newStartTime = constrainedLeftPx / currentPixelsPerSecond;
|
||||||
|
newStartTime = quantizeTime(newStartTime);
|
||||||
|
|
||||||
updateAudioClipProperties(clipId, { trackId: Number(newTrackId), startTime: newStartTime });
|
updateAudioClipProperties(clipId, { trackId: Number(newTrackId), startTimeInSeconds: newStartTime });
|
||||||
renderAudioEditor();
|
renderAudioEditor();
|
||||||
};
|
};
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
@ -324,6 +537,7 @@ export function renderAudioEditor() {
|
||||||
|
|
||||||
const timelineContainer = e.target.closest('.timeline-container');
|
const timelineContainer = e.target.closest('.timeline-container');
|
||||||
if (timelineContainer) {
|
if (timelineContainer) {
|
||||||
|
// Lógica de 'seek' (sem alterações)
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const handleSeek = (event) => {
|
const handleSeek = (event) => {
|
||||||
const rect = timelineContainer.getBoundingClientRect();
|
const rect = timelineContainer.getBoundingClientRect();
|
||||||
|
|
@ -340,8 +554,42 @@ export function renderAudioEditor() {
|
||||||
document.addEventListener('mouseup', onMouseUpSeek);
|
document.addEventListener('mouseup', onMouseUpSeek);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- LISTENER ADICIONADO (Menu de Contexto) ---
|
||||||
|
newTrackContainer.addEventListener('contextmenu', (e) => {
|
||||||
|
e.preventDefault(); // Impede o menu de contexto padrão do navegador
|
||||||
|
const menu = document.getElementById('timeline-context-menu');
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
const clipElement = e.target.closest('.timeline-clip');
|
||||||
|
|
||||||
|
if (clipElement) {
|
||||||
|
// 1. Seleciona o clipe clicado (define o estado)
|
||||||
|
const clipId = clipElement.dataset.clipId;
|
||||||
|
if (appState.global.selectedClipId !== clipId) {
|
||||||
|
appState.global.selectedClipId = clipId;
|
||||||
|
|
||||||
|
// Atualiza visualmente
|
||||||
|
newTrackContainer.querySelectorAll('.timeline-clip.selected').forEach(c => {
|
||||||
|
c.classList.remove('selected');
|
||||||
|
});
|
||||||
|
clipElement.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Posiciona e mostra o menu
|
||||||
|
menu.style.display = 'block';
|
||||||
|
menu.style.left = `${e.clientX}px`;
|
||||||
|
menu.style.top = `${e.clientY}px`;
|
||||||
|
} else {
|
||||||
|
// Esconde o menu se clicar com o botão direito fora de um clipe
|
||||||
|
menu.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// --- FIM DO LISTENER ADICIONADO ---
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Funções de UI (sem alterações) ---
|
||||||
|
|
||||||
export function updateAudioEditorUI() {
|
export function updateAudioEditorUI() {
|
||||||
const playBtn = document.getElementById('audio-editor-play-btn');
|
const playBtn = document.getElementById('audio-editor-play-btn');
|
||||||
if (!playBtn) return;
|
if (!playBtn) return;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// js/main.js
|
// js/main.js
|
||||||
import { appState, resetProjectState } from "./state.js";
|
import { appState, resetProjectState } from "./state.js";
|
||||||
import { addTrackToState, removeLastTrackFromState } from "./pattern/pattern_state.js";
|
import { addTrackToState, removeLastTrackFromState } from "./pattern/pattern_state.js";
|
||||||
import { addAudioTrackLane } from "./audio/audio_state.js";
|
// --- CORREÇÃO AQUI ---
|
||||||
|
import { addAudioTrackLane, removeAudioClip } from "./audio/audio_state.js";
|
||||||
import { updateTransportLoop } from "./audio/audio_audio.js";
|
import { updateTransportLoop } from "./audio/audio_audio.js";
|
||||||
import {
|
import {
|
||||||
togglePlayback,
|
togglePlayback,
|
||||||
|
|
@ -20,6 +21,21 @@ import { renderAudioEditor } from "./audio/audio_ui.js";
|
||||||
import { adjustValue, enforceNumericInput } from "./utils.js";
|
import { adjustValue, enforceNumericInput } from "./utils.js";
|
||||||
import { ZOOM_LEVELS } from "./config.js";
|
import { ZOOM_LEVELS } from "./config.js";
|
||||||
|
|
||||||
|
// --- NOVA FUNÇÃO ---
|
||||||
|
// Atualiza a aparência dos botões de ferramenta
|
||||||
|
function updateToolButtons() {
|
||||||
|
const sliceToolBtn = document.getElementById("slice-tool-btn");
|
||||||
|
const trimToolBtn = document.getElementById("resize-tool-trim");
|
||||||
|
const stretchToolBtn = document.getElementById("resize-tool-stretch");
|
||||||
|
|
||||||
|
if(sliceToolBtn) sliceToolBtn.classList.toggle("active", appState.global.sliceToolActive);
|
||||||
|
if(trimToolBtn) trimToolBtn.classList.toggle("active", !appState.global.sliceToolActive && appState.global.resizeMode === 'trim');
|
||||||
|
if(stretchToolBtn) stretchToolBtn.classList.toggle("active", !appState.global.sliceToolActive && appState.global.resizeMode === 'stretch');
|
||||||
|
|
||||||
|
// Desativa a ferramenta de corte se outra for selecionada
|
||||||
|
document.body.classList.toggle("slice-tool-active", appState.global.sliceToolActive);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const newProjectBtn = document.getElementById("new-project-btn");
|
const newProjectBtn = document.getElementById("new-project-btn");
|
||||||
const openMmpBtn = document.getElementById("open-mmp-btn");
|
const openMmpBtn = document.getElementById("open-mmp-btn");
|
||||||
|
|
@ -36,6 +52,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const rewindBtn = document.getElementById("rewind-btn");
|
const rewindBtn = document.getElementById("rewind-btn");
|
||||||
const metronomeBtn = document.getElementById("metronome-btn");
|
const metronomeBtn = document.getElementById("metronome-btn");
|
||||||
const sliceToolBtn = document.getElementById("slice-tool-btn");
|
const sliceToolBtn = document.getElementById("slice-tool-btn");
|
||||||
|
|
||||||
|
// --- NOVOS BOTÕES ---
|
||||||
|
const resizeToolTrimBtn = document.getElementById("resize-tool-trim");
|
||||||
|
const resizeToolStretchBtn = document.getElementById("resize-tool-stretch");
|
||||||
|
|
||||||
const mmpFileInput = document.getElementById("mmp-file-input");
|
const mmpFileInput = document.getElementById("mmp-file-input");
|
||||||
const sampleFileInput = document.getElementById("sample-file-input");
|
const sampleFileInput = document.getElementById("sample-file-input");
|
||||||
const openProjectModal = document.getElementById("open-project-modal");
|
const openProjectModal = document.getElementById("open-project-modal");
|
||||||
|
|
@ -46,6 +67,55 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const zoomInBtn = document.getElementById("zoom-in-btn");
|
const zoomInBtn = document.getElementById("zoom-in-btn");
|
||||||
const zoomOutBtn = document.getElementById("zoom-out-btn");
|
const zoomOutBtn = document.getElementById("zoom-out-btn");
|
||||||
|
|
||||||
|
// --- LISTENERS ADICIONADOS (COM LÓGICA DE CONTROLLER) ---
|
||||||
|
|
||||||
|
// Listener para o botão "Excluir Clipe" no menu de contexto
|
||||||
|
const deleteClipBtn = document.getElementById('delete-clip');
|
||||||
|
if (deleteClipBtn) {
|
||||||
|
deleteClipBtn.addEventListener('click', () => {
|
||||||
|
const clipId = appState.global.selectedClipId; // 1. Lê o estado
|
||||||
|
if (clipId) {
|
||||||
|
if (removeAudioClip(clipId)) { // 2. Chama a função de state
|
||||||
|
appState.global.selectedClipId = null; // 3. Atualiza o estado global
|
||||||
|
renderAudioEditor(); // 4. Renderiza a mudança
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Esconde o menu
|
||||||
|
const menu = document.getElementById('timeline-context-menu');
|
||||||
|
if (menu) menu.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listener global para a tecla Delete/Backspace
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// Ignora se estiver digitando em um input
|
||||||
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipId = appState.global.selectedClipId; // 1. Lê o estado
|
||||||
|
// Verifica se há um clipe selecionado e a tecla pressionada é Delete ou Backspace
|
||||||
|
if ((e.key === 'Delete' || e.key === 'Backspace') && clipId) {
|
||||||
|
e.preventDefault(); // Impede o navegador de voltar a página (ação do Backspace)
|
||||||
|
|
||||||
|
if (removeAudioClip(clipId)) { // 2. Chama a função de state
|
||||||
|
appState.global.selectedClipId = null; // 3. Atualiza o estado global
|
||||||
|
renderAudioEditor(); // 4. Renderiza a mudança
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listener global para fechar menu de contexto ou desselecionar clipe
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
// Esconde o menu de contexto se clicar fora dele
|
||||||
|
const menu = document.getElementById('timeline-context-menu');
|
||||||
|
if (menu && !e.target.closest('#timeline-context-menu')) {
|
||||||
|
menu.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- FIM DOS LISTENERS ADICIONADOS ---
|
||||||
|
|
||||||
newProjectBtn.addEventListener("click", () => {
|
newProjectBtn.addEventListener("click", () => {
|
||||||
if ((appState.pattern.tracks.length > 0 || appState.audio.clips.length > 0) && !confirm("Você tem certeza? Alterações não salvas serão perdidas.")) return;
|
if ((appState.pattern.tracks.length > 0 || appState.audio.clips.length > 0) && !confirm("Você tem certeza? Alterações não salvas serão perdidas.")) return;
|
||||||
resetProjectState();
|
resetProjectState();
|
||||||
|
|
@ -74,10 +144,31 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
stopBtn.addEventListener("click", stopPlayback);
|
stopBtn.addEventListener("click", stopPlayback);
|
||||||
rewindBtn.addEventListener("click", rewindPlayback);
|
rewindBtn.addEventListener("click", rewindPlayback);
|
||||||
metronomeBtn.addEventListener("click", () => { initializeAudioContext(); appState.global.metronomeEnabled = !appState.global.metronomeEnabled; metronomeBtn.classList.toggle("active", appState.global.metronomeEnabled); });
|
metronomeBtn.addEventListener("click", () => { initializeAudioContext(); appState.global.metronomeEnabled = !appState.global.metronomeEnabled; metronomeBtn.classList.toggle("active", appState.global.metronomeEnabled); });
|
||||||
if(sliceToolBtn) { sliceToolBtn.addEventListener("click", () => { appState.global.sliceToolActive = !appState.global.sliceToolActive; sliceToolBtn.classList.toggle("active", appState.global.sliceToolActive); document.body.classList.toggle("slice-tool-active", appState.global.sliceToolActive); }); }
|
|
||||||
|
// --- LISTENERS DE FERRAMENTAS ATUALIZADOS ---
|
||||||
|
if(sliceToolBtn) {
|
||||||
|
sliceToolBtn.addEventListener("click", () => {
|
||||||
|
appState.global.sliceToolActive = !appState.global.sliceToolActive;
|
||||||
|
updateToolButtons();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(resizeToolTrimBtn) {
|
||||||
|
resizeToolTrimBtn.addEventListener("click", () => {
|
||||||
|
appState.global.resizeMode = 'trim';
|
||||||
|
appState.global.sliceToolActive = false;
|
||||||
|
updateToolButtons();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(resizeToolStretchBtn) {
|
||||||
|
resizeToolStretchBtn.addEventListener("click", () => {
|
||||||
|
appState.global.resizeMode = 'stretch';
|
||||||
|
appState.global.sliceToolActive = false;
|
||||||
|
updateToolButtons();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
openModalCloseBtn.addEventListener("click", closeOpenProjectModal);
|
openModalCloseBtn.addEventListener("click", closeOpenProjectModal);
|
||||||
|
|
||||||
// ### CORREÇÃO 2: Adicionada verificação 'if (icon)' ###
|
|
||||||
sidebarToggle.addEventListener("click", () => {
|
sidebarToggle.addEventListener("click", () => {
|
||||||
document.body.classList.toggle("sidebar-hidden");
|
document.body.classList.toggle("sidebar-hidden");
|
||||||
const icon = sidebarToggle.querySelector("i");
|
const icon = sidebarToggle.querySelector("i");
|
||||||
|
|
@ -116,50 +207,31 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
audioEditorPlayBtn.addEventListener("click", () => { if (appState.global.isAudioEditorPlaying) { stopAudioEditorPlayback(); } else { startAudioEditorPlayback(); } });
|
audioEditorPlayBtn.addEventListener("click", () => {
|
||||||
audioEditorStopBtn.addEventListener("click", stopAudioEditorPlayback);
|
if (appState.global.isAudioEditorPlaying) {
|
||||||
|
stopAudioEditorPlayback(false); // Pausa
|
||||||
// ### CORREÇÃO 1: Listeners duplicados combinados em um só ###
|
|
||||||
// No main.js
|
|
||||||
audioEditorLoopBtn.addEventListener("click", () => {
|
|
||||||
console.log("--- Botão de Loop Clicado ---"); // DEBUG 1
|
|
||||||
|
|
||||||
// 1. Altera o estado global de loop
|
|
||||||
appState.global.isLoopActive = !appState.global.isLoopActive;
|
|
||||||
console.log("Estado appState.global.isLoopActive:", appState.global.isLoopActive); // DEBUG 2
|
|
||||||
|
|
||||||
// 2. Sincroniza o estado do loop do editor
|
|
||||||
appState.audio.isAudioEditorLoopEnabled = appState.global.isLoopActive;
|
|
||||||
|
|
||||||
// 3. Atualiza a aparência do botão
|
|
||||||
audioEditorLoopBtn.classList.toggle("active", appState.global.isLoopActive);
|
|
||||||
|
|
||||||
// 4. Sincroniza o Tone.Transport
|
|
||||||
updateTransportLoop();
|
|
||||||
|
|
||||||
// 5. Mostra/esconde a área de loop
|
|
||||||
const loopArea = document.getElementById("loop-region");
|
|
||||||
|
|
||||||
// ESTE É O TESTE MAIS IMPORTANTE:
|
|
||||||
if (loopArea) {
|
|
||||||
console.log("Elemento #loop-region ENCONTRADO. Alterando classe 'visible'."); // DEBUG 3
|
|
||||||
loopArea.classList.toggle("visible", appState.global.isLoopActive);
|
|
||||||
} else {
|
} else {
|
||||||
console.error("ERRO GRAVE: Elemento #loop-region NÃO FOI ENCONTRADO!"); // DEBUG 4
|
startAudioEditorPlayback();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
audioEditorStopBtn.addEventListener("click", () => stopAudioEditorPlayback(true)); // Stop (rebobina)
|
||||||
|
|
||||||
// 6. Reinicia o playback se estiver tocando
|
audioEditorLoopBtn.addEventListener("click", () => {
|
||||||
|
appState.global.isLoopActive = !appState.global.isLoopActive;
|
||||||
|
appState.audio.isAudioEditorLoopEnabled = appState.global.isLoopActive;
|
||||||
|
audioEditorLoopBtn.classList.toggle("active", appState.global.isLoopActive);
|
||||||
|
updateTransportLoop();
|
||||||
|
const loopArea = document.getElementById("loop-region");
|
||||||
|
if (loopArea) {
|
||||||
|
loopArea.classList.toggle("visible", appState.global.isLoopActive);
|
||||||
|
}
|
||||||
restartAudioEditorIfPlaying();
|
restartAudioEditorIfPlaying();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (addAudioTrackBtn) { addAudioTrackBtn.addEventListener("click", () => { addAudioTrackLane(); renderAudioEditor(); }); }
|
if (addAudioTrackBtn) { addAudioTrackBtn.addEventListener("click", () => { addAudioTrackLane(); renderAudioEditor(); }); }
|
||||||
|
|
||||||
// ### CORREÇÃO 3: Ordem de execução corrigida ###
|
|
||||||
|
|
||||||
// 1. Carrega o conteúdo do navegador de samples
|
|
||||||
loadAndRenderSampleBrowser();
|
loadAndRenderSampleBrowser();
|
||||||
|
|
||||||
// 2. Adiciona o listener DEPOIS que o conteúdo supostamente existe
|
|
||||||
const browserContent = document.getElementById('browser-content');
|
const browserContent = document.getElementById('browser-content');
|
||||||
if (browserContent) {
|
if (browserContent) {
|
||||||
browserContent.addEventListener('click', function(event) {
|
browserContent.addEventListener('click', function(event) {
|
||||||
|
|
@ -171,6 +243,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Renderiza o resto
|
|
||||||
renderAll();
|
renderAll();
|
||||||
|
updateToolButtons(); // Define o estado inicial dos botões
|
||||||
});
|
});
|
||||||
|
|
@ -18,9 +18,13 @@ const globalState = {
|
||||||
zoomLevelIndex: 2,
|
zoomLevelIndex: 2,
|
||||||
|
|
||||||
// --- ADICIONADO PARA A ÁREA DE LOOP ---
|
// --- ADICIONADO PARA A ÁREA DE LOOP ---
|
||||||
isLoopActive: false, // O botão de loop principal agora controla este estado
|
isLoopActive: false,
|
||||||
loopStartTime: 0, // Início do loop em segundos
|
loopStartTime: 0,
|
||||||
loopEndTime: 8, // Fim do loop em segundos (padrão de 4 compassos a 120BPM)
|
loopEndTime: 8,
|
||||||
|
|
||||||
|
// --- ADICIONADO PARA O MODO DE REDIMENSIONAMENTO ---
|
||||||
|
resizeMode: 'trim', // Pode ser 'trim' (Modo 2) ou 'stretch' (Modo 1)
|
||||||
|
selectedClipId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Combina todos os estados em um único objeto namespaced
|
// Combina todos os estados em um único objeto namespaced
|
||||||
|
|
@ -50,5 +54,7 @@ export function resetProjectState() {
|
||||||
isLoopActive: false,
|
isLoopActive: false,
|
||||||
loopStartTime: 0,
|
loopStartTime: 0,
|
||||||
loopEndTime: 8,
|
loopEndTime: 8,
|
||||||
|
resizeMode: 'trim', // Reseta para o modo 'trim'
|
||||||
|
selectedClipId: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// js/ui.js
|
// js/ui.js
|
||||||
|
import { appState } from "./state.js"; // <-- CORREÇÃO: Importação adicionada
|
||||||
import { playSample } from "./pattern/pattern_audio.js";
|
import { playSample } from "./pattern/pattern_audio.js";
|
||||||
import { renderPatternEditor } from "./pattern/pattern_ui.js";
|
import { renderPatternEditor } from "./pattern/pattern_ui.js";
|
||||||
import { renderAudioEditor } from "./audio/audio_ui.js";
|
import { renderAudioEditor } from "./audio/audio_ui.js";
|
||||||
|
|
@ -21,9 +22,6 @@ export function getSamplePathMap() {
|
||||||
return samplePathMap;
|
return samplePathMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
function buildSamplePathMap(tree, currentPath) {
|
function buildSamplePathMap(tree, currentPath) {
|
||||||
for (const key in tree) {
|
for (const key in tree) {
|
||||||
if (key === "_isFile") continue;
|
if (key === "_isFile") continue;
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,68 @@
|
||||||
import { appState } from './state.js';
|
import { appState } from './state.js';
|
||||||
import { PIXELS_PER_STEP, ZOOM_LEVELS } from './config.js';
|
import { PIXELS_PER_STEP, ZOOM_LEVELS } from './config.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper interna para ler o BPM do input.
|
||||||
|
* @returns {number} O BPM atual.
|
||||||
|
*/
|
||||||
|
function _getBpm() {
|
||||||
|
const bpmInput = document.getElementById("bpm-input");
|
||||||
|
return parseFloat(bpmInput.value, 10) || 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula e exporta quantos compassos (beats) existem por compasso (bar).
|
||||||
|
* @returns {number} O número de batidas por compasso (ex: 4 para 4/4).
|
||||||
|
*/
|
||||||
|
export function getBeatsPerBar() {
|
||||||
|
const compassoAInput = document.getElementById("compasso-a-input");
|
||||||
|
return parseInt(compassoAInput.value, 10) || 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula e exporta quantos segundos dura uma "batida" (beat).
|
||||||
|
* No contexto de BPM, uma "batida" é quase sempre uma semínima (1/4).
|
||||||
|
* @returns {number} Duração da batida em segundos.
|
||||||
|
*/
|
||||||
|
export function getSecondsPerBeat() {
|
||||||
|
return 60.0 / _getBpm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula e exporta quantos segundos dura um "step".
|
||||||
|
* Baseado na config, um "step" é uma semicolcheia (1/16).
|
||||||
|
* Há 4 steps (1/16) por batida (1/4).
|
||||||
|
* @returns {number} Duração do step em segundos.
|
||||||
|
*/
|
||||||
|
export function getSecondsPerStep() {
|
||||||
|
return getSecondsPerBeat() / 4.0; // 4 steps (1/16) por beat (1/4)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quantiza (arredonda) um tempo em segundos para o "step" do grid mais próximo.
|
||||||
|
* @param {number} timeInSeconds - O tempo arbitrário (ex: 1.234s).
|
||||||
|
* @returns {number} O tempo alinhado ao grid (ex: 1.250s).
|
||||||
|
*/
|
||||||
|
export function quantizeTime(timeInSeconds) {
|
||||||
|
// TODO: Adicionar um toggle global (appState.global.isSnapEnabled)
|
||||||
|
|
||||||
|
const secondsPerStep = getSecondsPerStep();
|
||||||
|
if (secondsPerStep <= 0) return timeInSeconds; // Evita divisão por zero
|
||||||
|
|
||||||
|
const roundedSteps = Math.round(timeInSeconds / secondsPerStep);
|
||||||
|
return roundedSteps * secondsPerStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcula a quantidade de pixels que representa um segundo na timeline,
|
* Calcula a quantidade de pixels que representa um segundo na timeline,
|
||||||
* levando em conta o BPM e o nível de zoom atual.
|
* levando em conta o BPM e o nível de zoom atual.
|
||||||
* @returns {number} A quantidade de pixels por segundo.
|
* @returns {number} A quantidade de pixels por segundo.
|
||||||
*/
|
*/
|
||||||
export function getPixelsPerSecond() {
|
export function getPixelsPerSecond() {
|
||||||
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
const bpm = _getBpm(); // Usa a helper interna
|
||||||
|
// (bpm / 60) = batidas por segundo
|
||||||
|
// * 4 = steps por segundo (assumindo 4 steps/beat)
|
||||||
const stepsPerSecond = (bpm / 60) * 4;
|
const stepsPerSecond = (bpm / 60) * 4;
|
||||||
const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex];
|
const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex];
|
||||||
return stepsPerSecond * PIXELS_PER_STEP * zoomFactor;
|
return stepsPerSecond * PIXELS_PER_STEP * zoomFactor;
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,9 @@
|
||||||
<i class="fa-solid fa-search-minus" id="zoom-out-btn" title="Zoom Out"></i>
|
<i class="fa-solid fa-search-minus" id="zoom-out-btn" title="Zoom Out"></i>
|
||||||
<i class="fa-solid fa-search-plus" id="zoom-in-btn" title="Zoom In"></i>
|
<i class="fa-solid fa-search-plus" id="zoom-in-btn" title="Zoom In"></i>
|
||||||
<i class="fa-solid fa-scissors" id="slice-tool-btn" title="Ferramenta de Corte"></i>
|
<i class="fa-solid fa-scissors" id="slice-tool-btn" title="Ferramenta de Corte"></i>
|
||||||
|
|
||||||
|
<i class="fa-solid fa-arrows-left-right-to-line" id="resize-tool-trim" title="Modo de Redimensionamento (Aparar/Trimming)"></i>
|
||||||
|
<i class="fa-solid fa-arrows-left-right" id="resize-tool-stretch" title="Modo de Redimensionamento (Esticar/Time Stretch)"></i>
|
||||||
<i class="fa-solid fa-play" id="audio-editor-play-btn" title="Play/Pause"></i>
|
<i class="fa-solid fa-play" id="audio-editor-play-btn" title="Play/Pause"></i>
|
||||||
<i class="fa-solid fa-stop" id="audio-editor-stop-btn" title="Stop"></i>
|
<i class="fa-solid fa-stop" id="audio-editor-stop-btn" title="Stop"></i>
|
||||||
<i class="fa-solid fa-repeat" id="audio-editor-loop-btn" title="Ativar/Desativar Loop"></i>
|
<i class="fa-solid fa-repeat" id="audio-editor-loop-btn" title="Ativar/Desativar Loop"></i>
|
||||||
|
|
@ -215,6 +218,8 @@
|
||||||
<div id="timeline-context-menu">
|
<div id="timeline-context-menu">
|
||||||
<div id="set-loop-start">Definir Início do Loop</div>
|
<div id="set-loop-start">Definir Início do Loop</div>
|
||||||
<div id="set-loop-end">Definir Fim do Loop</div>
|
<div id="set-loop-end">Definir Fim do Loop</div>
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
<div id="delete-clip" style="color: var(--accent-red);">Excluir Clipe</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue