corrigindo cortes de samples no download de projetos do mmpCreator
Deploy / Deploy (push) Successful in 2m16s Details

This commit is contained in:
JotaChina 2025-12-28 09:47:30 -03:00
parent 717e1594d4
commit 9ae9e47be5
1 changed files with 262 additions and 9 deletions

View File

@ -12,7 +12,7 @@ import {
import { loadAudioForTrack } from "./pattern/pattern_state.js"; import { loadAudioForTrack } from "./pattern/pattern_state.js";
import { renderAll, getSamplePathMap } from "./ui.js"; import { renderAll, getSamplePathMap } from "./ui.js";
import { DEFAULT_PAN, DEFAULT_VOLUME, NOTE_LENGTH } from "./config.js"; import { DEFAULT_PAN, DEFAULT_VOLUME, NOTE_LENGTH } from "./config.js";
import { initializeAudioContext, getMainGainNode } from "./audio.js"; import { initializeAudioContext, getMainGainNode, getAudioContext } from "./audio.js";
import { DEFAULT_PROJECT_XML, getSecondsPerStep, SAMPLE_SRC } from "./utils.js"; import { DEFAULT_PROJECT_XML, getSecondsPerStep, SAMPLE_SRC } from "./utils.js";
import * as Tone from "https://esm.sh/tone"; import * as Tone from "https://esm.sh/tone";
import { sendAction } from "./socket.js"; import { sendAction } from "./socket.js";
@ -669,14 +669,15 @@ export async function parseMmpContent(xmlString) {
} }
// -------------------------------------------------------------- // --------------------------------------------------------------
// GERAÇÃO DE ARQUIVO (EXPORT) // GERAÇÃO DE ARQUIVO (EXPORT) — MMPZ com WAVs “sliceados”
// -------------------------------------------------------------- // --------------------------------------------------------------
export function generateMmpFile() { // ✅ agora é async porque vamos gerar zip + wavs
export async function generateMmpFile() {
if (appState.global.originalXmlDoc) { if (appState.global.originalXmlDoc) {
modifyAndSaveExistingMmp(); await modifyAndSaveExistingMmp();
} else { } else {
generateNewMmp(); await generateNewMmp();
} }
} }
@ -806,7 +807,6 @@ function applyPlaylistClipsToXml(xmlDoc) {
} }
} }
function createTrackXml(track) { function createTrackXml(track) {
if (!track.patterns || track.patterns.length === 0) return ""; if (!track.patterns || track.patterns.length === 0) return "";
@ -866,12 +866,12 @@ function createTrackXml(track) {
</track>`; </track>`;
} }
function modifyAndSaveExistingMmp() { async function modifyAndSaveExistingMmp() {
const content = generateXmlFromState(); const content = generateXmlFromState();
downloadFile(content, "projeto_editado.mmp"); downloadFile(content, "projeto_editado.mmp");
} }
function generateNewMmp() { async function generateNewMmp() {
const content = generateXmlFromState(); const content = generateXmlFromState();
downloadFile(content, "novo_projeto.mmp"); downloadFile(content, "novo_projeto.mmp");
} }
@ -888,4 +888,257 @@ function downloadFile(content, fileName) {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
export { generateXmlFromState as generateXmlFromStateExported }; async function generateAndDownloadMmpz(baseName) {
initializeAudioContext();
// 1) gera XML base (patterns OK) — hoje isso já funciona no seu projeto
const baseXmlString = generateXmlFromState(); // já inclui applyPlaylistClipsToXml :contentReference[oaicite:3]{index=3}
const xmlDoc = new DOMParser().parseFromString(baseXmlString, "application/xml");
// 2) cria zip e injeta samples sliceados + reescreve sampletracks/sampletco
// eslint-disable-next-line no-undef
const zip = new JSZip();
await applySampleTracksToXmlAndZip(xmlDoc, zip);
// 3) grava o .mmp dentro do zip
const finalXml = new XMLSerializer().serializeToString(xmlDoc);
const mmpName = `${baseName}.mmp`;
zip.file(mmpName, finalXml);
// 4) baixa .mmpz
const blob = await zip.generateAsync({ type: "blob", compression: "DEFLATE" });
downloadBlob(blob, `${baseName}.mmpz`);
}
function downloadBlob(blob, fileName) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// -------------------- WAV helpers --------------------
function _sanitizeFileName(name) {
return String(name || "clip")
.normalize("NFKD")
.replace(/[^\w\-\.]+/g, "_")
.replace(/_+/g, "_")
.replace(/^_+|_+$/g, "")
.slice(0, 80) || "clip";
}
function _clamp(n, a, b) {
const x = Number(n);
if (!Number.isFinite(x)) return a;
return Math.max(a, Math.min(b, x));
}
function _encodeWav16(channelData, sampleRate) {
const numChannels = channelData.length;
const length = channelData[0]?.length || 0;
const bytesPerSample = 2;
const blockAlign = numChannels * bytesPerSample;
const byteRate = sampleRate * blockAlign;
const dataSize = length * blockAlign;
const buffer = new ArrayBuffer(44 + dataSize);
const view = new DataView(buffer);
const writeStr = (off, s) => {
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
};
writeStr(0, "RIFF");
view.setUint32(4, 36 + dataSize, true);
writeStr(8, "WAVE");
writeStr(12, "fmt ");
view.setUint32(16, 16, true); // PCM
view.setUint16(20, 1, true); // format = 1
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, byteRate, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, 16, true); // bits
writeStr(36, "data");
view.setUint32(40, dataSize, true);
let offset = 44;
for (let i = 0; i < length; i++) {
for (let ch = 0; ch < numChannels; ch++) {
let s = channelData[ch][i] || 0;
s = Math.max(-1, Math.min(1, s));
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
offset += 2;
}
}
return buffer;
}
function _sliceToWavArrayBuffer(audioBuffer, offsetSec, durSec, { volume = 1, pan = 0 } = {}) {
const sr = audioBuffer.sampleRate;
const start = Math.max(0, Math.floor((offsetSec || 0) * sr));
const end = Math.min(audioBuffer.length, start + Math.floor((durSec || 0) * sr));
const sliceLen = Math.max(0, end - start);
if (sliceLen <= 0) return null;
volume = _clamp(volume, 0, 1.5);
pan = _clamp(pan, -1, 1);
// equal-power pan
const angle = (pan + 1) * (Math.PI / 4);
const gL = Math.cos(angle) * volume;
const gR = Math.sin(angle) * volume;
const inCh = audioBuffer.numberOfChannels || 1;
// se for mono e pan != 0, gera estéreo pra “congelar” a pan
const outCh = (inCh === 1 && Math.abs(pan) > 1e-6) ? 2 : inCh;
const out = new Array(outCh).fill(0).map(() => new Float32Array(sliceLen));
if (inCh === 1) {
const src = audioBuffer.getChannelData(0).subarray(start, end);
if (outCh === 1) {
for (let i = 0; i < sliceLen; i++) out[0][i] = src[i] * volume;
} else {
for (let i = 0; i < sliceLen; i++) {
out[0][i] = src[i] * gL;
out[1][i] = src[i] * gR;
}
}
} else if (inCh >= 2) {
const L = audioBuffer.getChannelData(0).subarray(start, end);
const R = audioBuffer.getChannelData(1).subarray(start, end);
for (let i = 0; i < sliceLen; i++) {
out[0][i] = L[i] * gL;
out[1][i] = R[i] * gR;
}
// se tiver mais canais, copia/atenua sem pan (raro)
for (let ch = 2; ch < outCh; ch++) {
const C = audioBuffer.getChannelData(ch).subarray(start, end);
for (let i = 0; i < sliceLen; i++) out[ch][i] = C[i] * volume;
}
}
return _encodeWav16(out, sr);
}
async function _ensureClipBuffer(clip) {
if (clip?.buffer) return clip.buffer;
// fallback: tenta buscar e decodificar do sourcePath
if (!clip?.sourcePath) return null;
const ctx = getAudioContext();
const res = await fetch(clip.sourcePath);
if (!res.ok) return null;
const arr = await res.arrayBuffer();
const decoded = await ctx.decodeAudioData(arr.slice(0));
clip.buffer = decoded;
return decoded;
}
// -------------------- XML: SampleTracks + zip --------------------
async function applySampleTracksToXmlAndZip(xmlDoc, zip) {
const songTc = xmlDoc.querySelector("song > trackcontainer");
if (!songTc) return;
// sampletracks no LMMS são track[type="2"] e cada clipe é <sampletco> :contentReference[oaicite:4]{index=4}
const existingSample = songTc.querySelector(':scope > track[type="2"]');
const template = existingSample ? existingSample.cloneNode(true) : null;
// remove todas as sample tracks existentes e recria pelo estado atual
Array.from(songTc.querySelectorAll(':scope > track[type="2"]')).forEach(n => n.remove());
const secondsPerStep = getSecondsPerStep();
const tracks = appState.audio?.tracks || [];
const clips = appState.audio?.clips || [];
// recria track por lane
for (let i = 0; i < tracks.length; i++) {
const lane = tracks[i];
const laneName = lane?.name || `Áudio ${i + 1}`;
const trackNode = template
? template.cloneNode(true)
: (() => {
const t = xmlDoc.createElement("track");
t.setAttribute("type", "2");
const st = xmlDoc.createElement("sampletrack");
st.setAttribute("vol", "100");
st.setAttribute("pan", "0");
t.appendChild(st);
return t;
})();
trackNode.setAttribute("type", "2");
trackNode.setAttribute("name", laneName);
// garante sampletrack com vol/pan neutro (vamos “bakar” no WAV por clipe)
let st = trackNode.querySelector("sampletrack");
if (!st) {
st = xmlDoc.createElement("sampletrack");
trackNode.insertBefore(st, trackNode.firstChild);
}
st.setAttribute("vol", "100");
st.setAttribute("pan", "0");
// limpa sampletco anteriores
Array.from(trackNode.querySelectorAll(":scope > sampletco")).forEach(n => n.remove());
const laneClips = clips
.filter(c => String(c.trackId) === String(lane.id))
.slice()
.sort((a, b) => (a.startTimeInSeconds || 0) - (b.startTimeInSeconds || 0));
for (let cidx = 0; cidx < laneClips.length; cidx++) {
const clip = laneClips[cidx];
const buffer = await _ensureClipBuffer(clip);
if (!buffer) continue;
const offsetSec = clip.offset || 0;
const durSec = clip.durationInSeconds || 0;
if (durSec <= 0.0001) continue;
const wav = _sliceToWavArrayBuffer(buffer, offsetSec, durSec, {
volume: clip.volume ?? 1,
pan: clip.pan ?? 0,
});
if (!wav) continue;
const sliceName =
`${_sanitizeFileName(laneName)}__${_sanitizeFileName(clip.name || "clip")}` +
`__${_sanitizeFileName(clip.id || String(cidx))}.wav`;
const zipPath = `samples/${sliceName}`;
zip.file(zipPath, wav);
// converte segundos -> ticks LMMS (12 ticks por step), inverso do seu parser :contentReference[oaicite:5]{index=5}
const posTicks = Math.round(((clip.startTimeInSeconds || 0) / secondsPerStep) * 12);
const lenTicks = Math.max(1, Math.round((durSec / secondsPerStep) * 12));
const tco = xmlDoc.createElement("sampletco");
tco.setAttribute("pos", String(Math.max(0, posTicks)));
tco.setAttribute("len", String(lenTicks));
tco.setAttribute("muted", String((clip.volume === 0 || clip.muted) ? 1 : 0));
tco.setAttribute("src", zipPath);
trackNode.appendChild(tco);
}
songTc.appendChild(trackNode);
}
}
export { generateXmlFromState as generateXmlFromStateExported };