tentando obter playlist real do projeto mmp
This commit is contained in:
1 parent
f86bc02ba9
commit
aa084edcb2
66 files changed
+4976
-630
No files matched your search
@@ -28,33 +28,25 @@ export function renderAudioEditor() {
|
||||
if (!audioEditor || !existingTrackContainer) return;
|
||||
|
||||
// --- CORREÇÃO DO ERRO DOMException ---
|
||||
// Identificamos quem é o pai real do container de tracks (agora é .audio-tracks-wrapper)
|
||||
const tracksParent = existingTrackContainer.parentElement;
|
||||
|
||||
// --- CRIAÇÃO E RENDERIZAÇÃO DA RÉGUA ---
|
||||
// Buscamos a régua dentro desse pai correto
|
||||
let rulerWrapper = tracksParent.querySelector(".ruler-wrapper");
|
||||
|
||||
if (!rulerWrapper) {
|
||||
// Se a régua criada pelo JS ainda não existe, cria ela
|
||||
// (Ignora a régua estática do HTML se houver conflito, priorizando esta estrutura)
|
||||
rulerWrapper = document.createElement("div");
|
||||
rulerWrapper.className = "ruler-wrapper";
|
||||
rulerWrapper.innerHTML = `
|
||||
<div class="ruler-spacer"></div>
|
||||
<div class="timeline-ruler"></div>
|
||||
`;
|
||||
|
||||
// Insere ANTES do container de tracks, dentro do pai correto
|
||||
tracksParent.insertBefore(rulerWrapper, existingTrackContainer);
|
||||
}
|
||||
|
||||
// Remove a régua estática antiga se ela ainda estiver lá atrapalhando (opcional, mas recomendado)
|
||||
const staticRuler = tracksParent.querySelector("#audio-timeline-ruler");
|
||||
if (staticRuler && staticRuler.parentElement === tracksParent) {
|
||||
staticRuler.remove();
|
||||
}
|
||||
// Remove elementos soltos antigos se existirem para evitar duplicação
|
||||
const oldLoopRegion = tracksParent.querySelector("#loop-region");
|
||||
const oldPlayhead = tracksParent.querySelector("#playhead");
|
||||
if(oldLoopRegion) oldLoopRegion.remove();
|
||||
@@ -108,12 +100,11 @@ export function renderAudioEditor() {
|
||||
loopRegion.classList.toggle("visible", appState.global.isLoopActive);
|
||||
ruler.appendChild(loopRegion);
|
||||
|
||||
// --- LISTENER DA RÉGUA (MODIFICADO para enviar Ações de Loop/Seek) ---
|
||||
// --- LISTENER DA RÉGUA ---
|
||||
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";
|
||||
|
||||
@@ -125,9 +116,7 @@ export function renderAudioEditor() {
|
||||
if (loopHandle) {
|
||||
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 initialStart = appState.global.loopStartTime;
|
||||
const initialEnd = appState.global.loopEndTime;
|
||||
@@ -151,26 +140,18 @@ export function renderAudioEditor() {
|
||||
const loopRegionEl = newRuler.querySelector("#loop-region");
|
||||
if (loopRegionEl) {
|
||||
loopRegionEl.style.left = `${newStart * currentPixelsPerSecond}px`;
|
||||
loopRegionEl.style.width = `${
|
||||
(newEnd - 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);
|
||||
@@ -200,18 +181,12 @@ export function renderAudioEditor() {
|
||||
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);
|
||||
@@ -226,15 +201,9 @@ export function renderAudioEditor() {
|
||||
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
|
||||
handleSeek(e);
|
||||
const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent);
|
||||
const onMouseUpSeek = () => {
|
||||
document.removeEventListener("mousemove", onMouseMoveSeek);
|
||||
@@ -244,7 +213,7 @@ export function renderAudioEditor() {
|
||||
document.addEventListener("mouseup", onMouseUpSeek);
|
||||
});
|
||||
|
||||
// Menu Contexto Régua (sem alterações)
|
||||
// Menu Contexto Régua
|
||||
newRuler.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById("timeline-context-menu").style.display = "none";
|
||||
@@ -261,32 +230,49 @@ export function renderAudioEditor() {
|
||||
menu.style.top = `${e.clientY}px`;
|
||||
});
|
||||
|
||||
// Recriação Container Pistas (sem alterações)
|
||||
// Recriação Container Pistas
|
||||
const newTrackContainer = existingTrackContainer.cloneNode(false);
|
||||
|
||||
// Substitui no pai correto (tracksParent), não no audioEditor
|
||||
tracksParent.replaceChild(newTrackContainer, existingTrackContainer);
|
||||
|
||||
// Render Pistas (sem alterações)
|
||||
// === RENDERIZAÇÃO DAS PISTAS (LANES) ===
|
||||
appState.audio.tracks.forEach((trackData) => {
|
||||
const audioTrackLane = document.createElement("div");
|
||||
audioTrackLane.className = "audio-track-lane";
|
||||
audioTrackLane.dataset.trackId = trackData.id;
|
||||
|
||||
// Ícone dinâmico baseado no tipo
|
||||
let iconHTML = '<i class="fa-solid fa-music"></i>';
|
||||
if(trackData.type === 'bassline') iconHTML = '<i class="fa-solid fa-th-large" title="Bassline"></i>';
|
||||
|
||||
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-info-header">
|
||||
${iconHTML}
|
||||
<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> `;
|
||||
<div class="spectrogram-view-grid" style="width: ${totalWidth}px;"></div>
|
||||
<div class="playhead"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
newTrackContainer.appendChild(audioTrackLane);
|
||||
const timelineContainer = audioTrackLane.querySelector(
|
||||
".timeline-container"
|
||||
);
|
||||
|
||||
const timelineContainer = audioTrackLane.querySelector(".timeline-container");
|
||||
const grid = timelineContainer.querySelector(".spectrogram-view-grid");
|
||||
|
||||
// Configura variáveis CSS para a grade
|
||||
grid.style.setProperty("--step-width", `${stepWidthPx}px`);
|
||||
grid.style.setProperty("--beat-width", `${beatWidthPx}px`);
|
||||
grid.style.setProperty("--bar-width", `${barWidthPx}px`);
|
||||
|
||||
// --- LÓGICA DE DRAG & DROP (Para Samples de Áudio) ---
|
||||
timelineContainer.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
audioTrackLane.classList.add("drag-over");
|
||||
@@ -303,26 +289,11 @@ export function renderAudioEditor() {
|
||||
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
|
||||
);
|
||||
if (!trackData.id || startTimeInSeconds == null || isNaN(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",
|
||||
@@ -336,13 +307,65 @@ export function renderAudioEditor() {
|
||||
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 (MODIFICADO)
|
||||
// =================================================================
|
||||
// 👇 ADIÇÃO: RENDERIZAÇÃO DOS CLIPES DE BASSLINE (Blocos Azuis)
|
||||
// =================================================================
|
||||
if (trackData.type === "bassline" && trackData.playlist_clips) {
|
||||
trackData.playlist_clips.forEach(clip => {
|
||||
const clipDiv = document.createElement("div");
|
||||
clipDiv.className = "timeline-clip bassline-clip"; // Use CSS para cor
|
||||
|
||||
// CONVERSÃO DE TICKS (MMP) PARA PIXELS (UI)
|
||||
// No LMMS: 1 passo (step) = 12 ticks.
|
||||
// stepWidthPx é a largura de 1 passo na interface atual (com zoom).
|
||||
// Logo: Pixels = (Ticks / 12) * stepWidthPx
|
||||
const steps = clip.pos / 12;
|
||||
const lengthInSteps = clip.len / 12;
|
||||
|
||||
const leftPos = steps * stepWidthPx;
|
||||
const widthDim = lengthInSteps * stepWidthPx;
|
||||
|
||||
clipDiv.style.position = "absolute";
|
||||
clipDiv.style.left = `${leftPos}px`;
|
||||
clipDiv.style.width = `${widthDim}px`;
|
||||
clipDiv.style.height = "100%";
|
||||
clipDiv.style.backgroundColor = "rgba(0, 170, 170, 0.6)"; // Teal LMMS
|
||||
clipDiv.style.border = "1px solid #00aaaa";
|
||||
clipDiv.style.boxSizing = "border-box";
|
||||
clipDiv.style.cursor = "pointer";
|
||||
clipDiv.style.zIndex = "5"; // Garante que fique clicável
|
||||
clipDiv.title = `${clip.name} (Pos: ${clip.pos})`;
|
||||
|
||||
// Label pequeno
|
||||
const label = document.createElement("span");
|
||||
label.innerText = clip.name;
|
||||
label.style.fontSize = "0.7rem";
|
||||
label.style.color = "#fff";
|
||||
label.style.padding = "4px";
|
||||
label.style.pointerEvents = "none";
|
||||
label.style.whiteSpace = "nowrap";
|
||||
label.style.overflow = "hidden";
|
||||
clipDiv.appendChild(label);
|
||||
|
||||
// INTERAÇÃO: Clique duplo para abrir o editor de patterns
|
||||
clipDiv.addEventListener("dblclick", (e) => {
|
||||
e.stopPropagation();
|
||||
if (window.openPatternEditor) {
|
||||
window.openPatternEditor(trackData);
|
||||
} else {
|
||||
console.error("Função window.openPatternEditor não encontrada.");
|
||||
}
|
||||
});
|
||||
|
||||
// Adiciona ao Grid da timeline
|
||||
grid.appendChild(clipDiv);
|
||||
});
|
||||
}
|
||||
});
|
||||
// 👆 FIM DA ADIÇÃO
|
||||
|
||||
// === RENDERIZAÇÃO DE CLIPES DE ÁUDIO (Samples Editáveis) ===
|
||||
appState.audio.clips.forEach((clip) => {
|
||||
const parentGrid = newTrackContainer.querySelector(
|
||||
`.audio-track-lane[data-track-id="${clip.trackId}"] .spectrogram-view-grid`
|
||||
@@ -365,39 +388,24 @@ export function renderAudioEditor() {
|
||||
clip.pitch > 0 ? `+${clip.pitch.toFixed(1)}` : `${clip.pitch.toFixed(1)}`;
|
||||
if (clip.pitch === 0) pitchStr = "";
|
||||
|
||||
// Define o HTML base (sem a visualização de steps ainda)
|
||||
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> `;
|
||||
|
||||
// --- INÍCIO DA MODIFICAÇÃO (Passo 4: Desenhar Steps) ---
|
||||
// (Fazemos isso DEPOIS de definir o innerHTML)
|
||||
|
||||
// 1. Verifica se este clipe tem os dados da "partitura" (steps)
|
||||
// Visualização de Steps (Pattern Clip)
|
||||
if (
|
||||
clip.patternData &&
|
||||
Array.isArray(clip.patternData) &&
|
||||
clip.patternData.length > 0
|
||||
) {
|
||||
// 2. Adiciona a classe CSS principal (do creation.html)
|
||||
clipElement.classList.add("pattern-clip");
|
||||
|
||||
// 3. Determina o número de steps (do primeiro array de trilha)
|
||||
// Assumimos que todos têm o mesmo comprimento, pois vieram do mesmo pattern.
|
||||
const totalSteps = clip.patternData[0]?.length || 0;
|
||||
|
||||
if (totalSteps > 0) {
|
||||
// 4. Chama a nova função (adicionada no final deste arquivo)
|
||||
// para construir o HTML da visualização
|
||||
const patternViewEl = createPatternViewElement(
|
||||
clip.patternData,
|
||||
totalSteps
|
||||
);
|
||||
|
||||
// 5. Adiciona a visualização ao clipe
|
||||
// (O CSS .pattern-clip-view o posicionará sobre o canvas)
|
||||
clipElement.appendChild(patternViewEl);
|
||||
}
|
||||
}
|
||||
// --- FIM DA MODIFICAÇÃO ---
|
||||
|
||||
parentGrid.appendChild(clipElement);
|
||||
|
||||
@@ -449,7 +457,7 @@ export function renderAudioEditor() {
|
||||
});
|
||||
});
|
||||
|
||||
// Sync Scroll (sem alterações)
|
||||
// Sync Scroll
|
||||
newTrackContainer.addEventListener("scroll", () => {
|
||||
const scrollPos = newTrackContainer.scrollLeft;
|
||||
const mainRuler = document.querySelector(".timeline-ruler");
|
||||
@@ -458,12 +466,12 @@ export function renderAudioEditor() {
|
||||
}
|
||||
});
|
||||
|
||||
// Event Listener Principal (mousedown no container de pistas)
|
||||
// Event Listener Principal (mousedown)
|
||||
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) {
|
||||
@@ -478,7 +486,7 @@ export function renderAudioEditor() {
|
||||
const handle = e.target.closest(".clip-resize-handle");
|
||||
|
||||
// Slice Tool
|
||||
if (appState.global.sliceToolActive && clipElement) {
|
||||
if (appState.global.sliceToolActive && clipElement && !clipElement.classList.contains("bassline-clip")) { // Protege bassline
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const clipId = clipElement.dataset.clipId;
|
||||
@@ -502,13 +510,14 @@ export function renderAudioEditor() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resize Handle
|
||||
// Resize Handle (Apenas para Audio Clips por enquanto)
|
||||
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();
|
||||
@@ -517,175 +526,84 @@ export function renderAudioEditor() {
|
||||
const initialStartTime = clip.startTimeInSeconds;
|
||||
const initialDuration = clip.durationInSeconds;
|
||||
const initialOffset = clip.offset || 0;
|
||||
const initialOriginalDuration =
|
||||
clip.originalDuration || clip.buffer.duration;
|
||||
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
|
||||
);
|
||||
newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime);
|
||||
const maxEndTime = bufferStartTime + initialOriginalDuration;
|
||||
newEndTime = Math.min(newEndTime, maxEndTime);
|
||||
clipElement.style.width = `${
|
||||
(newEndTime - initialStartTime) * currentPixelsPerSecond
|
||||
}px`;
|
||||
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;
|
||||
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;
|
||||
const newWidthFinalPx = (initialStartTime + initialDuration - newStartTime) * currentPixelsPerSecond;
|
||||
clipElement.style.left = `${newLeftFinalPx}px`;
|
||||
clipElement.style.width = `${newWidthFinalPx}px`;
|
||||
}
|
||||
} else if (appState.global.resizeMode === "stretch") {
|
||||
// (Lógica de Stretch omitida por brevidade, mas mantida igual ao original)
|
||||
// ... (mantido igual ao original)
|
||||
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);
|
||||
// Aplicação das mudanças (mantida lógica original)
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
const newOffset = newStartTime - bufferStartTime;
|
||||
if(handleType === "right") {
|
||||
updateAudioClipProperties(clipId, { durationInSeconds: newDuration, pitch: 0 });
|
||||
sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { durationInSeconds: newDuration, pitch: 0 } });
|
||||
} else {
|
||||
updateAudioClipProperties(clipId, { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, offset: newOffset, pitch: 0 });
|
||||
sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, offset: newOffset, pitch: 0 } });
|
||||
}
|
||||
} else {
|
||||
// Stretch Logic update
|
||||
const newPlaybackRate = initialOriginalDuration / newDuration;
|
||||
const newPitch = 12 * Math.log2(newPlaybackRate);
|
||||
if(handleType === "right") {
|
||||
updateAudioClipProperties(clipId, { durationInSeconds: newDuration, pitch: newPitch, offset: 0 });
|
||||
sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { durationInSeconds: newDuration, pitch: newPitch, offset: 0 } });
|
||||
} else {
|
||||
updateAudioClipProperties(clipId, { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, pitch: newPitch, offset: 0 });
|
||||
sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, pitch: newPitch, offset: 0 } });
|
||||
}
|
||||
}
|
||||
restartAudioEditorIfPlaying();
|
||||
renderAudioEditor();
|
||||
@@ -695,71 +613,50 @@ export function renderAudioEditor() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Drag Clip
|
||||
if (clipElement) {
|
||||
// Drag Clip (Audio apenas, por enquanto)
|
||||
if (clipElement && !clipElement.classList.contains("bassline-clip")) {
|
||||
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"));
|
||||
newTrackContainer.querySelectorAll(".timeline-clip.selected").forEach((c) => c.classList.remove("selected"));
|
||||
clipElement.classList.add("selected");
|
||||
}
|
||||
e.preventDefault();
|
||||
const clickOffsetInClip =
|
||||
e.clientX - clipElement.getBoundingClientRect().left;
|
||||
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;
|
||||
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 newTrackId = finalLane.dataset.trackId;
|
||||
const timelineContainer = finalLane.querySelector(".timeline-container");
|
||||
const wrapperRect = timelineContainer.getBoundingClientRect();
|
||||
const newLeftPx =
|
||||
upEvent.clientX -
|
||||
wrapperRect.left -
|
||||
clickOffsetInClip +
|
||||
timelineContainer.scrollLeft;
|
||||
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: 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);
|
||||
}
|
||||
|
||||
updateAudioClipProperties(clipId, { trackId: newTrackId, startTimeInSeconds: newStartTime });
|
||||
sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { trackId: newTrackId, startTimeInSeconds: newStartTime } });
|
||||
renderAudioEditor();
|
||||
};
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
@@ -777,10 +674,9 @@ export function renderAudioEditor() {
|
||||
const clickX = event.clientX - rect.left;
|
||||
const absoluteX = clickX + scrollLeft;
|
||||
const newTime = absoluteX / currentPixelsPerSecond;
|
||||
// Sincronia de Seek na Pista)
|
||||
sendAction({ type: "SET_SEEK_TIME", seekTime: newTime });
|
||||
};
|
||||
handleSeek(e); // Aplica no mousedown
|
||||
handleSeek(e);
|
||||
const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent);
|
||||
const onMouseUpSeek = () => {
|
||||
document.removeEventListener("mousemove", onMouseMoveSeek);
|
||||
@@ -791,26 +687,34 @@ export function renderAudioEditor() {
|
||||
}
|
||||
});
|
||||
|
||||
// Menu Contexto Pista (sem alterações)
|
||||
// Menu Contexto Pista
|
||||
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");
|
||||
|
||||
// Itens do menu
|
||||
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) {
|
||||
// Bloqueia ações em Bassline clips por enquanto (pois são read-only na playlist)
|
||||
if(clipElement.classList.contains("bassline-clip")) {
|
||||
menu.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
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"));
|
||||
newTrackContainer.querySelectorAll(".timeline-clip.selected").forEach((c) => c.classList.remove("selected"));
|
||||
clipElement.classList.add("selected");
|
||||
}
|
||||
copyItem.style.display = "block";
|
||||
@@ -819,17 +723,14 @@ export function renderAudioEditor() {
|
||||
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);
|
||||
}
|
||||
sendActionSafe({ type: "REMOVE_AUDIO_CLIP", clipId: id });
|
||||
if (ok) renderAudioEditor();
|
||||
menu.style.display = "none";
|
||||
});
|
||||
@@ -849,7 +750,16 @@ export function renderAudioEditor() {
|
||||
});
|
||||
}
|
||||
|
||||
// Funções de UI (sem alterações)
|
||||
// Helper para envio seguro
|
||||
function sendActionSafe(action) {
|
||||
try {
|
||||
sendAction(action);
|
||||
} catch (err) {
|
||||
console.warn("[SYNC] Falha ao emitir ação:", action.type, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Funções de UI
|
||||
export function updateAudioEditorUI() {
|
||||
const playBtn = document.getElementById("audio-editor-play-btn");
|
||||
if (!playBtn) return;
|
||||
@@ -872,55 +782,38 @@ export function resetPlayheadVisual() {
|
||||
});
|
||||
}
|
||||
|
||||
// --- A Função de Desenho) ---
|
||||
// (Adicionada ao final de audio_ui.js)
|
||||
|
||||
/**
|
||||
* Cria o elemento HTML (e seus filhos) para a visualização
|
||||
* dos steps de um pattern clip.
|
||||
* * @param {Array<Array<boolean>>} patternData - ex: [[true, false], [true, true]]
|
||||
* @returns {HTMLElement} Um <div> com a classe 'pattern-clip-view'
|
||||
*/
|
||||
function createPatternViewElement(patternData) {
|
||||
const view = document.createElement("div");
|
||||
view.className = "pattern-clip-view"; // (do creation.html)
|
||||
view.className = "pattern-clip-view";
|
||||
|
||||
// Filtra trilhas que possam ser vazias ou inválidas no array
|
||||
const validTracksData = patternData.filter(
|
||||
(steps) => Array.isArray(steps) && steps.length > 0
|
||||
);
|
||||
|
||||
// Encontra o total de steps (usando a trilha mais longa como referência)
|
||||
const totalSteps = validTracksData.reduce(
|
||||
(max, steps) => Math.max(max, steps.length),
|
||||
0
|
||||
);
|
||||
if (totalSteps === 0) return view; // Retorna view vazia se não houver steps
|
||||
if (totalSteps === 0) return view;
|
||||
|
||||
validTracksData.forEach((trackSteps) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "pattern-clip-track-row"; // (do creation.html)
|
||||
|
||||
// Calcula a largura de cada step como porcentagem
|
||||
row.className = "pattern-clip-track-row";
|
||||
const stepWidthPercent = (1 / totalSteps) * 100;
|
||||
|
||||
for (let i = 0; i < totalSteps; i++) {
|
||||
// Se o step[i] for true, desenha a nota
|
||||
if (trackSteps[i] === true) {
|
||||
const note = document.createElement("div");
|
||||
note.className = "pattern-step-note"; // (do creation.html)
|
||||
|
||||
// Define a posição (left) e a largura (width) em porcentagem
|
||||
// Isso permite que o clip seja redimensionado (stretch)
|
||||
// e as notas se ajustem.
|
||||
note.className = "pattern-step-note";
|
||||
note.style.left = `${(i / totalSteps) * 100}%`;
|
||||
note.style.width = `${stepWidthPercent}%`;
|
||||
|
||||
row.appendChild(note);
|
||||
}
|
||||
}
|
||||
view.appendChild(row);
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
Reference in new issue
Block a user