tentando obter playlist real do projeto mmp
Deploy / Deploy (push) Successful in 1m56s
Details
Deploy / Deploy (push) Successful in 1m56s
Details
This commit is contained in:
parent
09c3ee742a
commit
fa69d896bd
|
|
@ -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;
|
||||
|
||||
// Extrai as notas e popula os steps
|
||||
// === CORREÇÃO MATEMÁTICA ===
|
||||
// No LMMS, 1 semínima (beat) = 192 ticks.
|
||||
// 1 semicolcheia (1/16 step) = 192 / 4 = 48 ticks.
|
||||
const ticksPerStep = 48;
|
||||
|
||||
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 = `<?xml version="1.0"?>
|
||||
<!DOCTYPE lmms-project>
|
||||
<lmms-project version="1.0" type="song" creator="MMPCreator" creatorversion="1.0">
|
||||
<head mastervol="100" timesig_denominator="${sig_den}" bpm="${bpm}" timesig_numerator="${sig_num}" masterpitch="0" num_bars="${num_bars}"/>
|
||||
<song>
|
||||
<trackcontainer type="song">
|
||||
<track type="1" solo="0" muted="0" name="Beat/Bassline 0">
|
||||
<bbtrack>
|
||||
<trackcontainer type="bbtrackcontainer">
|
||||
${tracksXml}
|
||||
</trackcontainer>
|
||||
</bbtrack>
|
||||
<bbtco color="4286611584" len="192" usestyle="1" pos="0" muted="0" name="Pattern 1"/>
|
||||
</track>
|
||||
</trackcontainer>
|
||||
<timeline lp1pos="192" lp0pos="0" lpstate="0"/>
|
||||
<controllers/>
|
||||
<projectnotes width="686" y="10" minimized="0" visible="0" x="700" maximized="0" height="400"><![CDATA[<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||
p, li { white-space: pre-wrap; }
|
||||
</style></head><body>
|
||||
<p>Feito com MMPCreator</p>
|
||||
</body></html>]]></projectnotes>
|
||||
</song>
|
||||
</lmms-project>`;
|
||||
downloadFile(mmpContent, "novo_projeto.mmp");
|
||||
const content = generateXmlFromState();
|
||||
downloadFile(content, "novo_projeto.mmp");
|
||||
}
|
||||
|
||||
function downloadFile(content, fileName) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,108 +163,65 @@ 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");
|
||||
|
|
@ -199,51 +229,39 @@ export function redrawSequencer() {
|
|||
|
||||
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,
|
||||
trackIndex,
|
||||
patternIndex: activePatternIndex,
|
||||
stepIndex: i,
|
||||
isActive: isActive
|
||||
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");
|
||||
}
|
||||
// Estilização do Grid (Marcadores de Compasso e Beat)
|
||||
if (Math.floor(i / 4) % 2 === 0) stepElement.classList.add("step-dark");
|
||||
|
||||
const stepsPerBar = 16;
|
||||
if (i > 0 && i % stepsPerBar === 0) {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue