renderizando projetos no mmpCreator utilizando o lmms
Deploy / Deploy (push) Successful in 2m0s
Details
Deploy / Deploy (push) Successful in 2m0s
Details
This commit is contained in:
parent
d2e3369609
commit
1ee2b43285
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue