fix(export): offline render VSTi qua engine khi chua play — fill midiCacheRef khong can play truoc

This commit is contained in:
2026-08-26 07:51:08 +07:00
parent 26086cb331
commit c146d3eb14
2 changed files with 97 additions and 27 deletions
+68 -9
View File
@@ -27996,6 +27996,63 @@ const App = () => {
};
// ── Server-side Export ──
// Render MIDI offline qua engine khi chưa play (midiCacheRef rỗng → export
// offline câm MIDI/VSTi). Render từng track qua native_bridge --render (VSTi/
// SF2 trong bộ nhớ, preset đúng bridge channel), decode WAV → đổ vào
// midiCacheRef cùng cấu trúc capture preview (chunks interleaved stereo) →
// clientSideExport có âm không cần play trước. Schema per-track giữ NGUYÊN
// mọi melodic track (channel round-robin phụ thuộc thứ tự melodic), chỉ clear
// items track khác; items track target giữ MIDI_ITEM (audio clips client đã
// schedule riêng qua t.buffer — không nhân đôi âm).
const ensureMidiCacheViaEngine = async (midiTracks) => {
if (serverStatus !== 'connected' || !window.SonicAPI || !window.SonicAPI.renderProject) return false;
const baseSchema = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm', bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted });
let okCount = 0;
for (const t of midiTracks) {
const schema = JSON.parse(JSON.stringify(baseSchema));
(schema.main_session.tracks || []).forEach(st => {
if (st.id === t.id) {
st.items = (st.items || []).filter(it => it.type === 'MIDI_ITEM');
} else {
st.items = [];
}
});
// Section store: bỏ hết items — âm section không được lẫn vào cache track MIDI.
Object.keys(schema.section_store || {}).forEach(sk => {
((schema.section_store[sk].tracks) || []).forEach(st => { st.items = []; });
});
try {
const rate = parseInt(exportSettings.sampleRate) || 44100;
const res = await window.SonicAPI.renderProject(schema, 'midicache_' + t.id + '_' + Date.now() + '.wav', {
bit_depth: 16,
sample_rate: rate,
dither: false,
normalize: false
});
const br = await fetch(res.url);
if (!br.ok) throw new Error('HTTP ' + br.status);
const ab = await br.arrayBuffer();
const dctx = new OfflineAudioContext(2, 1, rate);
const buf = await dctx.decodeAudioData(ab);
if (!buf || buf.length < 100) continue;
const L = buf.getChannelData(0);
const R = buf.numberOfChannels > 1 ? buf.getChannelData(1) : L;
const chunks = [];
const CH = 4096;
for (let i = 0; i < L.length; i += CH) {
const n = Math.min(CH, L.length - i);
const ch = new Float32Array(n * 2);
for (let j = 0; j < n; j++) { ch[j * 2] = L[i + j]; ch[j * 2 + 1] = R[i + j]; }
chunks.push(ch);
}
midiCacheRef.current[t.id] = { chunks, duration: buf.duration, sampleRate: buf.sampleRate };
okCount++;
} catch (e) {
console.warn('[MidiCache] engine render track', t.id, 'fail:', e);
}
}
return okCount > 0;
};
const triggerWavExport = async () => {
// Stop transport ngay khi bắt đầu export offline → tắt âm thanh đang phát.
try { if (typeof stopAllPlayback === 'function') stopAllPlayback(); } catch (e) { }
@@ -28031,15 +28088,17 @@ const App = () => {
return c && c.chunks.length > 0 && c.duration >= needDur - 0.3 && cacheHasEnergy(c);
});
// sfEntry → file offline đủ duration nhưng KHÔNG CÓ ÂM THANH. Khi cache
// không đủ → TỰ chuyển sang bounce REALTIME (capture masterBus.output =
// đúng âm đang nghe) thay vì xuất file câm. Áp dụng Export Project (mix).
if (midiTracks.length > 0 && !cacheOK && exportSettings.source === 'project' && exportSettings.format === 'wav') {
showToast("Cache MIDI chưa đủ — tự chuyển sang bounce REALTIME để giữ nguyên âm thanh...", "info");
await triggerBounceExport();
return;
}
if (midiTracks.length > 0 && !cacheOK) {
showToast("Project có MIDI items chưa đủ cache preview — file offline có thể thiếu âm MIDI. Bật Realtime để bounce trung thực.", "warning");
// không đủ (chưa play preview) → render MIDI qua engine ngay (VSTi/SF2
// trong bộ nhớ, preset đúng bridge channel) đổ vào midiCacheRef — export
// offline có âm KHÔNG cần phát trước. Chỉ khi engine không render được
// mới cảnh báo (file có thể thiếu âm MIDI) rồi vẫn xuất.
if (midiTracks.length > 0 && !cacheOK && exportSettings.source !== 'active_clip' && exportSettings.source !== 'clip_selection') {
const filled = await ensureMidiCacheViaEngine(midiTracks);
if (filled) {
showToast("Đã render MIDI/VSTi offline qua engine — export không cần phát trước.", "success");
} else {
showToast("Project có MIDI items chưa đủ cache preview — file offline có thể thiếu âm MIDI. Bật Realtime để bounce trung thực.", "warning");
}
}
}
let exportTracks;
File diff suppressed because one or more lines are too long