corrigindo cortes de samples no download de projetos do mmpCreator
Deploy / Deploy (push) Successful in 2m12s
Details
Deploy / Deploy (push) Successful in 2m12s
Details
This commit is contained in:
parent
9ae9e47be5
commit
b34bbc28fd
|
|
@ -867,13 +867,13 @@ function createTrackXml(track) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function modifyAndSaveExistingMmp() {
|
async function modifyAndSaveExistingMmp() {
|
||||||
const content = generateXmlFromState();
|
console.log("EXPORT MMPZ");
|
||||||
downloadFile(content, "projeto_editado.mmp");
|
await generateAndDownloadMmpz("projeto_editado");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateNewMmp() {
|
async function generateNewMmp() {
|
||||||
const content = generateXmlFromState();
|
console.log("EXPORT MMPZ");
|
||||||
downloadFile(content, "novo_projeto.mmp");
|
await generateAndDownloadMmpz("novo_projeto");
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFile(content, fileName) {
|
function downloadFile(content, fileName) {
|
||||||
|
|
@ -891,22 +891,17 @@ function downloadFile(content, fileName) {
|
||||||
async function generateAndDownloadMmpz(baseName) {
|
async function generateAndDownloadMmpz(baseName) {
|
||||||
initializeAudioContext();
|
initializeAudioContext();
|
||||||
|
|
||||||
// 1) gera XML base (patterns OK) — hoje isso já funciona no seu projeto
|
const baseXmlString = generateXmlFromState(); // seu método atual (patterns OK)
|
||||||
const baseXmlString = generateXmlFromState(); // já inclui applyPlaylistClipsToXml :contentReference[oaicite:3]{index=3}
|
|
||||||
const xmlDoc = new DOMParser().parseFromString(baseXmlString, "application/xml");
|
const xmlDoc = new DOMParser().parseFromString(baseXmlString, "application/xml");
|
||||||
|
|
||||||
// 2) cria zip e injeta samples sliceados + reescreve sampletracks/sampletco
|
// JSZip precisa existir (você já usa pra abrir mmpz)
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
await applySampleTracksToXmlAndZip(xmlDoc, zip);
|
await applySampleTracksToXmlAndZip(xmlDoc, zip);
|
||||||
|
|
||||||
// 3) grava o .mmp dentro do zip
|
|
||||||
const finalXml = new XMLSerializer().serializeToString(xmlDoc);
|
const finalXml = new XMLSerializer().serializeToString(xmlDoc);
|
||||||
const mmpName = `${baseName}.mmp`;
|
zip.file(`${baseName}.mmp`, finalXml);
|
||||||
zip.file(mmpName, finalXml);
|
|
||||||
|
|
||||||
// 4) baixa .mmpz
|
|
||||||
const blob = await zip.generateAsync({ type: "blob", compression: "DEFLATE" });
|
const blob = await zip.generateAsync({ type: "blob", compression: "DEFLATE" });
|
||||||
downloadBlob(blob, `${baseName}.mmpz`);
|
downloadBlob(blob, `${baseName}.mmpz`);
|
||||||
}
|
}
|
||||||
|
|
@ -922,17 +917,19 @@ function downloadBlob(blob, fileName) {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- WAV helpers --------------------
|
function downloadBlob(blob, fileName) {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
function _sanitizeFileName(name) {
|
const a = document.createElement("a");
|
||||||
return String(name || "clip")
|
a.href = url;
|
||||||
.normalize("NFKD")
|
a.download = fileName;
|
||||||
.replace(/[^\w\-\.]+/g, "_")
|
document.body.appendChild(a);
|
||||||
.replace(/_+/g, "_")
|
a.click();
|
||||||
.replace(/^_+|_+$/g, "")
|
document.body.removeChild(a);
|
||||||
.slice(0, 80) || "clip";
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------- WAV helpers --------------------
|
||||||
|
|
||||||
function _clamp(n, a, b) {
|
function _clamp(n, a, b) {
|
||||||
const x = Number(n);
|
const x = Number(n);
|
||||||
if (!Number.isFinite(x)) return a;
|
if (!Number.isFinite(x)) return a;
|
||||||
|
|
@ -959,13 +956,13 @@ function _encodeWav16(channelData, sampleRate) {
|
||||||
view.setUint32(4, 36 + dataSize, true);
|
view.setUint32(4, 36 + dataSize, true);
|
||||||
writeStr(8, "WAVE");
|
writeStr(8, "WAVE");
|
||||||
writeStr(12, "fmt ");
|
writeStr(12, "fmt ");
|
||||||
view.setUint32(16, 16, true); // PCM
|
view.setUint32(16, 16, true);
|
||||||
view.setUint16(20, 1, true); // format = 1
|
view.setUint16(20, 1, true);
|
||||||
view.setUint16(22, numChannels, true);
|
view.setUint16(22, numChannels, true);
|
||||||
view.setUint32(24, sampleRate, true);
|
view.setUint32(24, sampleRate, true);
|
||||||
view.setUint32(28, byteRate, true);
|
view.setUint32(28, byteRate, true);
|
||||||
view.setUint16(32, blockAlign, true);
|
view.setUint16(32, blockAlign, true);
|
||||||
view.setUint16(34, 16, true); // bits
|
view.setUint16(34, 16, true);
|
||||||
writeStr(36, "data");
|
writeStr(36, "data");
|
||||||
view.setUint32(40, dataSize, true);
|
view.setUint32(40, dataSize, true);
|
||||||
|
|
||||||
|
|
@ -978,7 +975,6 @@ function _encodeWav16(channelData, sampleRate) {
|
||||||
offset += 2;
|
offset += 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -998,10 +994,7 @@ function _sliceToWavArrayBuffer(audioBuffer, offsetSec, durSec, { volume = 1, pa
|
||||||
const gR = Math.sin(angle) * volume;
|
const gR = Math.sin(angle) * volume;
|
||||||
|
|
||||||
const inCh = audioBuffer.numberOfChannels || 1;
|
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 outCh = (inCh === 1 && Math.abs(pan) > 1e-6) ? 2 : inCh;
|
||||||
|
|
||||||
const out = new Array(outCh).fill(0).map(() => new Float32Array(sliceLen));
|
const out = new Array(outCh).fill(0).map(() => new Float32Array(sliceLen));
|
||||||
|
|
||||||
if (inCh === 1) {
|
if (inCh === 1) {
|
||||||
|
|
@ -1014,23 +1007,32 @@ function _sliceToWavArrayBuffer(audioBuffer, offsetSec, durSec, { volume = 1, pa
|
||||||
out[1][i] = src[i] * gR;
|
out[1][i] = src[i] * gR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (inCh >= 2) {
|
} else {
|
||||||
const L = audioBuffer.getChannelData(0).subarray(start, end);
|
const L = audioBuffer.getChannelData(0).subarray(start, end);
|
||||||
const R = audioBuffer.getChannelData(1).subarray(start, end);
|
const R = audioBuffer.getChannelData(1).subarray(start, end);
|
||||||
for (let i = 0; i < sliceLen; i++) {
|
for (let i = 0; i < sliceLen; i++) {
|
||||||
out[0][i] = L[i] * gL;
|
out[0][i] = L[i] * gL;
|
||||||
out[1][i] = R[i] * gR;
|
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);
|
return _encodeWav16(out, sr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _makeSilenceWav(sampleRate = 44100, seconds = 0.1) {
|
||||||
|
const len = Math.max(1, Math.floor(sampleRate * seconds));
|
||||||
|
const ch = [new Float32Array(len)];
|
||||||
|
return _encodeWav16(ch, sampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ensureSilenceInZip(zip) {
|
||||||
|
if (__exportCache.silencePath) return __exportCache.silencePath;
|
||||||
|
const path = "samples/__silence.wav";
|
||||||
|
zip.file(path, _makeSilenceWav(44100, 0.1));
|
||||||
|
__exportCache.silencePath = path;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
async function _ensureClipBuffer(clip) {
|
async function _ensureClipBuffer(clip) {
|
||||||
if (clip?.buffer) return clip.buffer;
|
if (clip?.buffer) return clip.buffer;
|
||||||
|
|
||||||
|
|
@ -1047,24 +1049,74 @@ async function _ensureClipBuffer(clip) {
|
||||||
return decoded;
|
return decoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const __exportCache = {
|
||||||
|
decodedBySrc: new Map(), // src -> AudioBuffer
|
||||||
|
rawBySrc: new Map(), // src -> ArrayBuffer (wav original)
|
||||||
|
sliceByKey: new Map(), // key -> { path }
|
||||||
|
silencePath: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function _resetExportCache() {
|
||||||
|
__exportCache.decodedBySrc.clear();
|
||||||
|
__exportCache.rawBySrc.clear();
|
||||||
|
__exportCache.sliceByKey.clear();
|
||||||
|
__exportCache.silencePath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _fetchArrayBufferCached(url) {
|
||||||
|
if (__exportCache.rawBySrc.has(url)) return __exportCache.rawBySrc.get(url);
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error(`Falha ao buscar áudio: ${url}`);
|
||||||
|
const arr = await res.arrayBuffer();
|
||||||
|
__exportCache.rawBySrc.set(url, arr);
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _decodeAudioBufferCached(url) {
|
||||||
|
if (__exportCache.decodedBySrc.has(url)) return __exportCache.decodedBySrc.get(url);
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
const arr = await _fetchArrayBufferCached(url);
|
||||||
|
const decoded = await ctx.decodeAudioData(arr.slice(0));
|
||||||
|
__exportCache.decodedBySrc.set(url, decoded);
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------- XML: SampleTracks + zip --------------------
|
// -------------------- XML: SampleTracks + zip --------------------
|
||||||
|
|
||||||
async function applySampleTracksToXmlAndZip(xmlDoc, zip) {
|
async function applySampleTracksToXmlAndZip(xmlDoc, zip) {
|
||||||
|
_resetExportCache();
|
||||||
|
|
||||||
const songTc = xmlDoc.querySelector("song > trackcontainer");
|
const songTc = xmlDoc.querySelector("song > trackcontainer");
|
||||||
if (!songTc) return;
|
if (!songTc) return;
|
||||||
|
|
||||||
// sampletracks no LMMS são track[type="2"] e cada clipe é <sampletco> :contentReference[oaicite:4]{index=4}
|
// template se existir
|
||||||
const existingSample = songTc.querySelector(':scope > track[type="2"]');
|
const existingSample = songTc.querySelector(':scope > track[type="2"]');
|
||||||
const template = existingSample ? existingSample.cloneNode(true) : null;
|
const template = existingSample ? existingSample.cloneNode(true) : null;
|
||||||
|
|
||||||
// remove todas as sample tracks existentes e recria pelo estado atual
|
// remove todas as sample tracks antigas
|
||||||
Array.from(songTc.querySelectorAll(':scope > track[type="2"]')).forEach(n => n.remove());
|
Array.from(songTc.querySelectorAll(':scope > track[type="2"]')).forEach(n => n.remove());
|
||||||
|
|
||||||
const secondsPerStep = getSecondsPerStep();
|
const secondsPerStep = getSecondsPerStep();
|
||||||
|
|
||||||
const tracks = appState.audio?.tracks || [];
|
const tracks = appState.audio?.tracks || [];
|
||||||
const clips = appState.audio?.clips || [];
|
const clips = appState.audio?.clips || [];
|
||||||
|
|
||||||
// recria track por lane
|
// agrupa clips por lane
|
||||||
for (let i = 0; i < tracks.length; i++) {
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
const lane = tracks[i];
|
const lane = tracks[i];
|
||||||
const laneName = lane?.name || `Áudio ${i + 1}`;
|
const laneName = lane?.name || `Áudio ${i + 1}`;
|
||||||
|
|
@ -1084,7 +1136,7 @@ async function applySampleTracksToXmlAndZip(xmlDoc, zip) {
|
||||||
trackNode.setAttribute("type", "2");
|
trackNode.setAttribute("type", "2");
|
||||||
trackNode.setAttribute("name", laneName);
|
trackNode.setAttribute("name", laneName);
|
||||||
|
|
||||||
// garante sampletrack com vol/pan neutro (vamos “bakar” no WAV por clipe)
|
// sampletrack neutro (mix “baked” no wav)
|
||||||
let st = trackNode.querySelector("sampletrack");
|
let st = trackNode.querySelector("sampletrack");
|
||||||
if (!st) {
|
if (!st) {
|
||||||
st = xmlDoc.createElement("sampletrack");
|
st = xmlDoc.createElement("sampletrack");
|
||||||
|
|
@ -1093,7 +1145,6 @@ async function applySampleTracksToXmlAndZip(xmlDoc, zip) {
|
||||||
st.setAttribute("vol", "100");
|
st.setAttribute("vol", "100");
|
||||||
st.setAttribute("pan", "0");
|
st.setAttribute("pan", "0");
|
||||||
|
|
||||||
// limpa sampletco anteriores
|
|
||||||
Array.from(trackNode.querySelectorAll(":scope > sampletco")).forEach(n => n.remove());
|
Array.from(trackNode.querySelectorAll(":scope > sampletco")).forEach(n => n.remove());
|
||||||
|
|
||||||
const laneClips = clips
|
const laneClips = clips
|
||||||
|
|
@ -1104,36 +1155,97 @@ async function applySampleTracksToXmlAndZip(xmlDoc, zip) {
|
||||||
for (let cidx = 0; cidx < laneClips.length; cidx++) {
|
for (let cidx = 0; cidx < laneClips.length; cidx++) {
|
||||||
const clip = laneClips[cidx];
|
const clip = laneClips[cidx];
|
||||||
|
|
||||||
const buffer = await _ensureClipBuffer(clip);
|
const startSec = clip.startTimeInSeconds || 0;
|
||||||
if (!buffer) continue;
|
|
||||||
|
|
||||||
const offsetSec = clip.offset || 0;
|
|
||||||
const durSec = clip.durationInSeconds || 0;
|
const durSec = clip.durationInSeconds || 0;
|
||||||
if (durSec <= 0.0001) continue;
|
if (durSec <= 0.0001) continue;
|
||||||
|
|
||||||
const wav = _sliceToWavArrayBuffer(buffer, offsetSec, durSec, {
|
const vol = clip.volume ?? 1;
|
||||||
volume: clip.volume ?? 1,
|
const pan = clip.pan ?? 0;
|
||||||
pan: clip.pan ?? 0,
|
const muted = !!clip.muted || vol === 0;
|
||||||
});
|
|
||||||
if (!wav) continue;
|
|
||||||
|
|
||||||
const sliceName =
|
const posTicks = Math.round((startSec / secondsPerStep) * 12);
|
||||||
`${_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 lenTicks = Math.max(1, Math.round((durSec / secondsPerStep) * 12));
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// ✅ OTIMIZAÇÃO 1: mutado -> usa silence.wav
|
||||||
|
// -----------------------------------------
|
||||||
|
if (muted) {
|
||||||
|
const silPath = _ensureSilenceInZip(zip);
|
||||||
|
const tco = xmlDoc.createElement("sampletco");
|
||||||
|
tco.setAttribute("pos", String(Math.max(0, posTicks)));
|
||||||
|
tco.setAttribute("len", String(lenTicks));
|
||||||
|
tco.setAttribute("muted", "0"); // já é silêncio
|
||||||
|
tco.setAttribute("src", silPath);
|
||||||
|
trackNode.appendChild(tco);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// precisamos do sourcePath (arquivo original)
|
||||||
|
const srcUrl = clip.sourcePath || clip.src || clip.url;
|
||||||
|
if (!srcUrl) continue;
|
||||||
|
|
||||||
|
const offsetSec = clip.offset || 0;
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// ✅ OTIMIZAÇÃO 2: sem offset + sem bake mix
|
||||||
|
// -> usa arquivo original no zip (sem decode/encode)
|
||||||
|
// -----------------------------------------
|
||||||
|
const needsOffset = Math.abs(offsetSec) > 1e-6;
|
||||||
|
const needsBake = Math.abs((vol ?? 1) - 1) > 1e-6 || Math.abs((pan ?? 0) - 0) > 1e-6;
|
||||||
|
|
||||||
|
if (!needsOffset && !needsBake) {
|
||||||
|
const raw = await _fetchArrayBufferCached(srcUrl);
|
||||||
|
const name =
|
||||||
|
`${_sanitizeFileName(laneName)}__${_sanitizeFileName(clip.name || "clip")}` +
|
||||||
|
`__raw__${_sanitizeFileName(clip.id || String(cidx))}.wav`;
|
||||||
|
const zipPath = `samples/${name}`;
|
||||||
|
|
||||||
|
// evita regravar o mesmo raw várias vezes
|
||||||
|
if (!zip.file(zipPath)) zip.file(zipPath, raw);
|
||||||
|
|
||||||
const tco = xmlDoc.createElement("sampletco");
|
const tco = xmlDoc.createElement("sampletco");
|
||||||
tco.setAttribute("pos", String(Math.max(0, posTicks)));
|
tco.setAttribute("pos", String(Math.max(0, posTicks)));
|
||||||
tco.setAttribute("len", String(lenTicks));
|
tco.setAttribute("len", String(lenTicks));
|
||||||
tco.setAttribute("muted", String((clip.volume === 0 || clip.muted) ? 1 : 0));
|
tco.setAttribute("muted", "0");
|
||||||
tco.setAttribute("src", zipPath);
|
tco.setAttribute("src", zipPath);
|
||||||
|
trackNode.appendChild(tco);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// ✅ OTIMIZAÇÃO 3: cache de slice idêntico
|
||||||
|
// -----------------------------------------
|
||||||
|
const sliceKey = `${srcUrl}__o=${offsetSec.toFixed(6)}__d=${durSec.toFixed(6)}__v=${Number(vol).toFixed(6)}__p=${Number(pan).toFixed(6)}`;
|
||||||
|
const cached = __exportCache.sliceByKey.get(sliceKey);
|
||||||
|
if (cached) {
|
||||||
|
const tco = xmlDoc.createElement("sampletco");
|
||||||
|
tco.setAttribute("pos", String(Math.max(0, posTicks)));
|
||||||
|
tco.setAttribute("len", String(lenTicks));
|
||||||
|
tco.setAttribute("muted", "0");
|
||||||
|
tco.setAttribute("src", cached.path);
|
||||||
|
trackNode.appendChild(tco);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode uma vez por srcUrl
|
||||||
|
const buffer = await _decodeAudioBufferCached(srcUrl);
|
||||||
|
|
||||||
|
const wav = _sliceToWavArrayBuffer(buffer, offsetSec, durSec, { volume: vol, pan });
|
||||||
|
if (!wav) continue;
|
||||||
|
|
||||||
|
const sliceName =
|
||||||
|
`${_sanitizeFileName(laneName)}__${_sanitizeFileName(clip.name || "clip")}` +
|
||||||
|
`__slice__${_sanitizeFileName(clip.id || String(cidx))}.wav`;
|
||||||
|
const zipPath = `samples/${sliceName}`;
|
||||||
|
|
||||||
|
zip.file(zipPath, wav);
|
||||||
|
__exportCache.sliceByKey.set(sliceKey, { path: zipPath });
|
||||||
|
|
||||||
|
const tco = xmlDoc.createElement("sampletco");
|
||||||
|
tco.setAttribute("pos", String(Math.max(0, posTicks)));
|
||||||
|
tco.setAttribute("len", String(lenTicks));
|
||||||
|
tco.setAttribute("muted", "0");
|
||||||
|
tco.setAttribute("src", zipPath);
|
||||||
trackNode.appendChild(tco);
|
trackNode.appendChild(tco);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1141,4 +1253,5 @@ async function applySampleTracksToXmlAndZip(xmlDoc, zip) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export { generateXmlFromState as generateXmlFromStateExported };
|
export { generateXmlFromState as generateXmlFromStateExported };
|
||||||
Loading…
Reference in New Issue