atualizando em tempo real as patterns no editor de patterns e playlist
Deploy / Deploy (push) Successful in 2m7s
Details
Deploy / Deploy (push) Successful in 2m7s
Details
This commit is contained in:
parent
8b811aee07
commit
46105a8fb2
|
|
@ -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,28 +269,24 @@ function parseInstrumentNode(
|
|||
);
|
||||
});
|
||||
|
||||
// Se não houver patterns no XML, criamos um vazio para não quebrar a UI
|
||||
let patterns = [];
|
||||
const makeEmptyPattern = (idx, nameOverride = null) => ({
|
||||
name: nameOverride || `Pattern ${idx + 1}`,
|
||||
steps: new Array(16).fill(false),
|
||||
notes: [],
|
||||
pos: idx * BAR_TICKS,
|
||||
});
|
||||
|
||||
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 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 = [];
|
||||
// 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) => {
|
||||
const pos = parseInt(noteNode.getAttribute("pos"), 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;
|
||||
|
||||
notes.push({
|
||||
|
|
@ -300,29 +297,69 @@ function parseInstrumentNode(
|
|||
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 {
|
||||
name: patternName,
|
||||
steps: steps,
|
||||
notes: notes,
|
||||
pos: parseInt(patternNode.getAttribute("pos"), 10) || 0,
|
||||
steps,
|
||||
notes,
|
||||
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 {
|
||||
// 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
|
||||
// ✅ 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
|
||||
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 {
|
||||
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,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -997,22 +1025,27 @@ async function handleActionBroadcast(action) {
|
|||
case "TOGGLE_NOTE": {
|
||||
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] || { 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;
|
||||
|
||||
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
|
||||
});
|
||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||
}
|
||||
|
||||
saveStateToSession();
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
// ----------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue