atualizando em tempo real as patterns no editor de patterns e playlist
Deploy / Deploy (push) Successful in 2m7s Details

This commit is contained in:
JotaChina 2025-12-27 15:14:50 -03:00
parent 8b811aee07
commit 46105a8fb2
3 changed files with 153 additions and 98 deletions

View File

@ -241,7 +241,7 @@ export async function loadProjectFromServer(fileName) {
// ================================================================= // =================================================================
function parseInstrumentNode( function parseInstrumentNode(
trackNode, trackNode,
sortedBBTrackNameNodes, // Esse argumento agora será ignorado para evitar o bug sortedBBTrackNameNodes,
pathMap, pathMap,
parentBasslineId = null parentBasslineId = null
) { ) {
@ -253,14 +253,15 @@ function parseInstrumentNode(
const trackName = trackNode.getAttribute("name"); const trackName = trackNode.getAttribute("name");
const instrumentName = instrumentNode.getAttribute("name"); const instrumentName = instrumentNode.getAttribute("name");
// Lógica de Patterns: // ============================================================
// CORREÇÃO: Iteramos diretamente sobre os patterns encontrados no XML do instrumento. // ✅ Patterns (Song Editor x Beat/Bassline Rack)
// Não tentamos mais mapear 1-para-1 com os clipes da timeline, pois instrumentos de // - Song Editor: mantém o comportamento antigo (sequencial)
// Beat/Bassline reutilizam o mesmo pattern várias vezes. // - 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"); 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) => { const allPatternsArray = Array.from(allPatternsNodeList).sort((a, b) => {
return ( return (
(parseInt(a.getAttribute("pos"), 10) || 0) - (parseInt(a.getAttribute("pos"), 10) || 0) -
@ -268,28 +269,24 @@ function parseInstrumentNode(
); );
}); });
// Se não houver patterns no XML, criamos um vazio para não quebrar a UI const makeEmptyPattern = (idx, nameOverride = null) => ({
let patterns = []; name: nameOverride || `Pattern ${idx + 1}`,
steps: new Array(16).fill(false),
notes: [],
pos: idx * BAR_TICKS,
});
if (allPatternsArray.length > 0) { const buildPatternFromNode = (patternNode, fallbackName, fallbackPos) => {
patterns = allPatternsArray.map((patternNode, index) => { const patternName = patternNode.getAttribute("name") || fallbackName;
// 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 patternSteps = parseInt(patternNode.getAttribute("steps"), 10) || 16;
const steps = new Array(patternSteps).fill(false); const steps = new Array(patternSteps).fill(false);
const notes = []; 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;
patternNode.querySelectorAll("note").forEach((noteNode) => { patternNode.querySelectorAll("note").forEach((noteNode) => {
const pos = parseInt(noteNode.getAttribute("pos"), 10) || 0; const pos = parseInt(noteNode.getAttribute("pos"), 10) || 0;
const rawLen = parseInt(noteNode.getAttribute("len"), 10) || 0; 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; const len = rawLen < 0 ? TICKS_PER_STEP : rawLen;
notes.push({ notes.push({
@ -300,29 +297,69 @@ function parseInstrumentNode(
pan: parseInt(noteNode.getAttribute("pan"), 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); const stepIndex = Math.floor(pos / TICKS_PER_STEP);
if (stepIndex < patternSteps) steps[stepIndex] = true; if (stepIndex < patternSteps) steps[stepIndex] = true;
}); });
return { return {
name: patternName, name: patternName,
steps: steps, steps,
notes: notes, notes,
pos: parseInt(patternNode.getAttribute("pos"), 10) || 0, pos: fallbackPos,
}; };
};
let patterns = [];
if (allPatternsArray.length > 0) {
// ✅ Caso 1: instrumentos do Rack do Beat/Bassline
if (parentBasslineId) {
// quantos BB tracks existem no projeto?
let bbCount = Array.isArray(sortedBBTrackNameNodes)
? sortedBBTrackNameNodes.length
: 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);
// cria array denso (sem buracos)
patterns = new Array(bbCount).fill(null).map((_, i) => {
const nameFromBB =
sortedBBTrackNameNodes?.[i]?.getAttribute?.("name") || null;
return makeEmptyPattern(i, nameFromBB);
}); });
// 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 { } else {
// Fallback: Nenhum pattern encontrado no XML, cria um padrão vazio // ✅ Caso 2: instrumentos do Song Editor (mantém sequencial)
patterns.push({ patterns = allPatternsArray.map((patternNode, index) => {
name: "Pattern 0", const fallbackName = patternNode.getAttribute("name") || `Pattern ${index}`;
steps: new Array(16).fill(false), const fallbackPos = parseInt(patternNode.getAttribute("pos"), 10) || 0;
notes: [], return buildPatternFromNode(patternNode, fallbackName, fallbackPos);
pos: 0
}); });
} }
} else {
// 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 finalSamplePath = null;
let trackType = "plugin"; let trackType = "plugin";
@ -345,29 +382,19 @@ function parseInstrumentNode(
const volFromFile = parseFloat(instrumentTrackNode.getAttribute("vol")); const volFromFile = parseFloat(instrumentTrackNode.getAttribute("vol"));
const panFromFile = parseFloat(instrumentTrackNode.getAttribute("pan")); 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(), id: Date.now() + Math.random(),
name: trackName, name: trackName,
type: trackType, type: trackType,
samplePath: finalSamplePath, samplePath: finalSamplePath,
patterns, patterns: patterns,
activePatternIndex: 0, // ✅ evita index undefined
baseNote, // ✅ importante p/ sample pitch
pitch, // (opcional p/ transposição depois)
volume: !isNaN(volFromFile) ? volFromFile / 100 : DEFAULT_VOLUME, volume: !isNaN(volFromFile) ? volFromFile / 100 : DEFAULT_VOLUME,
pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN, pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN,
instrumentName, instrumentName: instrumentName,
instrumentXml: instrumentNode.innerHTML, instrumentXml: instrumentNode.innerHTML,
parentBasslineId, parentBasslineId: parentBasslineId,
}; };
} }
// ================================================================= // =================================================================

View File

@ -507,6 +507,34 @@ socket.on("action_broadcast", (payload) => {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// PROCESSAR AÇÕES // 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; let isLoadingProject = false;
async function handleActionBroadcast(action) { async function handleActionBroadcast(action) {
if (!action || !action.type) return; if (!action || !action.type) return;
@ -997,22 +1025,27 @@ async function handleActionBroadcast(action) {
case "TOGGLE_NOTE": { 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]; const t = appState.pattern.tracks[ti];
if (t) { if (t) {
t.patterns[pi] = t.patterns[pi] || { steps: [] }; 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 = [];
t.patterns[pi].steps[si] = isActive; t.patterns[pi].steps[si] = isActive;
try { updateStepUI(ti, pi, si, isActive); } catch {} try { updateStepUI(ti, pi, si, isActive); } catch {}
if (!isFromSelf) schedulePatternRerender(); if (!isFromSelf) schedulePatternRerender();
} }
// ✔ ADICIONE ISTO
if (window.ROOM_NAME && isFromSelf) { if (window.ROOM_NAME && isFromSelf) {
const xml = generateXmlFromStateExported(); const xml = generateXmlFromStateExported();
sendAction({ sendAction({ type: "SYNC_PATTERN_STATE", xml });
type: "SYNC_PATTERN_STATE",
xml
});
} }
saveStateToSession(); saveStateToSession();
@ -1102,6 +1135,9 @@ async function handleActionBroadcast(action) {
track.activePatternIndex = patternIndex; track.activePatternIndex = patternIndex;
}); });
// ✅ garante que TODAS as tracks tenham esse índice disponível
_ensurePatternsUpTo(patternIndex);
try { try {
const stepsPerBar = 16; // 4/4 em 1/16 const stepsPerBar = 16; // 4/4 em 1/16

View File

@ -61,20 +61,15 @@ function makePatternSnapshot() {
type: t.type, type: t.type,
samplePath: t.samplePath, samplePath: t.samplePath,
// ✅ extras necessários pro Song Editor / playlist // ✅ mantém relações do rack/BB
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
parentBasslineId: t.parentBasslineId ?? null, parentBasslineId: t.parentBasslineId ?? null,
instrumentSourceId: t.instrumentSourceId ?? 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, isMuted: t.isMuted ?? false,
muted: t.muted ?? false, muted: t.muted ?? false,
@ -92,12 +87,9 @@ function makePatternSnapshot() {
})), })),
activeTrackId: appState.pattern.activeTrackId, activeTrackId: appState.pattern.activeTrackId,
activePatternIndex: appState.pattern.activePatternIndex, activePatternIndex: appState.pattern.activePatternIndex,
focusedBasslineId: appState.pattern.focusedBasslineId ?? null,
}; };
} }
// ---------------------- // ----------------------
// Helper: existe snapshot local com áudio? // Helper: existe snapshot local com áudio?
// ---------------------- // ----------------------