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 = `
-
-
-
-
-
-
-
-
-
-
-
-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();