fix(export): offline render VSTi qua engine khi chua play — fill midiCacheRef khong can play truoc
This commit is contained in:
+68
-9
@@ -27996,6 +27996,63 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Server-side Export ──
|
// ── 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 () => {
|
const triggerWavExport = async () => {
|
||||||
// Stop transport ngay khi bắt đầu export offline → tắt âm thanh đang phát.
|
// Stop transport ngay khi bắt đầu export offline → tắt âm thanh đang phát.
|
||||||
try { if (typeof stopAllPlayback === 'function') stopAllPlayback(); } catch (e) { }
|
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);
|
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
|
// sfEntry → file offline đủ duration nhưng KHÔNG CÓ ÂM THANH. Khi cache
|
||||||
// không đủ → TỰ chuyển sang bounce REALTIME (capture masterBus.output =
|
// không đủ (chưa play preview) → render MIDI qua engine ngay (VSTi/SF2
|
||||||
// đúng âm đang nghe) thay vì xuất file câm. Áp dụng Export Project (mix).
|
// trong bộ nhớ, preset đúng bridge channel) đổ vào midiCacheRef — export
|
||||||
if (midiTracks.length > 0 && !cacheOK && exportSettings.source === 'project' && exportSettings.format === 'wav') {
|
// offline có âm KHÔNG cần phát trước. Chỉ khi engine không render được
|
||||||
showToast("Cache MIDI chưa đủ — tự chuyển sang bounce REALTIME để giữ nguyên âm thanh...", "info");
|
// mới cảnh báo (file có thể thiếu âm MIDI) rồi vẫn xuất.
|
||||||
await triggerBounceExport();
|
if (midiTracks.length > 0 && !cacheOK && exportSettings.source !== 'active_clip' && exportSettings.source !== 'clip_selection') {
|
||||||
return;
|
const filled = await ensureMidiCacheViaEngine(midiTracks);
|
||||||
}
|
if (filled) {
|
||||||
if (midiTracks.length > 0 && !cacheOK) {
|
showToast("Đã render MIDI/VSTi offline qua engine — export không cần phát trước.", "success");
|
||||||
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");
|
} 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;
|
let exportTracks;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user