diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index cc2da840..0f23c0fd 100755 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -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 ""; diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index eea8596c..472b9a37 100755 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -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'); diff --git a/assets/js/creations/socket.js b/assets/js/creations/socket.js index cd79875f..3e7abf7b 100755 --- a/assets/js/creations/socket.js +++ b/assets/js/creations/socket.js @@ -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;