editando patterns na playlist
Deploy / Deploy (push) Successful in 2m4s
Details
Deploy / Deploy (push) Successful in 2m4s
Details
This commit is contained in:
parent
39fa026e92
commit
4f92c93ab5
|
|
@ -706,6 +706,8 @@ function generateXmlFromState() {
|
|||
});
|
||||
}
|
||||
|
||||
applyPlaylistClipsToXml(xmlDoc);
|
||||
|
||||
const serializer = new XMLSerializer();
|
||||
return serializer.serializeToString(xmlDoc);
|
||||
}
|
||||
|
|
@ -717,6 +719,34 @@ export function syncPatternStateToServer() {
|
|||
saveStateToSession();
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
for (const b of basslines) {
|
||||
const idx = Number(b.patternIndex);
|
||||
const node = bbTrackNodes[idx];
|
||||
if (!node) continue;
|
||||
|
||||
// remove bbtco existentes
|
||||
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))));
|
||||
el.setAttribute("len", String(Math.max(12, Math.floor(c.len ?? 192))));
|
||||
node.appendChild(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTrackXml(track) {
|
||||
if (!track.patterns || track.patterns.length === 0) return "";
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
closeOpenProjectModal,
|
||||
} from "./ui.js";
|
||||
import { renderAudioEditor } from "./audio/audio_ui.js";
|
||||
import { adjustValue, enforceNumericInput, DEFAULT_PROJECT_XML } from "./utils.js";
|
||||
import { adjustValue, enforceNumericInput, DEFAULT_PROJECT_XML, secondsToSongTicks, snapSongTicks, LMMS_TICKS_PER_BAR } from "./utils.js";
|
||||
import { ZOOM_LEVELS } from "./config.js";
|
||||
import { loadProjectFromServer } from "./file.js";
|
||||
import { sendAction, joinRoom, setUserName } from "./socket.js";
|
||||
|
|
@ -661,8 +661,31 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
|
||||
renderAll();
|
||||
showToast(`Editando: ${basslineTrack.name}`, "info");
|
||||
};
|
||||
};
|
||||
|
||||
const sendPatternToSongBtn = document.getElementById("send-pattern-to-song-btn");
|
||||
sendPatternToSongBtn?.addEventListener("click", () => {
|
||||
const bpm = parseFloat(document.getElementById("bpm-input")?.value) || 120;
|
||||
|
||||
// playhead atual (se estiver tocando, usa logical; senão, seek)
|
||||
const sec =
|
||||
(appState.audio.audioEditorLogicalTime ?? 0) ||
|
||||
(appState.audio.audioEditorSeekTime ?? 0) ||
|
||||
0;
|
||||
|
||||
const patternIndex = appState.pattern.activePatternIndex || 0;
|
||||
|
||||
let posTicks = secondsToSongTicks(sec, bpm);
|
||||
posTicks = snapSongTicks(posTicks, LMMS_TICKS_PER_BAR); // snap por compasso (fica LMMS-like)
|
||||
|
||||
sendAction({
|
||||
type: "ADD_PLAYLIST_PATTERN_CLIP",
|
||||
patternIndex,
|
||||
pos: posTicks,
|
||||
len: LMMS_TICKS_PER_BAR, // 1 compasso default
|
||||
clipId: `plc_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
window.exitPatternFocus = function() {
|
||||
|
|
|
|||
|
|
@ -443,6 +443,42 @@ function schedulePatternRerender() {
|
|||
});
|
||||
}
|
||||
|
||||
function _genPlaylistClipId() {
|
||||
return `plc_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function _ensureBasslineForPatternIndex(patternIndex) {
|
||||
let b = appState.pattern.tracks.find(
|
||||
(t) => t.type === "bassline" && Number(t.patternIndex) === Number(patternIndex)
|
||||
);
|
||||
|
||||
// fallback: cria se não existir (não quebra nada)
|
||||
if (!b) {
|
||||
b = {
|
||||
id: `bassline_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
||||
name: `Beat/Bassline ${patternIndex}`,
|
||||
type: "bassline",
|
||||
patternIndex: Number(patternIndex),
|
||||
playlist_clips: [],
|
||||
patterns: [],
|
||||
isMuted: false,
|
||||
instrumentSourceId: null,
|
||||
volume: 1,
|
||||
pan: 0,
|
||||
};
|
||||
appState.pattern.tracks.push(b);
|
||||
}
|
||||
|
||||
if (!Array.isArray(b.playlist_clips)) b.playlist_clips = [];
|
||||
|
||||
// garante ids nos clips antigos
|
||||
b.playlist_clips.forEach((c) => {
|
||||
if (!c.id) c.id = _genPlaylistClipId();
|
||||
});
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// BROADCAST
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -518,6 +554,96 @@ async function handleActionBroadcast(action) {
|
|||
showToast(`▶ ${who} Play bases`, "info");
|
||||
break;
|
||||
}
|
||||
|
||||
case "ADD_PLAYLIST_PATTERN_CLIP": {
|
||||
const { patternIndex, pos, len, clipId, name } = action;
|
||||
|
||||
const b = _ensureBasslineForPatternIndex(patternIndex);
|
||||
|
||||
const newClip = {
|
||||
id: clipId || _genPlaylistClipId(),
|
||||
pos: Math.max(0, Math.floor(pos ?? 0)),
|
||||
len: Math.max(12, Math.floor(len ?? 192)),
|
||||
name: name || b.name || "Pattern",
|
||||
};
|
||||
|
||||
// evita duplicar
|
||||
if (!b.playlist_clips.some((c) => String(c.id) === String(newClip.id))) {
|
||||
b.playlist_clips.push(newClip);
|
||||
b.playlist_clips.sort((a, c) => (a.pos ?? 0) - (c.pos ?? 0));
|
||||
}
|
||||
|
||||
renderAll();
|
||||
saveStateToSession();
|
||||
|
||||
// ✅ sync XML (inclui bbtco depois do patch no file.js)
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
try {
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||
} catch {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "UPDATE_PLAYLIST_PATTERN_CLIP": {
|
||||
const { patternIndex, clipId, pos, len } = action;
|
||||
const b = _ensureBasslineForPatternIndex(patternIndex);
|
||||
|
||||
const c = b.playlist_clips.find((x) => String(x.id) === String(clipId));
|
||||
if (!c) break;
|
||||
|
||||
if (pos !== undefined) c.pos = Math.max(0, Math.floor(pos));
|
||||
if (len !== undefined) c.len = Math.max(12, Math.floor(len));
|
||||
|
||||
b.playlist_clips.sort((a, d) => (a.pos ?? 0) - (d.pos ?? 0));
|
||||
|
||||
renderAll();
|
||||
saveStateToSession();
|
||||
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
try {
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||
} catch {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_PLAYLIST_PATTERN_CLIP": {
|
||||
const { patternIndex, clipId } = action;
|
||||
const b = _ensureBasslineForPatternIndex(patternIndex);
|
||||
|
||||
b.playlist_clips = b.playlist_clips.filter((c) => String(c.id) !== String(clipId));
|
||||
|
||||
// limpa seleção se era o selecionado
|
||||
if (appState.global.selectedPlaylistClipId === clipId) {
|
||||
appState.global.selectedPlaylistClipId = null;
|
||||
appState.global.selectedPlaylistPatternIndex = null;
|
||||
}
|
||||
|
||||
renderAll();
|
||||
saveStateToSession();
|
||||
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
try {
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||
} catch {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "SELECT_PLAYLIST_PATTERN_CLIP": {
|
||||
const { patternIndex, clipId } = action;
|
||||
appState.global.selectedPlaylistClipId = clipId ?? null;
|
||||
appState.global.selectedPlaylistPatternIndex =
|
||||
patternIndex ?? null;
|
||||
renderAll();
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
|
||||
case "UPDATE_PATTERN_NOTES": {
|
||||
const {
|
||||
trackIndex: ti,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ const globalState = {
|
|||
loopStartTime: 0,
|
||||
loopEndTime: 8,
|
||||
resizeMode: "trim",
|
||||
selectedClipId: null,
|
||||
selectedClipId: null,
|
||||
selectedPlaylistClipId: null,
|
||||
selectedPlaylistPatternIndex: null,
|
||||
isRecording: false,
|
||||
clipboard: null,
|
||||
lastRulerClickTime: 0,
|
||||
|
|
@ -58,6 +60,24 @@ function makePatternSnapshot() {
|
|||
name: t.name,
|
||||
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
|
||||
parentBasslineId: t.parentBasslineId ?? null,
|
||||
instrumentSourceId: t.instrumentSourceId ?? null,
|
||||
isMuted: t.isMuted ?? false,
|
||||
muted: t.muted ?? false,
|
||||
|
||||
patterns: (t.patterns || []).map((p) => ({
|
||||
name: p.name,
|
||||
steps: p.steps,
|
||||
|
|
@ -72,10 +92,12 @@ function makePatternSnapshot() {
|
|||
})),
|
||||
activeTrackId: appState.pattern.activeTrackId,
|
||||
activePatternIndex: appState.pattern.activePatternIndex,
|
||||
focusedBasslineId: appState.pattern.focusedBasslineId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ----------------------
|
||||
// Helper: existe snapshot local com áudio?
|
||||
// ----------------------
|
||||
|
|
@ -137,6 +159,8 @@ export function resetProjectState() {
|
|||
loopEndTime: 8,
|
||||
resizeMode: "trim",
|
||||
selectedClipId: null,
|
||||
selectedPlaylistClipId: null,
|
||||
selectedPlaylistPatternIndex: null,
|
||||
isRecording: false,
|
||||
clipboard: null,
|
||||
lastRulerClickTime: 0,
|
||||
|
|
|
|||
|
|
@ -195,4 +195,30 @@ export function adjustValue(inputElement, step) {
|
|||
|
||||
inputElement.value = newValue;
|
||||
inputElement.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// LMMS ticks helpers (Song Editor)
|
||||
// ===============================
|
||||
|
||||
export const LMMS_TICKS_PER_STEP = 12; // 1 step (1/16) = 12 ticks
|
||||
export const STEPS_PER_BAR = 16; // 4/4: 16 steps por compasso
|
||||
export const LMMS_TICKS_PER_BAR = LMMS_TICKS_PER_STEP * STEPS_PER_BAR; // 192
|
||||
|
||||
export function secondsToSongTicks(seconds, bpm = null) {
|
||||
const b = bpm ?? (parseFloat(document.getElementById("bpm-input")?.value) || 120);
|
||||
// stepIntervalSec = 60/(bpm*4) => ticksPerSec = 12/stepIntervalSec = bpm*0.8
|
||||
const ticks = seconds * b * 0.8;
|
||||
return Math.round(ticks);
|
||||
}
|
||||
|
||||
export function songTicksToSeconds(ticks, bpm = null) {
|
||||
const b = bpm ?? (parseFloat(document.getElementById("bpm-input")?.value) || 120);
|
||||
// seconds = ticks / (bpm*0.8)
|
||||
return ticks / (b * 0.8);
|
||||
}
|
||||
|
||||
export function snapSongTicks(ticks, snap = LMMS_TICKS_PER_STEP) {
|
||||
const s = Math.max(1, Math.floor(snap));
|
||||
return Math.round(ticks / s) * s;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@
|
|||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button
|
||||
id="send-pattern-to-playlist-btn"
|
||||
id="send-pattern-to-song-btn"
|
||||
class="control-btn"
|
||||
title="Renderizar para Áudio"
|
||||
>
|
||||
|
|
|
|||
Loading…
Reference in New Issue