diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index e29627dc..589d66d8 100755 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -18,7 +18,7 @@ import * as Tone from "https://esm.sh/tone"; import { sendAction } from "./socket.js"; //-------------------------------------------------------------- -// MANIPULAÇÃO DE ARQUIVOS E PARSING +// MANIPULAÇÃO DE ARQUIVOS //-------------------------------------------------------------- export function handleLocalProjectReset() { @@ -90,7 +90,7 @@ export async function loadProjectFromServer(fileName) { // ================================================================= // FUNÇÃO AUXILIAR: PARSE DE INSTRUMENTO ÚNICO // ================================================================= -function parseInstrumentNode(trackNode, sortedBBTrackNameNodes, pathMap) { +function parseInstrumentNode(trackNode, sortedBBTrackNameNodes, pathMap, parentBasslineId = null) { const instrumentNode = trackNode.querySelector("instrument"); const instrumentTrackNode = trackNode.querySelector("instrumenttrack"); @@ -99,7 +99,7 @@ function parseInstrumentNode(trackNode, sortedBBTrackNameNodes, pathMap) { const trackName = trackNode.getAttribute("name"); const instrumentName = instrumentNode.getAttribute("name"); - // Identifica e ordena os patterns + // Lógica de Patterns const allPatternsNodeList = trackNode.querySelectorAll("pattern"); const allPatternsArray = Array.from(allPatternsNodeList).sort((a, b) => { return (parseInt(a.getAttribute("pos"), 10) || 0) - (parseInt(b.getAttribute("pos"), 10) || 0); @@ -120,9 +120,12 @@ function parseInstrumentNode(trackNode, sortedBBTrackNameNodes, pathMap) { const patternSteps = parseInt(patternNode.getAttribute("steps"), 10) || 16; const steps = new Array(patternSteps).fill(false); const notes = []; - const ticksPerStep = 12; + + // === CORREÇÃO MATEMÁTICA === + // No LMMS, 1 semínima (beat) = 192 ticks. + // 1 semicolcheia (1/16 step) = 192 / 4 = 48 ticks. + const ticksPerStep = 48; - // Extrai as notas e popula os steps patternNode.querySelectorAll("note").forEach((noteNode) => { const pos = parseInt(noteNode.getAttribute("pos"), 10); notes.push({ @@ -133,7 +136,7 @@ function parseInstrumentNode(trackNode, sortedBBTrackNameNodes, pathMap) { pan: parseInt(noteNode.getAttribute("pan"), 10), }); - // Converte posição em tick para índice do step (grid) + // Calcula qual quadradinho acender const stepIndex = Math.round(pos / ticksPerStep); if (stepIndex < patternSteps) steps[stepIndex] = true; }); @@ -146,6 +149,7 @@ function parseInstrumentNode(trackNode, sortedBBTrackNameNodes, pathMap) { }; }); + // Lógica de Sample vs Plugin let finalSamplePath = null; let trackType = "plugin"; @@ -178,11 +182,12 @@ function parseInstrumentNode(trackNode, sortedBBTrackNameNodes, pathMap) { pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN, instrumentName: instrumentName, instrumentXml: instrumentNode.innerHTML, + parentBasslineId: parentBasslineId // Guarda o ID do pai para filtragem na UI }; } // ================================================================= -// 🔥 FUNÇÃO DE PARSING PRINCIPAL (CORRIGIDA) +// 🔥 FUNÇÃO DE PARSING PRINCIPAL // ================================================================= export async function parseMmpContent(xmlString) { resetProjectState(); @@ -224,27 +229,26 @@ export async function parseMmpContent(xmlString) { } // ------------------------------------------------------------- - // 2. EXTRAÇÃO DE TODOS OS INSTRUMENTOS (RECURSIVO) + // 2. EXTRAÇÃO DE INSTRUMENTOS DA RAIZ (SONG EDITOR) // ------------------------------------------------------------- - // Aqui está a correção: Vamos buscar TODOS os instrumentos (type="0"), - // não importa se estão na raiz ou dentro de uma bassline. - // Isso garante que Kicker, Snare, etc., apareçam no Pattern Editor. + // Pega apenas os instrumentos que estão soltos no Song Editor (não dentro de BBTracks) + const songInstrumentNodes = Array.from(xmlDoc.querySelectorAll('song > trackcontainer > track[type="0"]')); - const allInstrumentNodes = Array.from(xmlDoc.querySelectorAll('track[type="0"]')); - - const allInstruments = allInstrumentNodes - .map(node => parseInstrumentNode(node, sortedBBTrackNameNodes, pathMap)) + const songTracks = songInstrumentNodes + .map(node => parseInstrumentNode(node, sortedBBTrackNameNodes, pathMap, null)) // null = Sem Pai .filter(t => t !== null); // ------------------------------------------------------------- - // 3. EXTRAÇÃO DAS TRILHAS DE BASSLINE (CONTAINERS) + // 3. EXTRAÇÃO DAS TRILHAS DE BASSLINE E SEUS FILHOS // ------------------------------------------------------------- - // Isso garante que os blocos azuis apareçam na Playlist. + let allBasslineInstruments = []; + const basslineContainers = bbTrackNodes.map(trackNode => { const trackName = trackNode.getAttribute("name") || "Beat/Bassline"; + const containerId = `bassline_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - // Extrai os clipes da timeline (blocos azuis) + // A. Extrai os clipes da timeline (blocos azuis) const playlistClips = Array.from(trackNode.querySelectorAll(":scope > bbtco")).map(bbtco => { return { pos: parseInt(bbtco.getAttribute("pos"), 10) || 0, @@ -253,22 +257,25 @@ export async function parseMmpContent(xmlString) { }; }); - // Se não tiver clipes, não cria a trilha visual inútil na playlist + // Se não tiver clipes, geralmente é container vazio, mas vamos criar mesmo assim if (playlistClips.length === 0) return null; - // Extrai também os instrumentos internos apenas para referência (opcional) - // mas não os usamos para renderizar no main list para evitar duplicidade de lógica + // B. Extrai os instrumentos INTERNOS desta Bassline const internalInstrumentNodes = Array.from(trackNode.querySelectorAll('bbtrack > trackcontainer > track[type="0"]')); + const internalInstruments = internalInstrumentNodes - .map(node => parseInstrumentNode(node, sortedBBTrackNameNodes, pathMap)) + .map(node => parseInstrumentNode(node, sortedBBTrackNameNodes, pathMap, containerId)) // Passa ID do Pai .filter(t => t !== null); + // Acumula na lista geral de instrumentos + allBasslineInstruments.push(...internalInstruments); + return { - id: `bassline_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + id: containerId, name: trackName, type: "bassline", // Tipo especial para o audio_ui.js playlist_clips: playlistClips, - instruments: internalInstruments, // Mantém para o recurso de Double-Click + instruments: internalInstruments, // Mantém referência volume: 1, pan: 0, patterns: [], @@ -280,13 +287,11 @@ export async function parseMmpContent(xmlString) { // 4. COMBINAÇÃO E FINALIZAÇÃO // ------------------------------------------------------------- - // A lista final contém: - // 1. Os Instrumentos (para que os steps apareçam no Pattern Editor) - // 2. As Basslines (para que os blocos apareçam na Playlist) - - // Colocamos as Basslines no final ou no início, conforme preferência. - // Geralmente, instrumentos primeiro é melhor para o Pattern Editor. - const newTracks = [...allInstruments, ...basslineContainers]; + // A lista final plana contém TODOS: + // 1. Instrumentos da Raiz + // 2. Instrumentos dentro de Basslines + // 3. As próprias Basslines (Containers) + const newTracks = [...songTracks, ...allBasslineInstruments, ...basslineContainers]; // Inicializa áudio apenas para instrumentos reais newTracks.forEach((track) => { @@ -298,7 +303,7 @@ export async function parseMmpContent(xmlString) { } }); - // Configura tamanho da timeline baseado nas notas dos instrumentos + // Configura tamanho da timeline let isFirstTrackWithNotes = true; newTracks.forEach(track => { if (track.type !== 'bassline' && isFirstTrackWithNotes) { @@ -324,8 +329,8 @@ export async function parseMmpContent(xmlString) { // Atualiza estado global appState.pattern.tracks = newTracks; + appState.pattern.focusedBasslineId = null; // Reseta o foco - // Seleciona o primeiro instrumento real como ativo const firstInst = newTracks.find(t => t.type !== 'bassline'); appState.pattern.activeTrackId = firstInst ? firstInst.id : null; appState.pattern.activePatternIndex = 0; @@ -367,16 +372,13 @@ function generateXmlFromState() { head.setAttribute("timesig_denominator", document.getElementById("compasso-b-input").value || 4); } - // Lógica de exportação simplificada: - // Remove todos os tracks do container BB e recria com base no estado atual. - // Nota: Isso coloca TODOS os instrumentos dentro da Bassline 0 na exportação, - // que é o comportamento padrão simplificado para garantir que tudo seja salvo. + // Exportação Simplificada: Coloca todos os instrumentos reais no primeiro container const bbTrackContainer = xmlDoc.querySelector('track[type="1"] > bbtrack > trackcontainer'); if (bbTrackContainer) { bbTrackContainer.querySelectorAll('track[type="0"]').forEach((node) => node.remove()); const tracksXml = appState.pattern.tracks - .filter(t => t.type !== 'bassline') // Ignora container visual + .filter(t => t.type !== 'bassline') .map((track) => createTrackXml(track)) .join(""); @@ -400,7 +402,7 @@ export function syncPatternStateToServer() { function createTrackXml(track) { if (!track.patterns || track.patterns.length === 0) return ""; - const ticksPerStep = 12; + const ticksPerStep = 48; // Sincronizado com o parsing const lmmsVolume = Math.round(track.volume * 100); const lmmsPan = Math.round(track.pan * 100); @@ -451,42 +453,8 @@ function modifyAndSaveExistingMmp() { } function generateNewMmp() { - const bpm = document.getElementById("bpm-input").value; - const sig_num = document.getElementById("compasso-a-input").value; - const sig_den = document.getElementById("compasso-b-input").value; - const num_bars = document.getElementById("bars-input").value; - - const tracksXml = appState.pattern.tracks - .filter(t => t.type !== 'bassline') - .map((track) => createTrackXml(track)) - .join(""); - - const mmpContent = ` - - - - - - - - - ${tracksXml} - - - - - - - - - -

Feito com MMPCreator

-]]>
-
-
`; - downloadFile(mmpContent, "novo_projeto.mmp"); + const content = generateXmlFromState(); + downloadFile(content, "novo_projeto.mmp"); } function downloadFile(content, fileName) { diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index 4d648a13..d837a060 100755 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -629,6 +629,25 @@ document.addEventListener("DOMContentLoaded", () => { // if (syncModeBtn) syncModeBtn.style.display = "none"; } + // --- FUNÇÕES GLOBAIS DE FOCO NO PATTERN --- + + window.openPatternEditor = function(basslineTrack) { + console.log("Focando na Bassline:", basslineTrack.name); + // Define o ID da bassline como foco + appState.pattern.focusedBasslineId = basslineTrack.id; + // Renderiza o editor, que agora vai filtrar e mostrar só o conteúdo dela + renderAll(); + + // Feedback visual opcional + showToast(`Editando: ${basslineTrack.name}`, "info"); + } + + window.exitPatternFocus = function() { + console.log("Saindo do foco da Bassline"); + appState.pattern.focusedBasslineId = null; + renderAll(); + } + loadAndRenderSampleBrowser(); renderAll(); diff --git a/assets/js/creations/pattern/pattern_ui.js b/assets/js/creations/pattern/pattern_ui.js index dcf1c517..63f33268 100755 --- a/assets/js/creations/pattern/pattern_ui.js +++ b/assets/js/creations/pattern/pattern_ui.js @@ -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 = ` ${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 = 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 = `
- -
- ${trackData.name} + +
+ ${trackData.name}
@@ -42,9 +115,10 @@ export function renderPatternEditor() {
`; + // 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; diff --git a/assets/js/creations/state.js b/assets/js/creations/state.js index 403112ef..519af057 100755 --- a/assets/js/creations/state.js +++ b/assets/js/creations/state.js @@ -38,12 +38,14 @@ const globalState = { lastRulerClickTime: 0, justReset: false, syncMode: "global", + focusedBasslineId: null, }; export let appState = { global: globalState, pattern: patternState, audio: audioState, // compartilhado com módulo de áudio + focusedBasslineId: null, }; window.appState = appState; @@ -141,12 +143,14 @@ export function resetProjectState() { lastRulerClickTime: 0, justReset: false, syncMode: appState.global.syncMode ?? "global", + focusedBasslineId: null, }); Object.assign(appState.pattern, { tracks: [], activeTrackId: null, activePatternIndex: 0, + focusedBasslineId: null, }); initializeAudioState();