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();
|
const serializer = new XMLSerializer();
|
||||||
return serializer.serializeToString(xmlDoc);
|
return serializer.serializeToString(xmlDoc);
|
||||||
}
|
}
|
||||||
|
|
@ -717,6 +719,34 @@ export function syncPatternStateToServer() {
|
||||||
saveStateToSession();
|
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) {
|
function createTrackXml(track) {
|
||||||
if (!track.patterns || track.patterns.length === 0) return "";
|
if (!track.patterns || track.patterns.length === 0) return "";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
closeOpenProjectModal,
|
closeOpenProjectModal,
|
||||||
} from "./ui.js";
|
} from "./ui.js";
|
||||||
import { renderAudioEditor } from "./audio/audio_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 { ZOOM_LEVELS } from "./config.js";
|
||||||
import { loadProjectFromServer } from "./file.js";
|
import { loadProjectFromServer } from "./file.js";
|
||||||
import { sendAction, joinRoom, setUserName } from "./socket.js";
|
import { sendAction, joinRoom, setUserName } from "./socket.js";
|
||||||
|
|
@ -661,8 +661,31 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
renderAll();
|
renderAll();
|
||||||
showToast(`Editando: ${basslineTrack.name}`, "info");
|
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() {
|
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
|
// BROADCAST
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
@ -518,6 +554,96 @@ async function handleActionBroadcast(action) {
|
||||||
showToast(`▶ ${who} Play bases`, "info");
|
showToast(`▶ ${who} Play bases`, "info");
|
||||||
break;
|
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": {
|
case "UPDATE_PATTERN_NOTES": {
|
||||||
const {
|
const {
|
||||||
trackIndex: ti,
|
trackIndex: ti,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ const globalState = {
|
||||||
loopStartTime: 0,
|
loopStartTime: 0,
|
||||||
loopEndTime: 8,
|
loopEndTime: 8,
|
||||||
resizeMode: "trim",
|
resizeMode: "trim",
|
||||||
selectedClipId: null,
|
selectedClipId: null,
|
||||||
|
selectedPlaylistClipId: null,
|
||||||
|
selectedPlaylistPatternIndex: null,
|
||||||
isRecording: false,
|
isRecording: false,
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
lastRulerClickTime: 0,
|
lastRulerClickTime: 0,
|
||||||
|
|
@ -58,6 +60,24 @@ function makePatternSnapshot() {
|
||||||
name: t.name,
|
name: t.name,
|
||||||
type: t.type,
|
type: t.type,
|
||||||
samplePath: t.samplePath,
|
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) => ({
|
patterns: (t.patterns || []).map((p) => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
steps: p.steps,
|
steps: p.steps,
|
||||||
|
|
@ -72,10 +92,12 @@ 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?
|
||||||
// ----------------------
|
// ----------------------
|
||||||
|
|
@ -137,6 +159,8 @@ export function resetProjectState() {
|
||||||
loopEndTime: 8,
|
loopEndTime: 8,
|
||||||
resizeMode: "trim",
|
resizeMode: "trim",
|
||||||
selectedClipId: null,
|
selectedClipId: null,
|
||||||
|
selectedPlaylistClipId: null,
|
||||||
|
selectedPlaylistPatternIndex: null,
|
||||||
isRecording: false,
|
isRecording: false,
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
lastRulerClickTime: 0,
|
lastRulerClickTime: 0,
|
||||||
|
|
|
||||||
|
|
@ -195,4 +195,30 @@ export function adjustValue(inputElement, step) {
|
||||||
|
|
||||||
inputElement.value = newValue;
|
inputElement.value = newValue;
|
||||||
inputElement.dispatchEvent(new Event("input", { bubbles: true }));
|
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>
|
<div class="toolbar-divider"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id="send-pattern-to-playlist-btn"
|
id="send-pattern-to-song-btn"
|
||||||
class="control-btn"
|
class="control-btn"
|
||||||
title="Renderizar para Áudio"
|
title="Renderizar para Áudio"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue