criação de novas patterns de composição nos projetos
Deploy / Deploy (push) Successful in 2m0s Details

This commit is contained in:
JotaChina 2025-12-27 18:32:06 -03:00
parent d003c2ccd6
commit 17d3419475
3 changed files with 144 additions and 12 deletions

View File

@ -553,20 +553,17 @@ export async function parseMmpContent(xmlString) {
name: trackName,
}));
if (playlistClips.length === 0) return null;
// ❌ remove esta linha:
// if (playlistClips.length === 0) return null;
// ✅ mantém mesmo vazia
return {
id: `bassline_${Date.now()}_${Math.random().toString(36).slice(2)}`,
name: trackName,
type: "bassline",
playlist_clips: playlistClips,
// 🔥 importante: qual “coluna/pattern” este BBTrack representa
playlist_clips: playlistClips, // pode ser []
patternIndex: idx,
// 🔥 importante: de onde vêm os instrumentos
instrumentSourceId: rackId,
volume: 1,
pan: 0,
patterns: [],
@ -575,6 +572,7 @@ export async function parseMmpContent(xmlString) {
})
.filter(Boolean);
// -------------------------------------------------------------
// 4. COMBINAÇÃO E FINALIZAÇÃO
// -------------------------------------------------------------
@ -744,27 +742,60 @@ export function syncPatternStateToServer() {
const currentXml = generateXmlFromState();
sendAction({ type: "SYNC_PATTERN_STATE", xml: currentXml });
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) {
const bbTrackNodes = Array.from(xmlDoc.querySelectorAll('track[type="1"]'));
if (!bbTrackNodes.length) return;
const basslines = appState.pattern.tracks
.filter((t) => t.type === "bassline" && Number.isFinite(Number(t.patternIndex)))
.slice()
.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) {
const idx = Number(b.patternIndex);
const node = bbTrackNodes[idx];
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());
const clips = (b.playlist_clips || []).slice().sort((x, y) => (x.pos ?? 0) - (y.pos ?? 0));
for (const c of clips) {
const el = xmlDoc.createElement("bbtco");
el.setAttribute("pos", String(Math.max(0, Math.floor(c.pos ?? 0))));
@ -774,6 +805,7 @@ function applyPlaylistClipsToXml(xmlDoc) {
}
}
function createTrackXml(track) {
if (!track.patterns || track.patterns.length === 0) return "";

View File

@ -125,6 +125,14 @@ document.addEventListener("DOMContentLoaded", () => {
const zoomInBtn = document.getElementById("zoom-in-btn");
const zoomOutBtn = document.getElementById("zoom-out-btn");
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
const recordBtn = document.getElementById('record-btn');

View File

@ -47,6 +47,40 @@ import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
import { PORT_SOCK } from "./config.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")
// -----------------------------------------------------------------------------
@ -616,6 +650,64 @@ async function handleActionBroadcast(action) {
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": {
const { patternIndex, pos, len, clipId, name } = action;