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(
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,
};
}
// =================================================================

View File

@ -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

View File

@ -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?
// ----------------------