renderizando projetos no mmpCreator utilizando o lmms
Deploy / Deploy (push) Successful in 2m0s Details

This commit is contained in:
JotaChina 2025-12-28 14:41:30 -03:00
parent d2e3369609
commit 1ee2b43285
2 changed files with 213 additions and 1 deletions

View File

@ -129,6 +129,82 @@ document.addEventListener("DOMContentLoaded", () => {
const removePatternBtn = document.getElementById("remove-pattern-btn");
const downloadPackageBtn = document.getElementById("download-package-btn");
// Render
const renderAudioBtn = document.getElementById("render-audio-btn");
renderAudioBtn?.addEventListener("click", async () => {
const fmt = (prompt("Formato: wav / mp3 / ogg / flac", "wav") || "")
.trim()
.toLowerCase();
if (!fmt) return;
const allowed = new Set(["wav", "mp3", "ogg", "flac"]);
const format = allowed.has(fmt) ? fmt : "wav";
const originalIcon = renderAudioBtn.className;
renderAudioBtn.className = "fa-solid fa-spinner fa-spin";
renderAudioBtn.style.pointerEvents = "none";
try {
showToast("🎛️ Renderizando no LMMS (servidor)...", "info", 4000);
// mesma lógica do socket: room vem da URL
const roomName = new URLSearchParams(window.location.search).get("room");
// usa a mesma porta do socket (o socket.js usa PORT_SOCK) :contentReference[oaicite:7]{index=7}
// se você não quiser mexer em imports, dá pra trocar por ':33001' direto.
const baseUrl = `https://${window.location.hostname}:33001`;
const body = {
roomName: roomName || null,
format,
name:
appState.global?.currentBeatBasslineName ||
appState.global?.projectName ||
"projeto",
};
const resp = await fetch(`${baseUrl}/render`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!resp.ok) {
const txt = await resp.text();
throw new Error(txt || `HTTP ${resp.status}`);
}
const blob = await resp.blob();
// tenta usar filename vindo do Content-Disposition
let filename = `projeto.${format}`;
const cd = resp.headers.get("Content-Disposition");
const m = cd && /filename="?([^"]+)"?/i.exec(cd);
if (m?.[1]) filename = m[1];
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1500);
showToast("✅ Render concluído!", "success", 3000);
} catch (e) {
console.error(e);
showToast("❌ Falha ao renderizar no LMMS.", "error", 6000);
alert("Falha ao renderizar no LMMS. Veja o console para detalhes.");
} finally {
renderAudioBtn.className = originalIcon;
renderAudioBtn.style.pointerEvents = "auto";
}
});
// Download projeto
downloadPackageBtn?.addEventListener("click", generateMmpFile);
@ -369,7 +445,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (file) handleFileLoad(file).then(() => closeOpenProjectModal());
});
uploadSampleBtn?.addEventListener("click", () => sampleFileInput?.click());
saveMmpBtn?.addEventListener("click", renderProjectAndDownload);
saveMmpBtn?.addEventListener("click", generateMmpFile);
addInstrumentBtn?.addEventListener("click", () => {
initializeAudioContext();

View File

@ -9,6 +9,10 @@ const { Server } = require("socket.io");
const fs = require("fs");
const path = require("path");
const pino = require("pino");
const os = require("os");
const crypto = require("crypto");
const { spawn } = require("child_process");
//import { LOG_SERVER } from "../utils.js"
const LOG_SERVER = `/var/www/html/trens/src_mmpSearch/logs/creation_logs/server`
const SESSION_JSON = `/var/www/html/trens/src_mmpSearch/logs/creation_logs/sessions`
@ -627,6 +631,138 @@ io.on("connection", (socket) => {
});
});
// Conversão de projeto em áudio no mmpCreator
app.use(express.json({ limit: "25mb" }));
app.use((req, res, next) => {
const origin = req.headers.origin;
// ajuste se você usa outros hosts no dev
const allowed = new Set([
"https://alice.ufsj.edu.br",
]);
if (origin && allowed.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Vary", "Origin");
}
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
res.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
if (req.method === "OPTIONS") return res.sendStatus(204);
next();
});
const LMMS_BIN = process.env.LMMS_BIN || "lmms";
const LMMS_TIMEOUT_MS = Number(process.env.LMMS_TIMEOUT_MS || 5 * 60 * 1000);
// opcional (pra servidor sem display): LMMS_USE_XVFB=1
function buildLmmsCommand(inputMmp, outputAudio) {
const useXvfb =
process.env.LMMS_USE_XVFB === "1" ||
(!process.env.DISPLAY && process.env.LMMS_USE_XVFB !== "0");
if (useXvfb) {
// xvfb-run -a lmms -r in.mmp -o out.wav
return { cmd: "xvfb-run", args: ["-a", LMMS_BIN, "-r", inputMmp, "-o", outputAudio] };
}
return { cmd: LMMS_BIN, args: ["-r", inputMmp, "-o", outputAudio] };
}
function sanitizeFileName(name) {
return String(name || "projeto")
.normalize("NFKD")
.replace(/[^\w\s.-]/g, "")
.trim()
.replace(/\s+/g, "_")
.slice(0, 120) || "projeto";
}
function run(cmd, args, { timeoutMs } = {}) {
return new Promise((resolve, reject) => {
const p = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
let out = "";
let err = "";
p.stdout.on("data", (d) => (out += d.toString()));
p.stderr.on("data", (d) => (err += d.toString()));
const t = setTimeout(() => {
try { p.kill("SIGKILL"); } catch {}
reject(new Error(`LMMS timeout (${timeoutMs}ms)\n${err || out}`));
}, timeoutMs || LMMS_TIMEOUT_MS);
p.on("error", (e) => {
clearTimeout(t);
reject(e);
});
p.on("close", (code) => {
clearTimeout(t);
if (code === 0) return resolve({ out, err });
reject(new Error(`LMMS exit code=${code}\n${err || out}`));
});
});
}
// trava simples pra não renderizar a mesma sala em paralelo
const renderLocks = new Map();
app.post("/render", async (req, res) => {
const { roomName, xml, format, name } = req.body || {};
const ext = String(format || "wav").toLowerCase();
const allowed = new Set(["wav", "ogg", "flac", "mp3"]);
if (!allowed.has(ext)) {
return res.status(400).json({ ok: false, error: "invalid_format", allowed: [...allowed] });
}
// prioridade: sala -> pega do estado autoritativo
let projectXml = null;
if (roomName) {
if (renderLocks.get(roomName)) {
return res.status(429).json({ ok: false, error: "render_in_progress" });
}
projectXml = ensureRoom(roomName)?.projectXml || null;
}
// fallback: se mandou xml direto
if (!projectXml) projectXml = xml;
if (!projectXml || String(projectXml).trim().length === 0) {
return res.status(400).json({ ok: false, error: "missing_xml_or_room" });
}
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lmms-render-"));
const inputPath = path.join(tmpDir, "project.mmp");
const outputPath = path.join(tmpDir, `render.${ext}`);
if (roomName) renderLocks.set(roomName, true);
try {
fs.writeFileSync(inputPath, projectXml, "utf8");
const { cmd, args } = buildLmmsCommand(inputPath, outputPath);
await run(cmd, args, { timeoutMs: LMMS_TIMEOUT_MS });
const fileBase = sanitizeFileName(name || roomName || "projeto");
const downloadName = `${fileBase}.${ext}`;
return res.download(outputPath, downloadName, (err) => {
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
if (roomName) renderLocks.delete(roomName);
if (err) console.error("[/render] download error:", err);
});
} catch (e) {
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
if (roomName) renderLocks.delete(roomName);
console.error("[/render] fail:", e);
return res.status(500).json({ ok: false, error: "render_failed", details: String(e?.message || e) });
}
});
// --- ENDPOINT DE NOTIFICAÇÃO EXTERNA ---
app.post("/notify-update", express.json(), (req, res) => {
const { updateType } = req.body;