feat(export): modal chi 2 nut Export/Huy + option Realtime, Bounce MIDI chuyen vao menu File, bo auto-bounce offline

This commit is contained in:
2026-08-24 18:19:11 +07:00
parent 26ca6e6ae0
commit 993afb9ee4
3 changed files with 33 additions and 47 deletions
+24 -31
View File
@@ -12430,7 +12430,7 @@ const InteractiveEqPro = ({ track, params, onChange, getModule, applyTo, spectru
);
};
const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExporting, onExport, onBounce, onRenderMastering }) => {
const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExporting, onExport }) => {
if (!open) return null;
const pickDir = async () => {
try {
@@ -12618,20 +12618,22 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
placeholder="US-ABC-26-00001" className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none" />
</div>
</div>
<div className="grid grid-cols-3 gap-2 pt-1">
<button onClick={onBounce} disabled={isExporting}
title="Bounce realtime — file WAV đầy đủ MIDI + FX Rack + Mastering Chain (chạy lại project thật)"
className="w-full py-1 bg-fuchsia-800 hover:bg-fuchsia-700 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1">
<i data-lucide="radio" className="w-3 h-3" /> {isExporting ? '...' : 'Bounce MIDI'}
</button>
<div className="flex items-center gap-2 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5">
<label className="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" checked={!!exportSettings.realtime}
onChange={e => setExportSettings(p => ({ ...p, realtime: e.target.checked }))}
className="accent-cyan-500 w-3.5 h-3.5" />
<span className="text-xs text-zinc-300">Realtime (bounce — chạy lại project thật, giữ nguyên MIDI + FX Rack + Mastering Chain)</span>
</label>
</div>
<div className="grid grid-cols-2 gap-2 pt-1">
<button onClick={onExport} disabled={isExporting}
className="w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1">
<i data-lucide="download-cloud" className="w-3 h-3" /> {isExporting ? '...' : 'Export'}
</button>
<button onClick={onRenderMastering} disabled={isExporting}
title="Render qua engine Python + mastering chain (VST3 bridge) — bit-depth/SR/dither/normalize (-1dBTP|-14LUFS) + mono side/mid check, trả LUFS + dBTP"
className="w-full py-1 bg-amber-800 hover:bg-amber-700 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1">
<i data-lucide="gauge" className="w-3 h-3" /> {isExporting ? '...' : 'Mastering'}
<button onClick={onClose} disabled={isExporting}
className="w-full py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 font-bold rounded text-xs">
Hủy
</button>
</div>
</div>
@@ -18404,6 +18406,7 @@ const App = () => {
normalize: true,
normalizeTarget: '-14lufs',
monoCheck: true,
realtime: false,
// Gap 13: BWF/INFO metadata cho WAV mastering export
metaTitle: '',
metaArtist: '',
@@ -27921,16 +27924,13 @@ const App = () => {
// ── Server-side Export ──
const triggerWavExport = async () => {
// TỰ ĐỘNG bounce realtime nếu project có MIDI items: FluidSynth (ScriptProcessor)
// không render được offline, nên bounce là cách duy nhất giữ trung thực
// MIDI + FX Rack + Mastering Chain trong file xuất.
// Realtime UNCHECKED → export OFFLINE thuần (server-side nếu connected, fallback client).
// MIDI không render được trong OfflineAudioContext: nếu project có MIDI items mà
// cache preview chưa đủ → file xuất có thể thiếu âm MIDI. Cảnh báo, vẫn xuất offline.
const allT = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
const hasMidiItems = allT.some(t => t.midiItems && t.midiItems.length > 0)
|| (sessionTabs || []).some(s => (s.tracks || []).some(t => t.midiItems && t.midiItems.length > 0));
if (hasMidiItems) {
// Nếu preview đã cache đủ độ dài cho MỌI track MIDI → export OFFLINE NHANH
// bằng cache (vẫn qua FX Rack + Mastering như playback). Ngược lại →
// bounce realtime để đảm bảo trung thực.
const midiTracks = allT.filter(t => t.midiItems && t.midiItems.length > 0);
let needDur = 1.0;
midiTracks.forEach(t => {
@@ -27939,10 +27939,6 @@ const App = () => {
else if (m.startTime) needDur = Math.max(needDur, m.startTime + (m.duration || 8));
});
});
// Cache có thể "đủ" (chunks + duration) nhưng RỖNG — khi bridge VSTi active,
// âm đi qua BridgeAudioNode, KHÔNG qua track.sfEntry nơi ensureMidiCapture
// tap → chunks toàn zero → export offline ra file câm tuyệt đối. Kiểm tra
// năng lượng cache: nếu không có tín hiệu → bounce realtime (bắt đúng âm bridge).
const cacheHasEnergy = (c) => {
if (!c) return false;
const chunks = c.chunks || [];
@@ -27958,14 +27954,9 @@ const App = () => {
const c = midiCacheRef.current[t.id];
return c && c.chunks.length > 0 && c.duration >= needDur - 0.3 && cacheHasEnergy(c);
});
if (cacheOK) {
showToast("Dùng MIDI cache từ preview — export OFFLINE NHANH (vẫn đủ FX Rack + Mastering Chain).", "info");
clientSideExport(allT);
return;
if (!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");
}
showToast("Chưa có MIDI cache đầy đủ — bounce realtime để đảm bảo trung thực.", "info");
triggerBounceExport();
return;
}
let exportTracks;
let clipStart = 0;
@@ -31214,6 +31205,10 @@ STRICT CONSTRAINTS:
label: 'Export Mix...',
icon: 'file-output',
action: () => setShowExportPanel(true)
}, {
label: 'Bounce MIDI...',
icon: 'radio',
action: () => triggerBounceExport()
}, {
label: 'Export MIDI...',
icon: 'music',
@@ -34568,9 +34563,7 @@ STRICT CONSTRAINTS:
exportSettings: exportSettings,
setExportSettings: setExportSettings,
isExporting: isExporting,
onExport: triggerWavExport,
onBounce: triggerBounceExport,
onRenderMastering: renderMasteringExport
onExport: () => exportSettings.realtime ? triggerBounceExport() : triggerWavExport()
}), instrumentSelectorTrackId && /*#__PURE__*/React.createElement("div", {
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
onClick: closeInstrumentSelector
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
<script src="/static/js/app.precompiled.js?v=202608231902" defer></script>
<script src="/static/js/app.precompiled.js?v=202608232330" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {