From 46105a8fb29c5f3df39ef1d885082ff0c4b73b8e Mon Sep 17 00:00:00 2001 From: JotaChina Date: Sat, 27 Dec 2025 15:14:50 -0300 Subject: [PATCH] atualizando em tempo real as patterns no editor de patterns e playlist --- assets/js/creations/file.js | 161 ++++++++++++++++++++-------------- assets/js/creations/socket.js | 68 ++++++++++---- assets/js/creations/state.js | 22 ++--- 3 files changed, 153 insertions(+), 98 deletions(-) diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index 4d53ecd5..cc2da840 100755 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -241,7 +241,7 @@ export async function loadProjectFromServer(fileName) { // ================================================================= function parseInstrumentNode( trackNode, - sortedBBTrackNameNodes, // Esse argumento agora será ignorado para evitar o bug + sortedBBTrackNameNodes, pathMap, parentBasslineId = null ) { @@ -253,14 +253,15 @@ function parseInstrumentNode( const trackName = trackNode.getAttribute("name"); const instrumentName = instrumentNode.getAttribute("name"); - // Lógica de Patterns: - // CORREÇÃO: Iteramos diretamente sobre os patterns encontrados no XML do instrumento. - // Não tentamos mais mapear 1-para-1 com os clipes da timeline, pois instrumentos de - // Beat/Bassline reutilizam o mesmo pattern várias vezes. - + // ============================================================ + // ✅ Patterns (Song Editor x Beat/Bassline Rack) + // - Song Editor: mantém o comportamento antigo (sequencial) + // - Rack (parentBasslineId != null): mapeia por pos/192 (índice real) + // e preenche patterns vazios para manter index estável. + // ============================================================ + const BAR_TICKS = 192; // 1 compasso em ticks (4/4) const allPatternsNodeList = trackNode.querySelectorAll("pattern"); - - // Ordena os patterns pela posição (importante para Song Editor, neutro para BB Editor) + const allPatternsArray = Array.from(allPatternsNodeList).sort((a, b) => { return ( (parseInt(a.getAttribute("pos"), 10) || 0) - @@ -268,61 +269,97 @@ function parseInstrumentNode( ); }); - // Se não houver patterns no XML, criamos um vazio para não quebrar a UI + const makeEmptyPattern = (idx, nameOverride = null) => ({ + name: nameOverride || `Pattern ${idx + 1}`, + steps: new Array(16).fill(false), + notes: [], + pos: idx * BAR_TICKS, + }); + + const buildPatternFromNode = (patternNode, fallbackName, fallbackPos) => { + const patternName = patternNode.getAttribute("name") || fallbackName; + + const patternSteps = parseInt(patternNode.getAttribute("steps"), 10) || 16; + const steps = new Array(patternSteps).fill(false); + const notes = []; + + patternNode.querySelectorAll("note").forEach((noteNode) => { + const pos = parseInt(noteNode.getAttribute("pos"), 10) || 0; + + const rawLen = parseInt(noteNode.getAttribute("len"), 10) || 0; + const len = rawLen < 0 ? TICKS_PER_STEP : rawLen; + + notes.push({ + pos, + len, + key: parseInt(noteNode.getAttribute("key"), 10), + vol: parseInt(noteNode.getAttribute("vol"), 10), + pan: parseInt(noteNode.getAttribute("pan"), 10), + }); + + const stepIndex = Math.floor(pos / TICKS_PER_STEP); + if (stepIndex < patternSteps) steps[stepIndex] = true; + }); + + return { + name: patternName, + steps, + notes, + pos: fallbackPos, + }; + }; + let patterns = []; if (allPatternsArray.length > 0) { - patterns = allPatternsArray.map((patternNode, index) => { - // Tenta pegar o nome do atributo, ou gera um genérico - const patternName = patternNode.getAttribute("name") || `Pattern ${index}`; - - const patternSteps = parseInt(patternNode.getAttribute("steps"), 10) || 16; - const steps = new Array(patternSteps).fill(false); - const notes = []; - // No XML base: 1 compasso (bar) = 192 ticks em 4/4, - // então 1 beat = 48 ticks e 1 step (1/16) = 12 ticks. - const ticksPerStep = 12; + // ✅ Caso 1: instrumentos do Rack do Beat/Bassline + if (parentBasslineId) { + // quantos BB tracks existem no projeto? + let bbCount = Array.isArray(sortedBBTrackNameNodes) + ? sortedBBTrackNameNodes.length + : 0; - patternNode.querySelectorAll("note").forEach((noteNode) => { - const pos = parseInt(noteNode.getAttribute("pos"), 10) || 0; + // garante bbCount >= maior índice encontrado no XML + let maxIdx = 0; + for (const pn of allPatternsArray) { + const posTicks = parseInt(pn.getAttribute("pos"), 10) || 0; + const idx = Math.round(posTicks / BAR_TICKS); + if (idx > maxIdx) maxIdx = idx; + } + bbCount = Math.max(bbCount, maxIdx + 1, 1); - const rawLen = parseInt(noteNode.getAttribute("len"), 10) || 0; - - // ✅ LMMS costuma salvar one-shots com len negativo (ex: -192). - // Na nossa DAW, tratamos isso como 1 step (1/16) = 12 ticks. - const len = rawLen < 0 ? TICKS_PER_STEP : rawLen; - - notes.push({ - pos, - len, - key: parseInt(noteNode.getAttribute("key"), 10), - vol: parseInt(noteNode.getAttribute("vol"), 10), - pan: parseInt(noteNode.getAttribute("pan"), 10), - }); - - // stepIndex também deve usar 12 (você já corrigiu isso antes) - const stepIndex = Math.floor(pos / TICKS_PER_STEP); - if (stepIndex < patternSteps) steps[stepIndex] = true; + // cria array denso (sem buracos) + patterns = new Array(bbCount).fill(null).map((_, i) => { + const nameFromBB = + sortedBBTrackNameNodes?.[i]?.getAttribute?.("name") || null; + return makeEmptyPattern(i, nameFromBB); }); - return { - name: patternName, - steps: steps, - notes: notes, - pos: parseInt(patternNode.getAttribute("pos"), 10) || 0, - }; - }); + // injeta patterns do XML no índice certo (pos/192) + for (const pn of allPatternsArray) { + const posTicks = parseInt(pn.getAttribute("pos"), 10) || 0; + const idx = Math.round(posTicks / BAR_TICKS); + if (idx < 0 || idx >= patterns.length) continue; + + const fallbackName = patterns[idx]?.name || `Pattern ${idx + 1}`; + patterns[idx] = buildPatternFromNode(pn, fallbackName, idx * BAR_TICKS); + } + } else { + // ✅ Caso 2: instrumentos do Song Editor (mantém sequencial) + patterns = allPatternsArray.map((patternNode, index) => { + const fallbackName = patternNode.getAttribute("name") || `Pattern ${index}`; + const fallbackPos = parseInt(patternNode.getAttribute("pos"), 10) || 0; + return buildPatternFromNode(patternNode, fallbackName, fallbackPos); + }); + } } else { - // Fallback: Nenhum pattern encontrado no XML, cria um padrão vazio - patterns.push({ - name: "Pattern 0", - steps: new Array(16).fill(false), - notes: [], - pos: 0 - }); + // Fallback: Nenhum pattern encontrado no XML + patterns.push(makeEmptyPattern(0, "Pattern 0")); } - // Lógica de Sample vs Plugin + // ============================================================ + // Sample vs Plugin + // ============================================================ let finalSamplePath = null; let trackType = "plugin"; @@ -345,29 +382,19 @@ function parseInstrumentNode( const volFromFile = parseFloat(instrumentTrackNode.getAttribute("vol")); const panFromFile = parseFloat(instrumentTrackNode.getAttribute("pan")); - const baseNoteFromFile = parseInt(instrumentTrackNode.getAttribute("basenote"), 10); - const pitchFromFile = parseFloat(instrumentTrackNode.getAttribute("pitch")); - const baseNote = !isNaN(baseNoteFromFile) ? baseNoteFromFile : 60; // fallback C4 (MIDI 60) - const pitch = !isNaN(pitchFromFile) ? pitchFromFile : 0; - - - return { + return { id: Date.now() + Math.random(), name: trackName, type: trackType, samplePath: finalSamplePath, - patterns, - activePatternIndex: 0, // ✅ evita index undefined - baseNote, // ✅ importante p/ sample pitch - pitch, // (opcional p/ transposição depois) + patterns: patterns, volume: !isNaN(volFromFile) ? volFromFile / 100 : DEFAULT_VOLUME, pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN, - instrumentName, + instrumentName: instrumentName, instrumentXml: instrumentNode.innerHTML, - parentBasslineId, + parentBasslineId: parentBasslineId, }; - } // ================================================================= diff --git a/assets/js/creations/socket.js b/assets/js/creations/socket.js index 0c232303..b2d281f1 100755 --- a/assets/js/creations/socket.js +++ b/assets/js/creations/socket.js @@ -507,6 +507,34 @@ socket.on("action_broadcast", (payload) => { // ----------------------------------------------------------------------------- // PROCESSAR AÇÕES // ----------------------------------------------------------------------------- + +const LMMS_BAR_TICKS = 192; +const STEPS_PER_BAR = 16; + +function _makeEmptyPattern(idx) { + // tenta respeitar bars-input atual, mas nunca menos que 1 bar + const bars = parseInt(document.getElementById("bars-input")?.value, 10) || 1; + const stepsLen = Math.max(STEPS_PER_BAR, bars * STEPS_PER_BAR); + + return { + name: `Pattern ${idx + 1}`, + steps: new Array(stepsLen).fill(false), + notes: [], + pos: idx * LMMS_BAR_TICKS, + }; +} + +function _ensurePatternsUpTo(patternIndex) { + appState.pattern.tracks.forEach((t) => { + if (t.type === "bassline") return; + + if (!Array.isArray(t.patterns)) t.patterns = []; + while (t.patterns.length <= patternIndex) { + t.patterns.push(_makeEmptyPattern(t.patterns.length)); + } + }); +} + let isLoadingProject = false; async function handleActionBroadcast(action) { if (!action || !action.type) return; @@ -995,29 +1023,34 @@ async function handleActionBroadcast(action) { // Notes case "TOGGLE_NOTE": { - const { trackIndex: ti, patternIndex: pi, stepIndex: si, isActive } = action; + const { trackIndex: ti, patternIndex: pi, stepIndex: si, isActive } = action; + + // ✅ garante que o índice exista em todas as tracks (evita “silêncio” na playlist) + _ensurePatternsUpTo(pi); + + const t = appState.pattern.tracks[ti]; + if (t) { + t.patterns[pi] = t.patterns[pi] || _makeEmptyPattern(pi); + + // mantém metadados estáveis p/ export/sync + if (t.patterns[pi].pos == null) t.patterns[pi].pos = pi * LMMS_BAR_TICKS; + if (!t.patterns[pi].name) t.patterns[pi].name = `Pattern ${pi + 1}`; + if (!Array.isArray(t.patterns[pi].notes)) t.patterns[pi].notes = []; - const t = appState.pattern.tracks[ti]; - if (t) { - t.patterns[pi] = t.patterns[pi] || { steps: [] }; t.patterns[pi].steps[si] = isActive; try { updateStepUI(ti, pi, si, isActive); } catch {} if (!isFromSelf) schedulePatternRerender(); - } + } - // ✔ ADICIONE ISTO - if (window.ROOM_NAME && isFromSelf) { - const xml = generateXmlFromStateExported(); - sendAction({ - type: "SYNC_PATTERN_STATE", - xml - }); - } + if (window.ROOM_NAME && isFromSelf) { + const xml = generateXmlFromStateExported(); + sendAction({ type: "SYNC_PATTERN_STATE", xml }); + } - saveStateToSession(); - break; - } + saveStateToSession(); + break; + } case "UPDATE_PATTERN_NOTES": { const { @@ -1102,6 +1135,9 @@ async function handleActionBroadcast(action) { track.activePatternIndex = patternIndex; }); + // ✅ garante que TODAS as tracks tenham esse índice disponível + _ensurePatternsUpTo(patternIndex); + try { const stepsPerBar = 16; // 4/4 em 1/16 diff --git a/assets/js/creations/state.js b/assets/js/creations/state.js index c0cd7ff4..87ab7339 100755 --- a/assets/js/creations/state.js +++ b/assets/js/creations/state.js @@ -61,20 +61,15 @@ function makePatternSnapshot() { type: t.type, samplePath: t.samplePath, - // ✅ extras necessários pro Song Editor / playlist - patternIndex: t.patternIndex ?? null, - playlist_clips: Array.isArray(t.playlist_clips) - ? t.playlist_clips.map((c) => ({ - id: c.id ?? null, - pos: c.pos ?? 0, - len: c.len ?? 192, - name: c.name ?? t.name ?? "Pattern", - })) - : null, - - // ✅ mantém compat com a sua estrutura de rack/focus + // ✅ mantém relações do rack/BB parentBasslineId: t.parentBasslineId ?? null, instrumentSourceId: t.instrumentSourceId ?? null, + + // ✅ mantém playlist patterns (bbtco “em memória”) + playlist_clips: t.playlist_clips ?? null, + patternIndex: Number.isInteger(t.patternIndex) ? t.patternIndex : null, + + // estados de mute (você usa ambos em pontos diferentes) isMuted: t.isMuted ?? false, muted: t.muted ?? false, @@ -92,12 +87,9 @@ function makePatternSnapshot() { })), activeTrackId: appState.pattern.activeTrackId, activePatternIndex: appState.pattern.activePatternIndex, - focusedBasslineId: appState.pattern.focusedBasslineId ?? null, }; } - - // ---------------------- // Helper: existe snapshot local com áudio? // ----------------------