// 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, PIXELS_PER_STEP, ZOOM_LEVELS } from "../config.js"; import { getPixelsPerSecond, quantizeTime, getBeatsPerBar, getSecondsPerStep } 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 --- 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 = ''; const pixelsPerSecond = getPixelsPerSecond(); let maxTime = appState.global.loopEndTime; appState.audio.clips.forEach(clip => { const endTime = (clip.startTimeInSeconds || 0) + (clip.durationInSeconds || 0); if (endTime > maxTime) maxTime = endTime; }); const containerWidth = existingTrackContainer.offsetWidth; const contentWidth = maxTime * pixelsPerSecond; const totalWidth = Math.max(contentWidth, containerWidth, 2000); ruler.style.width = `${totalWidth}px`; const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex]; const beatsPerBar = getBeatsPerBar(); const stepWidthPx = PIXELS_PER_STEP * zoomFactor; const beatWidthPx = stepWidthPx * 4; const barWidthPx = beatWidthPx * beatsPerBar; if (barWidthPx > 0) { const numberOfBars = Math.ceil(totalWidth / barWidthPx); for (let i = 1; i <= numberOfBars; i++) { const marker = document.createElement('div'); marker.className = 'ruler-marker'; marker.textContent = i; marker.style.left = `${(i - 1) * barWidthPx}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 (sem alterações) --- 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) { /* ... lógica de loop ... */ 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); appState.global.loopStartTime = newStart; } else { newEnd = Math.max(appState.global.loopStartTime + 0.1, initialEnd + deltaTime); appState.global.loopEndTime = newEnd; } updateTransportLoop(); loopRegion.style.left = `${newStart * currentPixelsPerSecond}px`; loopRegion.style.width = `${(newEnd - newStart) * currentPixelsPerSecond}px`; }; const onMouseUp = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); renderAudioEditor(); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); return; } if (loopRegionBody) { /* ... lógica de mover loop ... */ 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(); loopRegion.style.left = `${newStart * currentPixelsPerSecond}px`; }; const onMouseUp = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); renderAudioEditor(); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); return; } e.preventDefault(); const handleSeek = (event) => { /* ... lógica de seek ... */ 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 (sem alterações) --- 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 (sem alterações) --- appState.audio.tracks.forEach(trackData => { const audioTrackLane = document.createElement('div'); audioTrackLane.className = 'audio-track-lane'; audioTrackLane.dataset.trackId = trackData.id; audioTrackLane.innerHTML = `
${trackData.name}
VOL
PAN
`; 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; let startTimeInSeconds = dropX / pixelsPerSecond; startTimeInSeconds = quantizeTime(startTimeInSeconds); addAudioClipToTimeline(filePath, trackData.id, startTimeInSeconds); }); const grid = timelineContainer.querySelector('.spectrogram-view-grid'); grid.style.setProperty('--step-width', `${stepWidthPx}px`); grid.style.setProperty('--beat-width', `${beatWidthPx}px`); grid.style.setProperty('--bar-width', `${barWidthPx}px`); }); // --- RENDERIZAÇÃO DOS CLIPS (COM MODIFICAÇÃO PARA SELEÇÃO) --- 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; // --- INÍCIO DA MODIFICAÇÃO --- // 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 = ''; clipElement.innerHTML = `
${clip.name} ${pitchStr}
`; parentGrid.appendChild(clipElement); if (clip.buffer) { const canvas = clipElement.querySelector('.waveform-canvas-clip'); const canvasWidth = (clip.durationInSeconds || 0) * pixelsPerSecond; if (canvasWidth > 0) { canvas.width = canvasWidth; 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) => { 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 (sem alterações) --- newTrackContainer.addEventListener('scroll', () => { const scrollPos = newTrackContainer.scrollLeft; if (ruler.scrollLeft !== scrollPos) { ruler.scrollLeft = scrollPos; } }); // --- EVENT LISTENER PRINCIPAL (COM MODIFICAÇÃO PARA SELEÇÃO) --- newTrackContainer.addEventListener('mousedown', (e) => { // --- INÍCIO DA MODIFICAÇÃO --- // 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'); // Desseleciona se clicar fora de um clipe (e não for clique direito) 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) { // --- INÍCIO DA MODIFICAÇÃO (SELEÇÃO NO DRAG) --- 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; 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); let newStartTime = constrainedLeftPx / currentPixelsPerSecond; newStartTime = quantizeTime(newStartTime); updateAudioClipProperties(clipId, { trackId: Number(newTrackId), startTimeInSeconds: newStartTime }); renderAudioEditor(); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); return; } const timelineContainer = e.target.closest('.timeline-container'); if (timelineContainer) { // Lógica de 'seek' (sem alterações) 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); } }); // --- 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() { 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'; }); }