tentando obter playlist real do projeto mmp
This commit is contained in:
1 parent
3380b642b3
commit
7b569c1bde
4 files changed
+203
-223
No files matched your search
@@ -1,8 +1,6 @@
|
||||
// js/pattern_ui.js
|
||||
// js/pattern/pattern_ui.js
|
||||
import { appState } from "../state.js";
|
||||
import {
|
||||
updateTrackSample
|
||||
} from "./pattern_state.js";
|
||||
import { updateTrackSample } from "./pattern_state.js";
|
||||
import { playSample, stopPlayback } from "./pattern_audio.js";
|
||||
import { getTotalSteps } from "../utils.js";
|
||||
import { sendAction } from '../socket.js';
|
||||
@@ -14,10 +12,85 @@ export function renderPatternEditor() {
|
||||
const trackContainer = document.getElementById("track-container");
|
||||
trackContainer.innerHTML = "";
|
||||
|
||||
appState.pattern.tracks.forEach((trackData, trackIndex) => {
|
||||
// 1. LÓGICA DE FILTRAGEM E CONTEXTO
|
||||
// Decide quais trilhas mostrar baseando-se no ID de Bassline em foco
|
||||
let tracksToRender = [];
|
||||
let isFocusedMode = false;
|
||||
let contextName = "Song Editor"; // Nome da visualização atual
|
||||
|
||||
if (appState.pattern.focusedBasslineId) {
|
||||
isFocusedMode = true;
|
||||
// Mostra apenas os filhos da Bassline selecionada
|
||||
tracksToRender = appState.pattern.tracks.filter(t => t.parentBasslineId === appState.pattern.focusedBasslineId);
|
||||
|
||||
// Busca o nome para exibir no título
|
||||
const basslineTrack = appState.pattern.tracks.find(t => t.id === appState.pattern.focusedBasslineId);
|
||||
if (basslineTrack) contextName = basslineTrack.name;
|
||||
} else {
|
||||
// Modo Padrão: Mostra trilhas da raiz (sem pai) e que NÃO são containers de bassline
|
||||
tracksToRender = appState.pattern.tracks.filter(t => t.type !== 'bassline' && t.parentBasslineId === null);
|
||||
|
||||
// Fallback: Se a lista ficar vazia (projeto antigo ou mal formatado), mostra tudo que não é container
|
||||
if (tracksToRender.length === 0) {
|
||||
tracksToRender = appState.pattern.tracks.filter(t => t.type !== 'bassline');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. RENDERIZAÇÃO DO CABEÇALHO DE NAVEGAÇÃO
|
||||
const navHeader = document.createElement("div");
|
||||
navHeader.className = "editor-nav-header";
|
||||
// Estilos inline para garantir aparência imediata (mova para CSS depois se preferir)
|
||||
navHeader.style.padding = "8px 12px";
|
||||
navHeader.style.backgroundColor = "#2b2b2b";
|
||||
navHeader.style.marginBottom = "8px";
|
||||
navHeader.style.display = "flex";
|
||||
navHeader.style.alignItems = "center";
|
||||
navHeader.style.justifyContent = "space-between";
|
||||
navHeader.style.color = "#ddd";
|
||||
navHeader.style.borderBottom = "1px solid #444";
|
||||
navHeader.style.fontSize = "0.9rem";
|
||||
|
||||
const titleIcon = isFocusedMode ? "fa-th-large" : "fa-music";
|
||||
const title = document.createElement("span");
|
||||
title.innerHTML = `<i class="fa-solid ${titleIcon}" style="margin-right:8px"></i> <strong>${contextName}</strong>`;
|
||||
navHeader.appendChild(title);
|
||||
|
||||
// Botão Voltar (Aparece apenas se estiver dentro de uma Bassline)
|
||||
if (isFocusedMode) {
|
||||
const backBtn = document.createElement("button");
|
||||
backBtn.className = "control-btn";
|
||||
backBtn.innerHTML = `<i class="fa-solid fa-arrow-left"></i> Voltar`;
|
||||
backBtn.style.padding = "4px 10px";
|
||||
backBtn.style.backgroundColor = "#444";
|
||||
backBtn.style.border = "1px solid #555";
|
||||
backBtn.style.borderRadius = "4px";
|
||||
backBtn.style.color = "white";
|
||||
backBtn.style.cursor = "pointer";
|
||||
|
||||
backBtn.onmouseover = () => backBtn.style.backgroundColor = "#555";
|
||||
backBtn.onmouseout = () => backBtn.style.backgroundColor = "#444";
|
||||
|
||||
backBtn.onclick = () => {
|
||||
if (window.exitPatternFocus) {
|
||||
window.exitPatternFocus();
|
||||
} else {
|
||||
console.error("Função window.exitPatternFocus não encontrada no escopo global.");
|
||||
}
|
||||
};
|
||||
navHeader.appendChild(backBtn);
|
||||
}
|
||||
|
||||
trackContainer.appendChild(navHeader);
|
||||
|
||||
// 3. RENDERIZAÇÃO DAS TRILHAS FILTRADAS
|
||||
tracksToRender.forEach((trackData) => {
|
||||
// IMPORTANTE: Precisamos encontrar o índice real no array global
|
||||
// para que as ações (volume, pan, delete) afetem a trilha certa.
|
||||
const originalIndex = appState.pattern.tracks.findIndex(t => t.id === trackData.id);
|
||||
|
||||
const trackLane = document.createElement("div");
|
||||
trackLane.className = "track-lane";
|
||||
trackLane.dataset.trackIndex = trackIndex;
|
||||
trackLane.dataset.trackIndex = originalIndex;
|
||||
|
||||
if (trackData.id === appState.pattern.activeTrackId) {
|
||||
trackLane.classList.add('active-track');
|
||||
@@ -25,9 +98,9 @@ export function renderPatternEditor() {
|
||||
|
||||
trackLane.innerHTML = `
|
||||
<div class="track-info">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<div class="track-mute"></div>
|
||||
<span class="track-name">${trackData.name}</span>
|
||||
<i class="fa-solid fa-gear" title="Configurações do Instrumento"></i>
|
||||
<div class="track-mute" title="Mute"></div>
|
||||
<span class="track-name" title="${trackData.name}">${trackData.name}</span>
|
||||
</div>
|
||||
<div class="track-controls">
|
||||
<div class="knob-container">
|
||||
@@ -42,9 +115,10 @@ export function renderPatternEditor() {
|
||||
<div class="step-sequencer-wrapper"></div>
|
||||
`;
|
||||
|
||||
// Eventos de Seleção
|
||||
trackLane.addEventListener('click', () => {
|
||||
if (appState.pattern.activeTrackId === trackData.id) return;
|
||||
stopPlayback();
|
||||
// stopPlayback(); // (Opcional: descomente se quiser parar o som ao trocar de track)
|
||||
appState.pattern.activeTrackId = trackData.id;
|
||||
document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track'));
|
||||
trackLane.classList.add('active-track');
|
||||
@@ -52,18 +126,17 @@ export function renderPatternEditor() {
|
||||
redrawSequencer();
|
||||
});
|
||||
|
||||
// Drag and Drop de Samples
|
||||
trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); });
|
||||
trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over"));
|
||||
|
||||
trackLane.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
trackLane.classList.remove("drag-over");
|
||||
const filePath = e.dataTransfer.getData("text/plain");
|
||||
|
||||
if (filePath) {
|
||||
sendAction({
|
||||
type: 'SET_TRACK_SAMPLE',
|
||||
trackIndex: trackIndex,
|
||||
trackIndex: originalIndex,
|
||||
filePath: filePath
|
||||
});
|
||||
}
|
||||
@@ -76,12 +149,12 @@ export function renderPatternEditor() {
|
||||
redrawSequencer();
|
||||
}
|
||||
|
||||
// Renderiza o grid de steps ou piano roll miniatura
|
||||
export function redrawSequencer() {
|
||||
const totalGridSteps = getTotalSteps();
|
||||
|
||||
document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => {
|
||||
let sequencerContainer = wrapper.querySelector(".step-sequencer");
|
||||
|
||||
if (!sequencerContainer) {
|
||||
sequencerContainer = document.createElement("div");
|
||||
sequencerContainer.className = "step-sequencer";
|
||||
@@ -90,160 +163,105 @@ export function redrawSequencer() {
|
||||
sequencerContainer.innerHTML = "";
|
||||
}
|
||||
|
||||
// Busca o dado da trilha usando o índice global
|
||||
const parentTrackElement = wrapper.closest(".track-lane");
|
||||
const trackIndex = parseInt(parentTrackElement.dataset.trackIndex, 10);
|
||||
const trackData = appState.pattern.tracks[trackIndex];
|
||||
|
||||
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) return;
|
||||
|
||||
const activePatternIndex = trackData.activePatternIndex;
|
||||
const activePatternIndex = trackData.activePatternIndex || 0;
|
||||
const activePattern = trackData.patterns[activePatternIndex];
|
||||
if (!activePattern) return;
|
||||
|
||||
if (!activePattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LÓGICA DE DECISÃO V2: STEPS OU PIANO ROLL? (MANTIDA)
|
||||
// ============================================================
|
||||
|
||||
// --- DECISÃO: STEP SEQUENCER VS PIANO ROLL ---
|
||||
const notes = activePattern.notes || [];
|
||||
const hasNotes = notes.length > 0;
|
||||
let renderMode = 'steps';
|
||||
|
||||
if (hasNotes) {
|
||||
if (notes.length > 0) {
|
||||
const firstKey = notes[0].key;
|
||||
const isMelodic = notes.some(n => n.key !== firstKey);
|
||||
const hasLongNotes = notes.some(n => n.len > 48);
|
||||
|
||||
const sortedNotes = [...notes].sort((a, b) => a.pos - b.pos);
|
||||
let hasOverlap = false;
|
||||
for (let i = 0; i < sortedNotes.length - 1; i++) {
|
||||
if (sortedNotes[i].pos + sortedNotes[i].len > sortedNotes[i+1].pos) {
|
||||
hasOverlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMelodic || hasLongNotes || hasOverlap) {
|
||||
// Se houver variação de nota ou notas longas, usa visualização de Piano Roll
|
||||
if (notes.some(n => n.key !== firstKey) || notes.some(n => n.len > 48)) {
|
||||
renderMode = 'piano_roll';
|
||||
} else {
|
||||
renderMode = 'steps';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// RENDERIZAÇÃO
|
||||
// ============================================================
|
||||
|
||||
if (renderMode === 'piano_roll') {
|
||||
// --- MODO PIANO ROLL ---
|
||||
// --- MODO: MINI PIANO ROLL ---
|
||||
sequencerContainer.classList.add('mode-piano');
|
||||
|
||||
const miniView = document.createElement('div');
|
||||
miniView.className = 'track-mini-piano-roll';
|
||||
miniView.title = "Clique duplo para abrir o Piano Roll";
|
||||
|
||||
miniView.title = "Clique duplo para abrir o Piano Roll completo";
|
||||
|
||||
miniView.addEventListener('dblclick', (e) => {
|
||||
e.stopPropagation();
|
||||
if (window.openPianoRoll) {
|
||||
window.openPianoRoll(trackData.id);
|
||||
}
|
||||
if (window.openPianoRoll) window.openPianoRoll(trackData.id);
|
||||
});
|
||||
|
||||
// --- CÁLCULO REVERTIDO (VOLTA AO PADRÃO ANTERIOR) ---
|
||||
const barsInput = document.getElementById('bars-input');
|
||||
const barsCount = barsInput ? parseInt(barsInput.value) || 1 : 1;
|
||||
|
||||
// Revertido: Removemos a multiplicação por beatsPerBar que causou o bug visual
|
||||
const totalTicks = 192 * barsCount;
|
||||
// ----------------------------------------------------
|
||||
// 48 ticks = 1/16 step.
|
||||
const totalTicks = totalGridSteps * 48;
|
||||
|
||||
activePattern.notes.forEach(note => {
|
||||
const noteEl = document.createElement('div');
|
||||
noteEl.className = 'mini-note';
|
||||
|
||||
const leftPercent = (note.pos / totalTicks) * 100;
|
||||
const widthPercent = (note.len / totalTicks) * 100;
|
||||
const leftPercent = Math.min((note.pos / totalTicks) * 100, 100);
|
||||
const widthPercent = Math.min((note.len / totalTicks) * 100, 100 - leftPercent);
|
||||
|
||||
const keyRange = 48;
|
||||
const baseKey = 36;
|
||||
let relativeKey = note.key - baseKey;
|
||||
|
||||
if(relativeKey < 0) relativeKey = 0;
|
||||
if(relativeKey > keyRange) relativeKey = keyRange;
|
||||
|
||||
const topPercent = 100 - ((relativeKey / keyRange) * 100);
|
||||
// Mapeia nota MIDI para altura (aprox 4 oitavas visíveis)
|
||||
const relativeKey = Math.max(0, Math.min(note.key - 36, 48));
|
||||
const topPercent = 100 - ((relativeKey / 48) * 100);
|
||||
|
||||
noteEl.style.left = `${leftPercent}%`;
|
||||
noteEl.style.width = `${widthPercent}%`;
|
||||
noteEl.style.top = `${topPercent}%`;
|
||||
|
||||
miniView.appendChild(noteEl);
|
||||
});
|
||||
|
||||
sequencerContainer.appendChild(miniView);
|
||||
|
||||
} else {
|
||||
// --- MODO STEP SEQUENCER ---
|
||||
// --- MODO: STEP SEQUENCER (Bateria) ---
|
||||
sequencerContainer.classList.remove('mode-piano');
|
||||
|
||||
const patternSteps = activePattern.steps;
|
||||
if (!patternSteps || !Array.isArray(patternSteps)) return;
|
||||
|
||||
|
||||
for (let i = 0; i < totalGridSteps; i++) {
|
||||
const stepWrapper = document.createElement("div");
|
||||
stepWrapper.className = "step-wrapper";
|
||||
|
||||
const stepElement = document.createElement("div");
|
||||
stepElement.className = "step";
|
||||
|
||||
if (patternSteps[i] === true) {
|
||||
stepElement.classList.add("active");
|
||||
}
|
||||
if (patternSteps[i] === true) stepElement.classList.add("active");
|
||||
|
||||
// Evento de Toggle
|
||||
stepElement.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
initializeAudioContext();
|
||||
|
||||
const currentState = activePattern.steps[i] || false;
|
||||
const isActive = !currentState;
|
||||
|
||||
const isActive = !activePattern.steps[i];
|
||||
|
||||
sendAction({
|
||||
type: 'TOGGLE_NOTE',
|
||||
trackIndex: trackIndex,
|
||||
patternIndex: activePatternIndex,
|
||||
stepIndex: i,
|
||||
isActive: isActive
|
||||
type: 'TOGGLE_NOTE',
|
||||
trackIndex,
|
||||
patternIndex: activePatternIndex,
|
||||
stepIndex: i,
|
||||
isActive
|
||||
});
|
||||
|
||||
|
||||
// Preview Sonoro
|
||||
if (isActive) {
|
||||
if (trackData.type === 'sampler' && trackData.samplePath) {
|
||||
playSample(trackData.samplePath, trackData.id);
|
||||
}
|
||||
else if (trackData.type === 'plugin' && trackData.instrument) {
|
||||
try {
|
||||
trackData.instrument.triggerAttackRelease("C5", "16n", Tone.now());
|
||||
} catch(err) {
|
||||
console.warn("Erro ao tocar preview do synth:", err);
|
||||
}
|
||||
} else if (trackData.type === 'plugin' && trackData.instrument) {
|
||||
try { trackData.instrument.triggerAttackRelease("C5", "16n", Tone.now()); } catch(err) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4;
|
||||
const groupIndex = Math.floor(i / beatsPerBar);
|
||||
if (groupIndex % 2 === 0) {
|
||||
stepElement.classList.add("step-dark");
|
||||
}
|
||||
|
||||
const stepsPerBar = 16;
|
||||
if (i > 0 && i % stepsPerBar === 0) {
|
||||
// Estilização do Grid (Marcadores de Compasso e Beat)
|
||||
if (Math.floor(i / 4) % 2 === 0) stepElement.classList.add("step-dark");
|
||||
|
||||
if (i > 0 && i % 16 === 0) {
|
||||
const marker = document.createElement("div");
|
||||
marker.className = "step-marker";
|
||||
marker.textContent = Math.floor(i / stepsPerBar) + 1;
|
||||
marker.textContent = Math.floor(i / 16) + 1;
|
||||
stepWrapper.appendChild(marker);
|
||||
}
|
||||
|
||||
@@ -254,67 +272,38 @@ export function redrawSequencer() {
|
||||
});
|
||||
}
|
||||
|
||||
// Ilumina o step atual durante o playback
|
||||
export function highlightStep(stepIndex, isActive) {
|
||||
if (stepIndex < 0) return;
|
||||
document.querySelectorAll(".track-lane").forEach((track) => {
|
||||
const stepWrapper = track.querySelector(
|
||||
`.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`
|
||||
);
|
||||
const stepWrapper = track.querySelector(`.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`);
|
||||
if (stepWrapper) {
|
||||
const stepElement = stepWrapper.querySelector(".step");
|
||||
if (stepElement) {
|
||||
stepElement.classList.toggle("playing", isActive);
|
||||
}
|
||||
if (stepElement) stepElement.classList.toggle("playing", isActive);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Atualiza UI de um step específico (chamado pelo socket)
|
||||
export function updateStepUI(trackIndex, patternIndex, stepIndex, isActive) {
|
||||
const trackElement = document.querySelector(`.track-lane[data-track-index="${trackIndex}"]`);
|
||||
if (!trackElement) return;
|
||||
|
||||
const trackData = appState.pattern.tracks[trackIndex];
|
||||
if (!trackData) return;
|
||||
|
||||
const activePattern = trackData.patterns[patternIndex];
|
||||
|
||||
const notes = activePattern.notes || [];
|
||||
const hasNotes = notes.length > 0;
|
||||
let isComplex = false;
|
||||
|
||||
if (hasNotes) {
|
||||
const isMelodic = notes.some(n => n.key !== notes[0].key);
|
||||
const hasLongNotes = notes.some(n => n.len > 48);
|
||||
if (isMelodic || hasLongNotes) isComplex = true;
|
||||
}
|
||||
|
||||
if (isComplex) {
|
||||
redrawSequencer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (patternIndex !== trackData.activePatternIndex) return;
|
||||
|
||||
const stepWrapper = trackElement.querySelector(
|
||||
`.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`
|
||||
);
|
||||
if (!stepWrapper) return;
|
||||
const stepElement = stepWrapper.querySelector(".step");
|
||||
if (!stepElement) return;
|
||||
stepElement.classList.toggle("active", isActive);
|
||||
// Redesenhar tudo é mais seguro para garantir que filtros e views (piano vs steps) estejam corretos
|
||||
redrawSequencer();
|
||||
}
|
||||
|
||||
// Atualiza o dropdown de patterns na toolbar
|
||||
export function updateGlobalPatternSelector() {
|
||||
const globalPatternSelector = document.getElementById('global-pattern-selector');
|
||||
if (!globalPatternSelector) return;
|
||||
|
||||
const activeTrackId = appState.pattern.activeTrackId;
|
||||
const activeTrack = appState.pattern.tracks.find(t => t.id === activeTrackId);
|
||||
const referenceTrack = appState.pattern.tracks[0];
|
||||
|
||||
// Tenta pegar a track ativa ou a primeira visível como referência de patterns
|
||||
const referenceTrack = activeTrack || appState.pattern.tracks.find(t => !t.isMuted && t.type !== 'bassline');
|
||||
|
||||
globalPatternSelector.innerHTML = '';
|
||||
|
||||
if (referenceTrack && referenceTrack.patterns.length > 0) {
|
||||
if (referenceTrack && referenceTrack.patterns && referenceTrack.patterns.length > 0) {
|
||||
referenceTrack.patterns.forEach((pattern, index) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = index;
|
||||
|
||||
Reference in new issue
Block a user