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(
|
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)
|
||||||
const patternSteps = parseInt(patternNode.getAttribute("steps"), 10) || 16;
|
? sortedBBTrackNameNodes.length
|
||||||
const steps = new Array(patternSteps).fill(false);
|
: 0;
|
||||||
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) => {
|
// garante bbCount >= maior índice encontrado no XML
|
||||||
const pos = parseInt(noteNode.getAttribute("pos"), 10) || 0;
|
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;
|
// cria array denso (sem buracos)
|
||||||
|
patterns = new Array(bbCount).fill(null).map((_, i) => {
|
||||||
// ✅ LMMS costuma salvar one-shots com len negativo (ex: -192).
|
const nameFromBB =
|
||||||
// Na nossa DAW, tratamos isso como 1 step (1/16) = 12 ticks.
|
sortedBBTrackNameNodes?.[i]?.getAttribute?.("name") || null;
|
||||||
const len = rawLen < 0 ? TICKS_PER_STEP : rawLen;
|
return makeEmptyPattern(i, nameFromBB);
|
||||||
|
|
||||||
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)
|
return {
|
||||||
const pitch = !isNaN(pitchFromFile) ? pitchFromFile : 0;
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
|
||||||
|
|
@ -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({ type: "SYNC_PATTERN_STATE", xml });
|
||||||
sendAction({
|
}
|
||||||
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
// ----------------------
|
// ----------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue