820 lines
32 KiB
JavaScript
820 lines
32 KiB
JavaScript
// js/audio/audio_ui.js
|
|
import { appState } from "../state.js";
|
|
import {
|
|
addAudioClipToTimeline,
|
|
updateAudioClipProperties,
|
|
sliceAudioClip,
|
|
removeAudioClip,
|
|
} 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";
|
|
import { sendAction } from "../socket.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 = `
|
|
<div class="ruler-spacer"></div>
|
|
<div class="timeline-ruler"></div>
|
|
`;
|
|
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 = `<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 (MODIFICADO para enviar Ações de Loop/Seek) ---
|
|
const newRuler = ruler.cloneNode(true);
|
|
ruler.parentNode.replaceChild(newRuler, ruler);
|
|
|
|
newRuler.addEventListener("mousedown", (e) => {
|
|
// Esconde menus
|
|
document.getElementById("timeline-context-menu").style.display = "none";
|
|
document.getElementById("ruler-context-menu").style.display = "none";
|
|
|
|
const currentPixelsPerSecond = getPixelsPerSecond();
|
|
const loopHandle = e.target.closest(".loop-handle");
|
|
const loopRegionBody = e.target.closest("#loop-region:not(.loop-handle)");
|
|
|
|
// Drag Handle Loop
|
|
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);
|
|
appState.global.loopStartTime = newStart;
|
|
} else {
|
|
newEnd = Math.max(
|
|
appState.global.loopStartTime + 0.1,
|
|
initialEnd + deltaTime
|
|
);
|
|
appState.global.loopEndTime = newEnd;
|
|
}
|
|
updateTransportLoop();
|
|
const loopRegionEl = newRuler.querySelector("#loop-region");
|
|
if (loopRegionEl) {
|
|
loopRegionEl.style.left = `${newStart * currentPixelsPerSecond}px`;
|
|
loopRegionEl.style.width = `${
|
|
(newEnd - newStart) * currentPixelsPerSecond
|
|
}px`;
|
|
}
|
|
};
|
|
const onMouseUp = () => {
|
|
document.removeEventListener("mousemove", onMouseMove);
|
|
document.removeEventListener("mouseup", onMouseUp);
|
|
// =================================================================
|
|
// 👇 INÍCIO DA CORREÇÃO (Sincronia de Loop Drag Handle)
|
|
// =================================================================
|
|
sendAction({
|
|
type: "SET_LOOP_STATE",
|
|
isLoopActive: appState.global.isLoopActive,
|
|
loopStartTime: appState.global.loopStartTime,
|
|
loopEndTime: appState.global.loopEndTime,
|
|
});
|
|
// renderAudioEditor(); // Removido
|
|
// =================================================================
|
|
// 👆 FIM DA CORREÇÃO
|
|
};
|
|
document.addEventListener("mousemove", onMouseMove);
|
|
document.addEventListener("mouseup", onMouseUp);
|
|
return;
|
|
}
|
|
|
|
// Drag Body Loop
|
|
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();
|
|
const loopRegionEl = newRuler.querySelector("#loop-region");
|
|
if (loopRegionEl)
|
|
loopRegionEl.style.left = `${newStart * currentPixelsPerSecond}px`;
|
|
};
|
|
const onMouseUp = () => {
|
|
document.removeEventListener("mousemove", onMouseMove);
|
|
document.removeEventListener("mouseup", onMouseUp);
|
|
// =================================================================
|
|
// 👇 INÍCIO DA CORREÇÃO (Sincronia de Loop Drag Body)
|
|
// =================================================================
|
|
sendAction({
|
|
type: "SET_LOOP_STATE",
|
|
isLoopActive: appState.global.isLoopActive,
|
|
loopStartTime: appState.global.loopStartTime,
|
|
loopEndTime: appState.global.loopEndTime,
|
|
});
|
|
// renderAudioEditor(); // Removido
|
|
// =================================================================
|
|
// 👆 FIM DA CORREÇÃO
|
|
};
|
|
document.addEventListener("mousemove", onMouseMove);
|
|
document.addEventListener("mouseup", onMouseUp);
|
|
return;
|
|
}
|
|
|
|
// Seek na Régua
|
|
e.preventDefault();
|
|
const handleSeek = (event) => {
|
|
const rect = newRuler.getBoundingClientRect();
|
|
const scrollLeft = newRuler.scrollLeft;
|
|
const clickX = event.clientX - rect.left;
|
|
const absoluteX = clickX + scrollLeft;
|
|
const newTime = absoluteX / currentPixelsPerSecond;
|
|
// =================================================================
|
|
// 👇 INÍCIO DA CORREÇÃO (Sincronia de Seek na Régua)
|
|
// =================================================================
|
|
sendAction({ type: "SET_SEEK_TIME", seekTime: newTime });
|
|
// seekAudioEditor(newTime); // 👈 Substituído
|
|
// =================================================================
|
|
// 👆 FIM DA CORREÇÃO
|
|
};
|
|
handleSeek(e); // Aplica no mousedown
|
|
const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent);
|
|
const onMouseUpSeek = () => {
|
|
document.removeEventListener("mousemove", onMouseMoveSeek);
|
|
document.removeEventListener("mouseup", onMouseUpSeek);
|
|
};
|
|
document.addEventListener("mousemove", onMouseMoveSeek);
|
|
document.addEventListener("mouseup", onMouseUpSeek);
|
|
});
|
|
|
|
// Menu Contexto Régua (sem alterações)
|
|
newRuler.addEventListener("contextmenu", (e) => {
|
|
e.preventDefault();
|
|
document.getElementById("timeline-context-menu").style.display = "none";
|
|
const menu = document.getElementById("ruler-context-menu");
|
|
const currentPixelsPerSecond = getPixelsPerSecond();
|
|
const rect = newRuler.getBoundingClientRect();
|
|
const scrollLeft = newRuler.scrollLeft;
|
|
const clickX = e.clientX - rect.left;
|
|
const absoluteX = clickX + scrollLeft;
|
|
const clickTime = absoluteX / currentPixelsPerSecond;
|
|
appState.global.lastRulerClickTime = clickTime;
|
|
menu.style.display = "block";
|
|
menu.style.left = `${e.clientX}px`;
|
|
menu.style.top = `${e.clientY}px`;
|
|
});
|
|
|
|
// Recriação Container Pistas (sem alterações)
|
|
const newTrackContainer = existingTrackContainer.cloneNode(false);
|
|
audioEditor.replaceChild(newTrackContainer, existingTrackContainer);
|
|
|
|
// Render Pistas (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 = `
|
|
<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;
|
|
let startTimeInSeconds = dropX / pixelsPerSecond;
|
|
startTimeInSeconds = quantizeTime(startTimeInSeconds);
|
|
if (
|
|
!trackData.id ||
|
|
startTimeInSeconds == null ||
|
|
isNaN(startTimeInSeconds)
|
|
) {
|
|
console.error("Drop inválido. Ignorando.", {
|
|
id: trackData.id,
|
|
time: startTimeInSeconds,
|
|
});
|
|
return;
|
|
}
|
|
const clipId =
|
|
crypto?.randomUUID?.() ||
|
|
`clip_${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
|
|
addAudioClipToTimeline(
|
|
filePath,
|
|
trackData.id,
|
|
startTimeInSeconds,
|
|
clipId
|
|
);
|
|
try {
|
|
sendAction({
|
|
type: "ADD_AUDIO_CLIP",
|
|
filePath,
|
|
trackId: trackData.id,
|
|
startTimeInSeconds,
|
|
clipId,
|
|
name: String(filePath).split(/[\\/]/).pop(),
|
|
});
|
|
} catch (err) {
|
|
console.warn("[SYNC] Falha ao emitir ADD_AUDIO_CLIP", err);
|
|
}
|
|
});
|
|
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`);
|
|
});
|
|
|
|
// Render Clips (sem alterações)
|
|
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;
|
|
if (clip.id === appState.global.selectedClipId)
|
|
clipElement.classList.add("selected");
|
|
if (appState.global.clipboard?.cutSourceId === clip.id)
|
|
clipElement.classList.add("cut");
|
|
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 = ` <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.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;
|
|
const isStretched = clip.pitch !== 0;
|
|
const sourceOffset = isStretched ? 0 : clip.offset || 0;
|
|
const sourceDuration = isStretched
|
|
? clip.originalDuration
|
|
: clip.durationInSeconds;
|
|
drawWaveform(
|
|
canvas,
|
|
audioBuffer,
|
|
"var(--accent-green)",
|
|
sourceOffset,
|
|
sourceDuration
|
|
);
|
|
}
|
|
}
|
|
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 });
|
|
try {
|
|
sendAction({
|
|
type: "UPDATE_AUDIO_CLIP",
|
|
clipId: clipToUpdate.id,
|
|
props: { pitch: newPitch },
|
|
});
|
|
} catch (err) {
|
|
console.warn("[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (wheel)", err);
|
|
}
|
|
renderAudioEditor();
|
|
restartAudioEditorIfPlaying();
|
|
});
|
|
});
|
|
|
|
// Sync Scroll (sem alterações)
|
|
newTrackContainer.addEventListener("scroll", () => {
|
|
const scrollPos = newTrackContainer.scrollLeft;
|
|
const mainRuler = document.querySelector(".timeline-ruler");
|
|
if (mainRuler && mainRuler.scrollLeft !== scrollPos) {
|
|
mainRuler.scrollLeft = scrollPos;
|
|
}
|
|
});
|
|
|
|
// Event Listener Principal (mousedown no container de pistas)
|
|
newTrackContainer.addEventListener("mousedown", (e) => {
|
|
// Esconde menus
|
|
document.getElementById("timeline-context-menu").style.display = "none";
|
|
document.getElementById("ruler-context-menu").style.display = "none";
|
|
const clipElement = e.target.closest(".timeline-clip");
|
|
// Desseleciona se clicar fora
|
|
if (!clipElement && e.button !== 2) {
|
|
if (appState.global.selectedClipId) {
|
|
appState.global.selectedClipId = null;
|
|
newTrackContainer
|
|
.querySelectorAll(".timeline-clip.selected")
|
|
.forEach((c) => c.classList.remove("selected"));
|
|
}
|
|
}
|
|
|
|
const currentPixelsPerSecond = getPixelsPerSecond();
|
|
const handle = e.target.closest(".clip-resize-handle");
|
|
|
|
// Slice Tool
|
|
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);
|
|
try {
|
|
sendAction({
|
|
type: "UPDATE_AUDIO_CLIP",
|
|
clipId,
|
|
props: { __operation: "slice", sliceTimeInTimeline },
|
|
});
|
|
} catch (err) {
|
|
console.warn("[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (slice)", err);
|
|
}
|
|
renderAudioEditor();
|
|
return;
|
|
}
|
|
|
|
// Resize Handle
|
|
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 initialOriginalDuration =
|
|
clip.originalDuration || clip.buffer.duration;
|
|
const bufferStartTime = initialStartTime - initialOffset;
|
|
const onMouseMove = (moveEvent) => {
|
|
const deltaX = moveEvent.clientX - initialMouseX;
|
|
// Trim Mode
|
|
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`;
|
|
}
|
|
}
|
|
// Stretch Mode
|
|
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;
|
|
// Trim Mode
|
|
if (appState.global.resizeMode === "trim") {
|
|
const newOffset = newStartTime - bufferStartTime;
|
|
if (handleType === "right") {
|
|
updateAudioClipProperties(clipId, {
|
|
durationInSeconds: newDuration,
|
|
pitch: 0,
|
|
});
|
|
try {
|
|
sendAction({
|
|
type: "UPDATE_AUDIO_CLIP",
|
|
clipId,
|
|
props: { durationInSeconds: newDuration, pitch: 0 },
|
|
});
|
|
} catch (err) {
|
|
console.warn(
|
|
"[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (trim-right)",
|
|
err
|
|
);
|
|
}
|
|
} else if (handleType === "left") {
|
|
updateAudioClipProperties(clipId, {
|
|
startTimeInSeconds: newStartTime,
|
|
durationInSeconds: newDuration,
|
|
offset: newOffset,
|
|
pitch: 0,
|
|
});
|
|
try {
|
|
sendAction({
|
|
type: "UPDATE_AUDIO_CLIP",
|
|
clipId,
|
|
props: {
|
|
startTimeInSeconds: newStartTime,
|
|
durationInSeconds: newDuration,
|
|
offset: newOffset,
|
|
pitch: 0,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
console.warn(
|
|
"[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (trim-left)",
|
|
err
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// Stretch Mode
|
|
else if (appState.global.resizeMode === "stretch") {
|
|
const newPlaybackRate = initialOriginalDuration / newDuration;
|
|
const newPitch = 12 * Math.log2(newPlaybackRate);
|
|
if (handleType === "right") {
|
|
updateAudioClipProperties(clipId, {
|
|
durationInSeconds: newDuration,
|
|
pitch: newPitch,
|
|
offset: 0,
|
|
});
|
|
try {
|
|
sendAction({
|
|
type: "UPDATE_AUDIO_CLIP",
|
|
clipId,
|
|
props: {
|
|
durationInSeconds: newDuration,
|
|
pitch: newPitch,
|
|
offset: 0,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
console.warn(
|
|
"[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (stretch-right)",
|
|
err
|
|
);
|
|
}
|
|
} else if (handleType === "left") {
|
|
updateAudioClipProperties(clipId, {
|
|
startTimeInSeconds: newStartTime,
|
|
durationInSeconds: newDuration,
|
|
pitch: newPitch,
|
|
offset: 0,
|
|
});
|
|
try {
|
|
sendAction({
|
|
type: "UPDATE_AUDIO_CLIP",
|
|
clipId,
|
|
props: {
|
|
startTimeInSeconds: newStartTime,
|
|
durationInSeconds: newDuration,
|
|
pitch: newPitch,
|
|
offset: 0,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
console.warn(
|
|
"[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (stretch-left)",
|
|
err
|
|
);
|
|
}
|
|
}
|
|
}
|
|
restartAudioEditorIfPlaying();
|
|
renderAudioEditor();
|
|
};
|
|
document.addEventListener("mousemove", onMouseMove);
|
|
document.addEventListener("mouseup", onMouseUp);
|
|
return;
|
|
}
|
|
|
|
// Drag Clip
|
|
if (clipElement) {
|
|
const clipId = clipElement.dataset.clipId;
|
|
if (appState.global.selectedClipId !== clipId) {
|
|
appState.global.selectedClipId = clipId;
|
|
newTrackContainer
|
|
.querySelectorAll(".timeline-clip.selected")
|
|
.forEach((c) => c.classList.remove("selected"));
|
|
clipElement.classList.add("selected");
|
|
}
|
|
e.preventDefault();
|
|
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; // (é uma string)
|
|
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);
|
|
// (Correção Bug 4 - remove Number())
|
|
updateAudioClipProperties(clipId, {
|
|
trackId: newTrackId,
|
|
startTimeInSeconds: newStartTime,
|
|
});
|
|
try {
|
|
sendAction({
|
|
type: "UPDATE_AUDIO_CLIP",
|
|
clipId,
|
|
props: { trackId: newTrackId, startTimeInSeconds: newStartTime },
|
|
});
|
|
} catch (err) {
|
|
console.warn("[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (move)", err);
|
|
}
|
|
renderAudioEditor();
|
|
};
|
|
document.addEventListener("mousemove", onMouseMove);
|
|
document.addEventListener("mouseup", onMouseUp);
|
|
return;
|
|
}
|
|
|
|
// Seek na Pista
|
|
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;
|
|
// =================================================================
|
|
// 👇 INÍCIO DA CORREÇÃO (Sincronia de Seek na Pista)
|
|
// =================================================================
|
|
sendAction({ type: "SET_SEEK_TIME", seekTime: newTime });
|
|
// seekAudioEditor(newTime); // 👈 Substituído
|
|
// =================================================================
|
|
// 👆 FIM DA CORREÇÃO
|
|
};
|
|
handleSeek(e); // Aplica no mousedown
|
|
const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent);
|
|
const onMouseUpSeek = () => {
|
|
document.removeEventListener("mousemove", onMouseMoveSeek);
|
|
document.removeEventListener("mouseup", onMouseUpSeek);
|
|
};
|
|
document.addEventListener("mousemove", onMouseMoveSeek);
|
|
document.addEventListener("mouseup", onMouseUpSeek);
|
|
}
|
|
});
|
|
|
|
// Menu Contexto Pista (sem alterações)
|
|
newTrackContainer.addEventListener("contextmenu", (e) => {
|
|
e.preventDefault();
|
|
document.getElementById("ruler-context-menu").style.display = "none";
|
|
const menu = document.getElementById("timeline-context-menu");
|
|
if (!menu) return;
|
|
const clipElement = e.target.closest(".timeline-clip");
|
|
const copyItem = document.getElementById("copy-clip");
|
|
const cutItem = document.getElementById("cut-clip");
|
|
const pasteItem = document.getElementById("paste-clip");
|
|
const deleteItem = document.getElementById("delete-clip");
|
|
const canPaste = appState.global.clipboard?.type === "audio";
|
|
pasteItem.style.display = canPaste ? "block" : "none";
|
|
if (clipElement) {
|
|
const clipId = clipElement.dataset.clipId;
|
|
if (appState.global.selectedClipId !== clipId) {
|
|
appState.global.selectedClipId = clipId;
|
|
newTrackContainer
|
|
.querySelectorAll(".timeline-clip.selected")
|
|
.forEach((c) => c.classList.remove("selected"));
|
|
clipElement.classList.add("selected");
|
|
}
|
|
copyItem.style.display = "block";
|
|
cutItem.style.display = "block";
|
|
deleteItem.style.display = "block";
|
|
menu.style.display = "block";
|
|
menu.style.left = `${e.clientX}px`;
|
|
menu.style.top = `${e.clientY}px`;
|
|
if (!deleteItem.__synced) {
|
|
deleteItem.__synced = true;
|
|
deleteItem.addEventListener("click", () => {
|
|
const id = appState.global.selectedClipId;
|
|
if (!id) return;
|
|
const ok = removeAudioClip(id);
|
|
try {
|
|
sendAction({ type: "REMOVE_AUDIO_CLIP", clipId: id });
|
|
} catch (err) {
|
|
console.warn("[SYNC] Falha ao emitir REMOVE_AUDIO_CLIP", err);
|
|
}
|
|
if (ok) renderAudioEditor();
|
|
menu.style.display = "none";
|
|
});
|
|
}
|
|
} else {
|
|
copyItem.style.display = "none";
|
|
cutItem.style.display = "none";
|
|
deleteItem.style.display = "none";
|
|
if (canPaste) {
|
|
menu.style.display = "block";
|
|
menu.style.left = `${e.clientX}px`;
|
|
menu.style.top = `${e.clientY}px`;
|
|
} else {
|
|
menu.style.display = "none";
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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";
|
|
});
|
|
}
|