tentando obter playlist real do projeto mmp

This commit is contained in:
JotaChina committed 2025-12-20 12:09:37 -03:00
1 parent f86bc02ba9
commit aa084edcb2
66 files changed
+4976 -630

No files matched your search

+194 -301
View File
@@ -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;
}