367 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			367 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
// 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 = `
 | 
						|
            <div class="ruler-spacer"></div>
 | 
						|
            <div class="timeline-ruler"></div>
 | 
						|
        `;
 | 
						|
        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 = `<div class="loop-handle left"></div><div class="loop-handle right"></div>`;
 | 
						|
    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 = `
 | 
						|
            <div class="track-info">
 | 
						|
                <div class="track-info-header">
 | 
						|
                    <i class="fa-solid fa-gear"></i>
 | 
						|
                    <span class="track-name">${trackData.name}</span>
 | 
						|
                    <div class="track-mute"></div>
 | 
						|
                </div>
 | 
						|
                <div class="track-controls">
 | 
						|
                    <div class="knob-container">
 | 
						|
                        <div class="knob" data-control="volume"><div class="knob-indicator"></div></div>
 | 
						|
                        <span>VOL</span>
 | 
						|
                    </div>
 | 
						|
                    <div class="knob-container">
 | 
						|
                        <div class="knob" data-control="pan"><div class="knob-indicator"></div></div>
 | 
						|
                        <span>PAN</span>
 | 
						|
                    </div>
 | 
						|
                </div>
 | 
						|
            </div>
 | 
						|
            <div class="timeline-container">
 | 
						|
                <div class="spectrogram-view-grid" style="width: ${totalWidth}px;"></div>
 | 
						|
                <div class="playhead"></div>
 | 
						|
            </div>
 | 
						|
        `;
 | 
						|
        newTrackContainer.appendChild(audioTrackLane);
 | 
						|
 | 
						|
        const timelineContainer = audioTrackLane.querySelector('.timeline-container');
 | 
						|
        
 | 
						|
        timelineContainer.addEventListener("dragover", (e) => { e.preventDefault(); audioTrackLane.classList.add('drag-over'); });
 | 
						|
        timelineContainer.addEventListener("dragleave", () => audioTrackLane.classList.remove('drag-over'));
 | 
						|
        timelineContainer.addEventListener("drop", (e) => {
 | 
						|
            e.preventDefault();
 | 
						|
            audioTrackLane.classList.remove('drag-over');
 | 
						|
            const filePath = e.dataTransfer.getData("text/plain");
 | 
						|
            if (!filePath) return;
 | 
						|
            const rect = timelineContainer.getBoundingClientRect();
 | 
						|
            const dropX = e.clientX - rect.left + timelineContainer.scrollLeft;
 | 
						|
            const startTimeInSeconds = dropX / pixelsPerSecond;
 | 
						|
            addAudioClipToTimeline(filePath, trackData.id, startTimeInSeconds);
 | 
						|
        });
 | 
						|
 | 
						|
        const grid = timelineContainer.querySelector('.spectrogram-view-grid');
 | 
						|
        grid.style.setProperty('--bar-width', `${scaledBarWidth}px`);
 | 
						|
        grid.style.setProperty('--four-bar-width', `${scaledBarWidth * 4}px`);
 | 
						|
    });
 | 
						|
 | 
						|
    // --- RENDERIZAÇÃO DOS CLIPS ---
 | 
						|
    appState.audio.clips.forEach(clip => {
 | 
						|
        const parentGrid = newTrackContainer.querySelector(`.audio-track-lane[data-track-id="${clip.trackId}"] .spectrogram-view-grid`);
 | 
						|
        if (!parentGrid) return;
 | 
						|
        const clipElement = document.createElement('div');
 | 
						|
        clipElement.className = 'timeline-clip';
 | 
						|
        clipElement.dataset.clipId = clip.id;
 | 
						|
        clipElement.style.left = `${clip.startTime * pixelsPerSecond}px`;
 | 
						|
        clipElement.style.width = `${clip.duration * pixelsPerSecond}px`;
 | 
						|
        let pitchStr = clip.pitch > 0 ? `+${clip.pitch}` : `${clip.pitch}`;
 | 
						|
        if (clip.pitch === 0) pitchStr = '';
 | 
						|
        clipElement.innerHTML = `
 | 
						|
            <div class="clip-resize-handle left"></div>
 | 
						|
            <span class="clip-name">${clip.name} ${pitchStr}</span>
 | 
						|
            <canvas class="waveform-canvas-clip"></canvas>
 | 
						|
            <div class="clip-resize-handle right"></div>
 | 
						|
        `;
 | 
						|
        parentGrid.appendChild(clipElement);
 | 
						|
        if (clip.player && clip.player.loaded) {
 | 
						|
            const canvas = clipElement.querySelector('.waveform-canvas-clip');
 | 
						|
            canvas.width = clip.duration * pixelsPerSecond;
 | 
						|
            canvas.height = 40;
 | 
						|
            const audioBuffer = clip.player.buffer.get(); 
 | 
						|
            drawWaveform(canvas, audioBuffer, 'var(--accent-green)', clip.offset, clip.duration);
 | 
						|
        }
 | 
						|
        clipElement.addEventListener('wheel', (e) => {
 | 
						|
            e.preventDefault();
 | 
						|
            const clipToUpdate = appState.audio.clips.find(c => c.id == clipElement.dataset.clipId);
 | 
						|
            if (!clipToUpdate) return;
 | 
						|
            const direction = e.deltaY < 0 ? 1 : -1;
 | 
						|
            let newPitch = clipToUpdate.pitch + direction;
 | 
						|
            newPitch = Math.max(-24, Math.min(24, newPitch));
 | 
						|
            updateAudioClipProperties(clipToUpdate.id, { pitch: newPitch });
 | 
						|
            renderAudioEditor();
 | 
						|
            restartAudioEditorIfPlaying();
 | 
						|
        });
 | 
						|
    });
 | 
						|
    
 | 
						|
    // --- SINCRONIZAÇÃO DE SCROLL ENTRE A RÉGUA E AS PISTAS ---
 | 
						|
    newTrackContainer.addEventListener('scroll', () => {
 | 
						|
        const scrollPos = newTrackContainer.scrollLeft;
 | 
						|
        if (ruler.scrollLeft !== scrollPos) {
 | 
						|
            ruler.scrollLeft = scrollPos;
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    // --- EVENT LISTENER PRINCIPAL PARA INTERAÇÕES (MOVER, REDIMENSIONAR, ETC.) ---
 | 
						|
    newTrackContainer.addEventListener('mousedown', (e) => {
 | 
						|
        const currentPixelsPerSecond = getPixelsPerSecond();
 | 
						|
        const handle = e.target.closest('.clip-resize-handle');
 | 
						|
        const clipElement = e.target.closest('.timeline-clip');
 | 
						|
        
 | 
						|
        if (appState.global.sliceToolActive && clipElement) { /* ... lógica de corte ... */ return; }
 | 
						|
        if (handle) { /* ... lógica de redimensionamento de clipe ... */ return; }
 | 
						|
 | 
						|
        if (clipElement) {
 | 
						|
            e.preventDefault();
 | 
						|
            const clipId = clipElement.dataset.clipId;
 | 
						|
            const clickOffsetInClip = e.clientX - clipElement.getBoundingClientRect().left;
 | 
						|
            clipElement.classList.add('dragging');
 | 
						|
            let lastOverLane = clipElement.closest('.audio-track-lane');
 | 
						|
            const onMouseMove = (moveEvent) => {
 | 
						|
                const deltaX = moveEvent.clientX - e.clientX;
 | 
						|
                clipElement.style.transform = `translateX(${deltaX}px)`;
 | 
						|
                const overElement = document.elementFromPoint(moveEvent.clientX, moveEvent.clientY);
 | 
						|
                const overLane = overElement ? overElement.closest('.audio-track-lane') : null;
 | 
						|
                if (overLane && overLane !== lastOverLane) {
 | 
						|
                    if(lastOverLane) lastOverLane.classList.remove('drag-over');
 | 
						|
                    overLane.classList.add('drag-over');
 | 
						|
                    lastOverLane = overLane;
 | 
						|
                }
 | 
						|
            };
 | 
						|
            const onMouseUp = (upEvent) => {
 | 
						|
                clipElement.classList.remove('dragging');
 | 
						|
                if (lastOverLane) lastOverLane.classList.remove('drag-over');
 | 
						|
                clipElement.style.transform = '';
 | 
						|
                document.removeEventListener('mousemove', onMouseMove);
 | 
						|
                document.removeEventListener('mouseup', onMouseUp);
 | 
						|
                const finalLane = lastOverLane;
 | 
						|
                if (!finalLane) return;
 | 
						|
                const newTrackId = finalLane.dataset.trackId;
 | 
						|
                const timelineContainer = finalLane.querySelector('.timeline-container');
 | 
						|
                const wrapperRect = timelineContainer.getBoundingClientRect();
 | 
						|
                const newLeftPx = (upEvent.clientX - wrapperRect.left) - clickOffsetInClip + timelineContainer.scrollLeft;
 | 
						|
                
 | 
						|
                const constrainedLeftPx = Math.max(0, newLeftPx);
 | 
						|
                const newStartTime = constrainedLeftPx / currentPixelsPerSecond;
 | 
						|
 | 
						|
                updateAudioClipProperties(clipId, { trackId: Number(newTrackId), startTime: newStartTime });
 | 
						|
                renderAudioEditor();
 | 
						|
            };
 | 
						|
            document.addEventListener('mousemove', onMouseMove);
 | 
						|
            document.addEventListener('mouseup', onMouseUp);
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        
 | 
						|
        const timelineContainer = e.target.closest('.timeline-container');
 | 
						|
        if (timelineContainer) {
 | 
						|
            e.preventDefault();
 | 
						|
            const handleSeek = (event) => {
 | 
						|
                const rect = timelineContainer.getBoundingClientRect();
 | 
						|
                const scrollLeft = timelineContainer.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);
 | 
						|
        }
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
export function updateAudioEditorUI() {
 | 
						|
    const playBtn = document.getElementById('audio-editor-play-btn');
 | 
						|
    if (!playBtn) return;
 | 
						|
    if (appState.global.isAudioEditorPlaying) {
 | 
						|
        playBtn.classList.remove('fa-play');
 | 
						|
        playBtn.classList.add('fa-pause');
 | 
						|
    } else {
 | 
						|
        playBtn.classList.remove('fa-pause');
 | 
						|
        playBtn.classList.add('fa-play');
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export function updatePlayheadVisual(pixels) {
 | 
						|
    document.querySelectorAll('.audio-track-lane .playhead').forEach(ph => {
 | 
						|
        ph.style.left = `${pixels}px`;
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
export function resetPlayheadVisual() {
 | 
						|
    document.querySelectorAll('.audio-track-lane .playhead').forEach(ph => {
 | 
						|
        ph.style.left = '0px';
 | 
						|
    });
 | 
						|
} |