// js/audio/audio_ui.js import { appState } from "../state.js"; import { addAudioClipToTimeline, updateAudioClipProperties, sliceAudioClip, } from "./audio_state.js"; import { seekAudioEditor, restartAudioEditorIfPlaying, updateTransportLoop } from "./audio_audio.js"; import { drawWaveform } from "../waveform.js"; import { PIXELS_PER_BAR, ZOOM_LEVELS } from "../config.js"; import { getPixelsPerSecond } from "../utils.js"; export function renderAudioEditor() { const audioEditor = document.querySelector('.audio-editor'); const existingTrackContainer = document.getElementById('audio-track-container'); if (!audioEditor || !existingTrackContainer) return; // --- CRIAÇÃO E RENDERIZAÇÃO DA RÉGUA (AGORA COM WRAPPER E SPACER) --- let rulerWrapper = audioEditor.querySelector('.ruler-wrapper'); if (!rulerWrapper) { rulerWrapper = document.createElement('div'); rulerWrapper.className = 'ruler-wrapper'; rulerWrapper.innerHTML = `
`; audioEditor.insertBefore(rulerWrapper, existingTrackContainer); } const ruler = rulerWrapper.querySelector('.timeline-ruler'); ruler.innerHTML = ''; // Limpa a régua para redesenhar const pixelsPerSecond = getPixelsPerSecond(); let maxTime = appState.global.loopEndTime; appState.audio.clips.forEach(clip => { const endTime = clip.startTime + clip.duration; if (endTime > maxTime) maxTime = endTime; }); const containerWidth = existingTrackContainer.offsetWidth; const contentWidth = maxTime * pixelsPerSecond; const totalWidth = Math.max(contentWidth, containerWidth, 2000); // Garante uma largura mínima ruler.style.width = `${totalWidth}px`; const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex]; const scaledBarWidth = PIXELS_PER_BAR * zoomFactor; if (scaledBarWidth > 0) { const numberOfBars = Math.ceil(totalWidth / scaledBarWidth); for (let i = 1; i <= numberOfBars; i++) { const marker = document.createElement('div'); marker.className = 'ruler-marker'; marker.textContent = i; marker.style.left = `${(i - 1) * scaledBarWidth}px`; ruler.appendChild(marker); } } const loopRegion = document.createElement('div'); loopRegion.id = 'loop-region'; loopRegion.style.left = `${appState.global.loopStartTime * pixelsPerSecond}px`; loopRegion.style.width = `${(appState.global.loopEndTime - appState.global.loopStartTime) * pixelsPerSecond}px`; loopRegion.innerHTML = ``; loopRegion.classList.toggle("visible", appState.global.isLoopActive); ruler.appendChild(loopRegion); // --- LISTENER DA RÉGUA PARA INTERAÇÕES (LOOP E SEEK) --- ruler.addEventListener('mousedown', (e) => { const currentPixelsPerSecond = getPixelsPerSecond(); const loopHandle = e.target.closest('.loop-handle'); const loopRegionBody = e.target.closest('#loop-region:not(.loop-handle)'); if (loopHandle) { e.preventDefault(); e.stopPropagation(); const handleType = loopHandle.classList.contains('left') ? 'left' : 'right'; const initialMouseX = e.clientX; const initialStart = appState.global.loopStartTime; const initialEnd = appState.global.loopEndTime; const onMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - initialMouseX; const deltaTime = deltaX / currentPixelsPerSecond; let newStart = appState.global.loopStartTime; let newEnd = appState.global.loopEndTime; if (handleType === 'left') { newStart = Math.max(0, initialStart + deltaTime); newStart = Math.min(newStart, appState.global.loopEndTime - 0.1); // Não deixa passar do fim appState.global.loopStartTime = newStart; } else { newEnd = Math.max(appState.global.loopStartTime + 0.1, initialEnd + deltaTime); // Não deixa ser antes do início appState.global.loopEndTime = newEnd; } 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.width = `${(newEnd - newStart) * currentPixelsPerSecond}px`; // ### FIM DA CORREÇÃO 1 ### }; const onMouseUp = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); // Opcional: chamar renderAudioEditor() UMA VEZ no final para garantir a sincronia renderAudioEditor(); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); return; } if (loopRegionBody) { e.preventDefault(); e.stopPropagation(); const initialMouseX = e.clientX; const initialStart = appState.global.loopStartTime; const initialEnd = appState.global.loopEndTime; const initialDuration = initialEnd - initialStart; const onMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - initialMouseX; const deltaTime = deltaX / currentPixelsPerSecond; let newStart = Math.max(0, initialStart + deltaTime); let newEnd = newStart + initialDuration; appState.global.loopStartTime = newStart; appState.global.loopEndTime = newEnd; updateTransportLoop(); // ### 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 = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); // Opcional: chamar renderAudioEditor() UMA VEZ no final renderAudioEditor(); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); return; } // Se o clique não foi em um handle ou no corpo do loop, faz o "seek" e.preventDefault(); const handleSeek = (event) => { const rect = ruler.getBoundingClientRect(); const scrollLeft = ruler.scrollLeft; const clickX = event.clientX - rect.left; const absoluteX = clickX + scrollLeft; const newTime = absoluteX / currentPixelsPerSecond; seekAudioEditor(newTime); }; handleSeek(e); const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent); const onMouseUpSeek = () => { document.removeEventListener('mousemove', onMouseMoveSeek); document.removeEventListener('mouseup', onMouseUpSeek); }; document.addEventListener('mousemove', onMouseMoveSeek); document.addEventListener('mouseup', onMouseUpSeek); }); // --- RECRIAÇÃO DO CONTAINER DE PISTAS PARA EVITAR LISTENERS DUPLICADOS --- const newTrackContainer = existingTrackContainer.cloneNode(false); audioEditor.replaceChild(newTrackContainer, existingTrackContainer); if (appState.audio.tracks.length === 0) { appState.audio.tracks.push({ id: Date.now(), name: "Pista de Áudio 1" }); } // --- RENDERIZAÇÃO DAS PISTAS INDIVIDUAIS --- appState.audio.tracks.forEach(trackData => { const audioTrackLane = document.createElement('div'); audioTrackLane.className = 'audio-track-lane'; audioTrackLane.dataset.trackId = trackData.id; audioTrackLane.innerHTML = `