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:
+24
-31
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user