criação de novas patterns de composição nos projetos
Deploy / Deploy (push) Successful in 2m0s
Details
Deploy / Deploy (push) Successful in 2m0s
Details
This commit is contained in:
parent
d003c2ccd6
commit
17d3419475
|
|
@ -553,20 +553,17 @@ export async function parseMmpContent(xmlString) {
|
||||||
name: trackName,
|
name: trackName,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (playlistClips.length === 0) return null;
|
// ❌ remove esta linha:
|
||||||
|
// if (playlistClips.length === 0) return null;
|
||||||
|
|
||||||
|
// ✅ mantém mesmo vazia
|
||||||
return {
|
return {
|
||||||
id: `bassline_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
id: `bassline_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
||||||
name: trackName,
|
name: trackName,
|
||||||
type: "bassline",
|
type: "bassline",
|
||||||
playlist_clips: playlistClips,
|
playlist_clips: playlistClips, // pode ser []
|
||||||
|
|
||||||
// 🔥 importante: qual “coluna/pattern” este BBTrack representa
|
|
||||||
patternIndex: idx,
|
patternIndex: idx,
|
||||||
|
|
||||||
// 🔥 importante: de onde vêm os instrumentos
|
|
||||||
instrumentSourceId: rackId,
|
instrumentSourceId: rackId,
|
||||||
|
|
||||||
volume: 1,
|
volume: 1,
|
||||||
pan: 0,
|
pan: 0,
|
||||||
patterns: [],
|
patterns: [],
|
||||||
|
|
@ -575,6 +572,7 @@ export async function parseMmpContent(xmlString) {
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// 4. COMBINAÇÃO E FINALIZAÇÃO
|
// 4. COMBINAÇÃO E FINALIZAÇÃO
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
|
|
@ -744,27 +742,60 @@ export function syncPatternStateToServer() {
|
||||||
const currentXml = generateXmlFromState();
|
const currentXml = generateXmlFromState();
|
||||||
sendAction({ type: "SYNC_PATTERN_STATE", xml: currentXml });
|
sendAction({ type: "SYNC_PATTERN_STATE", xml: currentXml });
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
|
|
||||||
|
|
||||||
|
function ensureBbTrackCount(xmlDoc, neededCount) {
|
||||||
|
const songTc = xmlDoc.querySelector("song > trackcontainer");
|
||||||
|
if (!songTc) return;
|
||||||
|
|
||||||
|
let bbTracks = Array.from(songTc.querySelectorAll(':scope > track[type="1"]'));
|
||||||
|
if (bbTracks.length === 0) return;
|
||||||
|
|
||||||
|
const template = bbTracks[bbTracks.length - 1];
|
||||||
|
|
||||||
|
while (bbTracks.length < neededCount) {
|
||||||
|
const clone = template.cloneNode(true);
|
||||||
|
|
||||||
|
// limpa blocos
|
||||||
|
Array.from(clone.querySelectorAll(":scope > bbtco")).forEach((n) => n.remove());
|
||||||
|
|
||||||
|
// deixa o container interno “limpo” (opcional)
|
||||||
|
const inner = clone.querySelector('bbtrack > trackcontainer');
|
||||||
|
if (inner) inner.querySelectorAll('track[type="0"]').forEach((n) => n.remove());
|
||||||
|
|
||||||
|
// nome default
|
||||||
|
clone.setAttribute("name", `Beat/Bassline ${bbTracks.length}`);
|
||||||
|
|
||||||
|
songTc.appendChild(clone);
|
||||||
|
bbTracks.push(clone);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPlaylistClipsToXml(xmlDoc) {
|
function applyPlaylistClipsToXml(xmlDoc) {
|
||||||
const bbTrackNodes = Array.from(xmlDoc.querySelectorAll('track[type="1"]'));
|
|
||||||
if (!bbTrackNodes.length) return;
|
|
||||||
|
|
||||||
const basslines = appState.pattern.tracks
|
const basslines = appState.pattern.tracks
|
||||||
.filter((t) => t.type === "bassline" && Number.isFinite(Number(t.patternIndex)))
|
.filter((t) => t.type === "bassline" && Number.isFinite(Number(t.patternIndex)))
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => Number(a.patternIndex) - Number(b.patternIndex));
|
.sort((a, b) => Number(a.patternIndex) - Number(b.patternIndex));
|
||||||
|
|
||||||
|
const maxIdx = Math.max(-1, ...basslines.map((b) => Number(b.patternIndex)));
|
||||||
|
ensureBbTrackCount(xmlDoc, maxIdx + 1);
|
||||||
|
|
||||||
|
const bbTrackNodes = Array.from(xmlDoc.querySelectorAll('track[type="1"]'));
|
||||||
|
if (!bbTrackNodes.length) return;
|
||||||
|
|
||||||
for (const b of basslines) {
|
for (const b of basslines) {
|
||||||
const idx = Number(b.patternIndex);
|
const idx = Number(b.patternIndex);
|
||||||
const node = bbTrackNodes[idx];
|
const node = bbTrackNodes[idx];
|
||||||
if (!node) continue;
|
if (!node) continue;
|
||||||
|
|
||||||
// remove bbtco existentes
|
// ✅ mantém nome/mute sincronizados
|
||||||
|
if (b.name) node.setAttribute("name", b.name);
|
||||||
|
node.setAttribute("muted", b.isMuted ? "1" : "0");
|
||||||
|
|
||||||
Array.from(node.querySelectorAll(":scope > bbtco")).forEach((n) => n.remove());
|
Array.from(node.querySelectorAll(":scope > bbtco")).forEach((n) => n.remove());
|
||||||
|
|
||||||
const clips = (b.playlist_clips || []).slice().sort((x, y) => (x.pos ?? 0) - (y.pos ?? 0));
|
const clips = (b.playlist_clips || []).slice().sort((x, y) => (x.pos ?? 0) - (y.pos ?? 0));
|
||||||
|
|
||||||
for (const c of clips) {
|
for (const c of clips) {
|
||||||
const el = xmlDoc.createElement("bbtco");
|
const el = xmlDoc.createElement("bbtco");
|
||||||
el.setAttribute("pos", String(Math.max(0, Math.floor(c.pos ?? 0))));
|
el.setAttribute("pos", String(Math.max(0, Math.floor(c.pos ?? 0))));
|
||||||
|
|
@ -774,6 +805,7 @@ function applyPlaylistClipsToXml(xmlDoc) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function createTrackXml(track) {
|
function createTrackXml(track) {
|
||||||
if (!track.patterns || track.patterns.length === 0) return "";
|
if (!track.patterns || track.patterns.length === 0) return "";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const zoomInBtn = document.getElementById("zoom-in-btn");
|
const zoomInBtn = document.getElementById("zoom-in-btn");
|
||||||
const zoomOutBtn = document.getElementById("zoom-out-btn");
|
const zoomOutBtn = document.getElementById("zoom-out-btn");
|
||||||
const deleteClipBtn = document.getElementById("delete-clip");
|
const deleteClipBtn = document.getElementById("delete-clip");
|
||||||
|
|
||||||
|
// Pattern vazia
|
||||||
|
const newPatternBtn = document.getElementById("new-pattern-btn");
|
||||||
|
newPatternBtn?.addEventListener("click", () => {
|
||||||
|
const name = prompt("Nome da nova pattern:", "");
|
||||||
|
sendAction({ type: "CREATE_NEW_PATTERN", name: name || "" });
|
||||||
|
});
|
||||||
|
|
||||||
// Configuração do botão de Gravação
|
// Configuração do botão de Gravação
|
||||||
const recordBtn = document.getElementById('record-btn');
|
const recordBtn = document.getElementById('record-btn');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,40 @@ import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
|
||||||
import { PORT_SOCK } from "./config.js";
|
import { PORT_SOCK } from "./config.js";
|
||||||
import { DEFAULT_PROJECT_XML } from "./utils.js"
|
import { DEFAULT_PROJECT_XML } from "./utils.js"
|
||||||
|
|
||||||
|
function _getRackIdFallback() {
|
||||||
|
// 1) pega do primeiro bassline que já tenha instrumentSourceId
|
||||||
|
const b = (appState.pattern.tracks || []).find(
|
||||||
|
(t) => t.type === "bassline" && t.instrumentSourceId
|
||||||
|
);
|
||||||
|
if (b?.instrumentSourceId) return b.instrumentSourceId;
|
||||||
|
|
||||||
|
// 2) pega do primeiro instrumento do rack (parentBasslineId)
|
||||||
|
const child = (appState.pattern.tracks || []).find(
|
||||||
|
(t) => t.type !== "bassline" && t.parentBasslineId
|
||||||
|
);
|
||||||
|
if (child?.parentBasslineId) return child.parentBasslineId;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _nextPatternIndex() {
|
||||||
|
const bassMax = Math.max(
|
||||||
|
-1,
|
||||||
|
...(appState.pattern.tracks || [])
|
||||||
|
.filter((t) => t.type === "bassline" && Number.isFinite(Number(t.patternIndex)))
|
||||||
|
.map((t) => Number(t.patternIndex))
|
||||||
|
);
|
||||||
|
|
||||||
|
const nonBassMax = Math.max(
|
||||||
|
-1,
|
||||||
|
...(appState.pattern.tracks || [])
|
||||||
|
.filter((t) => t.type !== "bassline")
|
||||||
|
.map((t) => (t.patterns?.length || 0) - 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Math.max(bassMax, nonBassMax) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Gera um ID único otimista (ex: "track_1678886401000_abc123")
|
// Gera um ID único otimista (ex: "track_1678886401000_abc123")
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
@ -616,6 +650,64 @@ async function handleActionBroadcast(action) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "CREATE_NEW_PATTERN": {
|
||||||
|
const patternIndex =
|
||||||
|
Number.isFinite(Number(action.patternIndex)) ? Number(action.patternIndex) : _nextPatternIndex();
|
||||||
|
|
||||||
|
const name = (action.name || "").trim() || `Beat/Bassline ${patternIndex}`;
|
||||||
|
|
||||||
|
// 1) garante patterns em todas as tracks reais
|
||||||
|
_ensurePatternsUpTo(patternIndex);
|
||||||
|
|
||||||
|
// 2) cria bassline track container (pattern “da playlist”)
|
||||||
|
let b = (appState.pattern.tracks || []).find(
|
||||||
|
(t) => t.type === "bassline" && Number(t.patternIndex) === patternIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!b) {
|
||||||
|
b = {
|
||||||
|
id: `bassline_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
||||||
|
name,
|
||||||
|
type: "bassline",
|
||||||
|
patternIndex,
|
||||||
|
playlist_clips: [],
|
||||||
|
patterns: [],
|
||||||
|
isMuted: false,
|
||||||
|
instrumentSourceId: _getRackIdFallback(),
|
||||||
|
volume: 1,
|
||||||
|
pan: 0,
|
||||||
|
};
|
||||||
|
appState.pattern.tracks.push(b);
|
||||||
|
} else {
|
||||||
|
b.name = name;
|
||||||
|
if (!b.instrumentSourceId) b.instrumentSourceId = _getRackIdFallback();
|
||||||
|
if (!Array.isArray(b.playlist_clips)) b.playlist_clips = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) renomeia a “coluna” nas patterns exportáveis
|
||||||
|
(appState.pattern.tracks || []).forEach((t) => {
|
||||||
|
if (t.type === "bassline") return;
|
||||||
|
if (t.patterns?.[patternIndex]) t.patterns[patternIndex].name = name;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4) já seleciona essa pattern (opcional, mas fica UX boa)
|
||||||
|
appState.pattern.activePatternIndex = patternIndex;
|
||||||
|
(appState.pattern.tracks || []).forEach((t) => (t.activePatternIndex = patternIndex));
|
||||||
|
|
||||||
|
try { schedulePatternRerender(); } catch {}
|
||||||
|
renderAll();
|
||||||
|
saveStateToSession();
|
||||||
|
|
||||||
|
// 5) persiste no servidor (igual você já faz em outros updates)
|
||||||
|
const isFromSelf = action.__senderId === socket.id;
|
||||||
|
if (window.ROOM_NAME && isFromSelf) {
|
||||||
|
const xml = generateXmlFromStateExported();
|
||||||
|
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "ADD_PLAYLIST_PATTERN_CLIP": {
|
case "ADD_PLAYLIST_PATTERN_CLIP": {
|
||||||
const { patternIndex, pos, len, clipId, name } = action;
|
const { patternIndex, pos, len, clipId, name } = action;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue