// js/pattern/pattern_ui.js import { appState } from "../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'; import { initializeAudioContext } from '../audio.js'; import * as Tone from "https://esm.sh/tone"; // Função principal de renderização para o editor de patterns export function renderPatternEditor() { const trackContainer = document.getElementById("track-container"); trackContainer.innerHTML = ""; // 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; // 1) acha o container (Caixa/Kick/...) focado const basslineTrack = appState.pattern.tracks.find( t => t.id === appState.pattern.focusedBasslineId && t.type === "bassline" ); // 2) usa o rack real como "pai" (fallback: id do próprio container) const srcId = basslineTrack?.instrumentSourceId || appState.pattern.focusedBasslineId; // 3) mostra somente instrumentos pertencentes ao rack tracksToRender = appState.pattern.tracks.filter( t => t.type !== "bassline" && t.parentBasslineId === srcId ); // Nome no header 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 = ` ${contextName}`; 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 = ` 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 = originalIndex; if (trackData.id === appState.pattern.activeTrackId) { trackLane.classList.add('active-track'); } trackLane.innerHTML = `
${trackData.name}
VOL
PAN
`; // Eventos de Seleção trackLane.addEventListener('click', () => { if (appState.pattern.activeTrackId === trackData.id) return; // 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'); updateGlobalPatternSelector(); 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: originalIndex, filePath: filePath }); } }); trackContainer.appendChild(trackLane); }); updateGlobalPatternSelector(); 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"; wrapper.appendChild(sequencerContainer); } else { 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; const isFocused = !!appState.pattern.focusedBasslineId; const activePatternIndex = isFocused ? (appState.pattern.activePatternIndex || 0) : (trackData.activePatternIndex || 0); const activePattern = trackData.patterns[activePatternIndex]; if (!activePattern) return; // --- DECISÃO: STEP SEQUENCER VS PIANO ROLL --- const notes = activePattern.notes || []; let renderMode = 'steps'; if (notes.length > 0) { const firstKey = notes[0].key; // 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'; } } if (renderMode === '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 completo"; miniView.addEventListener('dblclick', (e) => { e.stopPropagation(); if (window.openPianoRoll) window.openPianoRoll(trackData.id); }); // 48 ticks = 1/16 step. const totalTicks = totalGridSteps * 48; activePattern.notes.forEach(note => { const noteEl = document.createElement('div'); noteEl.className = 'mini-note'; const leftPercent = Math.min((note.pos / totalTicks) * 100, 100); const widthPercent = Math.min((note.len / totalTicks) * 100, 100 - leftPercent); // 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 (Bateria) --- sequencerContainer.classList.remove('mode-piano'); const patternSteps = activePattern.steps; 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"); // Evento de Toggle stepElement.addEventListener("click", (e) => { e.stopPropagation(); initializeAudioContext(); const isActive = !activePattern.steps[i]; sendAction({ 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) {} } } }); // 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 / 16) + 1; stepWrapper.appendChild(marker); } stepWrapper.appendChild(stepElement); sequencerContainer.appendChild(stepWrapper); } } }); } // 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})`); if (stepWrapper) { const stepElement = stepWrapper.querySelector(".step"); if (stepElement) stepElement.classList.toggle("playing", isActive); } }); } // Atualiza UI de um step específico (chamado pelo socket) export function updateStepUI(trackIndex, patternIndex, stepIndex, 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); // Tenta pegar a track ativa ou a primeira visível como referência de patterns const isFocused = !!appState.pattern.focusedBasslineId; const referenceTrack = isFocused ? appState.pattern.tracks.find(t => t.type !== "bassline" && t.parentBasslineId !== null) : (activeTrack || appState.pattern.tracks.find(t => !t.isMuted && t.type !== "bassline")); globalPatternSelector.innerHTML = ''; if (referenceTrack && referenceTrack.patterns && referenceTrack.patterns.length > 0) { referenceTrack.patterns.forEach((pattern, index) => { const option = document.createElement('option'); option.value = index; option.textContent = pattern.name; globalPatternSelector.appendChild(option); }); if (isFocused) { globalPatternSelector.value = appState.pattern.activePatternIndex || 0; } else if (activeTrack) { globalPatternSelector.value = activeTrack.activePatternIndex || 0; } else { globalPatternSelector.value = 0; } globalPatternSelector.disabled = false; } else { const option = document.createElement('option'); option.textContent = 'Sem patterns'; globalPatternSelector.appendChild(option); globalPatternSelector.disabled = true; } }