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,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 = []; let patterns = [];
if (allPatternsArray.length > 0) { if (allPatternsArray.length > 0) {
patterns = allPatternsArray.map((patternNode, index) => { // ✅ Caso 1: instrumentos do Rack do Beat/Bassline
// Tenta pegar o nome do atributo, ou gera um genérico if (parentBasslineId) {
const patternName = patternNode.getAttribute("name") || `Pattern ${index}`; // quantos BB tracks existem no projeto?
let bbCount = Array.isArray(sortedBBTrackNameNodes)
? sortedBBTrackNameNodes.length
: 0;
const patternSteps = parseInt(patternNode.getAttribute("steps"), 10) || 16; // garante bbCount >= maior índice encontrado no XML
const steps = new Array(patternSteps).fill(false); let maxIdx = 0;
const notes = []; for (const pn of allPatternsArray) {
// No XML base: 1 compasso (bar) = 192 ticks em 4/4, const posTicks = parseInt(pn.getAttribute("pos"), 10) || 0;
// então 1 beat = 48 ticks e 1 step (1/16) = 12 ticks. const idx = Math.round(posTicks / BAR_TICKS);
const ticksPerStep = 12; if (idx > maxIdx) maxIdx = idx;
}
bbCount = Math.max(bbCount, maxIdx + 1, 1);
patternNode.querySelectorAll("note").forEach((noteNode) => { // cria array denso (sem buracos)
const pos = parseInt(noteNode.getAttribute("pos"), 10) || 0; patterns = new Array(bbCount).fill(null).map((_, i) => {
const nameFromBB =
const rawLen = parseInt(noteNode.getAttribute("len"), 10) || 0; sortedBBTrackNameNodes?.[i]?.getAttribute?.("name") || null;
return makeEmptyPattern(i, nameFromBB);
// ✅ 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;
}); });
return { // injeta patterns do XML no índice certo (pos/192)
name: patternName, for (const pn of allPatternsArray) {
steps: steps, const posTicks = parseInt(pn.getAttribute("pos"), 10) || 0;
notes: notes, const idx = Math.round(posTicks / BAR_TICKS);
pos: parseInt(patternNode.getAttribute("pos"), 10) || 0, 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 { } else {
// Fallback: Nenhum pattern encontrado no XML, cria um padrão vazio // Fallback: Nenhum pattern encontrado no XML
patterns.push({ patterns.push(makeEmptyPattern(0, "Pattern 0"));
name: "Pattern 0",
steps: new Array(16).fill(false),
notes: [],
pos: 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;
@ -995,29 +1023,34 @@ async function handleActionBroadcast(action) {
// Notes // Notes
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];
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; 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();
break; break;
} }
case "UPDATE_PATTERN_NOTES": { case "UPDATE_PATTERN_NOTES": {
const { const {
@ -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?
// ---------------------- // ----------------------