diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index 959038c5..71bbb8f0 100755 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -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(); diff --git a/assets/js/creations/server/server.js b/assets/js/creations/server/server.js index bfcea501..4367145f 100755 --- a/assets/js/creations/server/server.js +++ b/assets/js/creations/server/server.js @@ -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;