feat(export): thanh tien do offline export (render track VST -> encode -> save) trong ExportModal

This commit is contained in:
2026-08-24 18:24:42 +07:00
parent 993afb9ee4
commit edbb477657
3 changed files with 31 additions and 9 deletions
+23 -1
View File
@@ -12430,7 +12430,7 @@ const InteractiveEqPro = ({ track, params, onChange, getModule, applyTo, spectru
);
};
const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExporting, onExport }) => {
const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExporting, exportProgress, onExport }) => {
if (!open) return null;
const pickDir = async () => {
try {
@@ -12626,6 +12626,16 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
<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>
{isExporting && (
<div className="bg-[#141414] border border-zinc-800 rounded px-2 py-1.5">
<div className="h-1.5 bg-zinc-800 rounded-full overflow-hidden">
<div className="h-full bg-cyan-500 transition-all duration-200" style={{ width: (exportProgress >= 0 ? exportProgress : 25) + '%' }} />
</div>
<div className="text-[8px] text-zinc-500 mt-1">
{exportProgress >= 0 ? 'Đang xuất... ' + Math.round(exportProgress) + '%' : 'Đang xử lý...'}
</div>
</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">
@@ -17827,6 +17837,7 @@ const App = () => {
}, [selectionStart, selectionEnd, bpm]);
const [subTabHeight, setSubTabHeight] = useState(96);
const [isExporting, setIsExporting] = useState(false);
const [exportProgress, setExportProgress] = useState(-1);
const [projectName, setProjectName] = useState(() => localStorage.getItem('sonic_project_name') || '');
const [currentProjectId, setCurrentProjectId] = useState(() => localStorage.getItem('sonic_project_id') || null);
const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false);
@@ -28025,6 +28036,7 @@ const App = () => {
if (!exportSettings.loop && allOnServer && serverStatus === 'connected') {
// Use server-side export
setIsExporting(true);
setExportProgress(-1);
showToast("Đang gửi yêu cầu xuất âm thanh đến máy chủ...", "info");
try {
const sessionId = `session_${Date.now()}`;
@@ -28464,6 +28476,7 @@ const App = () => {
};
const clientSideExport = async (activeTracks, loopInfo) => {
setIsExporting(true);
setExportProgress(5);
showToast("Đang trộn âm thanh đa kênh (Offline Mixdown)...", "info");
try {
const targetRate = parseInt(exportSettings.sampleRate);
@@ -28579,7 +28592,10 @@ const App = () => {
// trên tổng → master VST chain cuối chain. Đúng thứ tự live: track fx
// → (sum) → master fx → out.
let mix = null;
let vstTi = 0;
for (const t of activeTracks) {
setExportProgress(10 + Math.round((vstTi / Math.max(1, activeTracks.length)) * 40));
vstTi++;
const chain = activeVstChain(t.vstFxChain || []);
const tctx = new OfflineAudioContext(2, Math.ceil(targetRate * Math.max(0.1, durationLimit)), targetRate);
const tNodes = {};
@@ -28626,6 +28642,7 @@ const App = () => {
});
renderedBuffer = await offlineCtx.startRendering();
}
setExportProgress(60);
const numExportCh = renderedBuffer.numberOfChannels;
const exportLength = renderedBuffer.length;
const bytesPerSample = bitDepth / 8;
@@ -28653,6 +28670,7 @@ const App = () => {
view.setUint32(40, exportLength * bytesPerSample * numExportCh, true);
let offset = 44;
for (let i = 0; i < exportLength; i++) {
if ((i & 0x3FFF) === 0) setExportProgress(60 + Math.round((i / Math.max(1, exportLength)) * 32));
for (let ch = 0; ch < numExportCh; ch++) {
const chData = renderedBuffer.getChannelData(ch);
const sample = Math.max(-1, Math.min(1, chData[i]));
@@ -28675,8 +28693,10 @@ const App = () => {
type: 'audio/wav'
});
const wavName = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
setExportProgress(95);
if ((exportSettings.format === 'mp3' || exportSettings.format === 'ogg') && serverStatus === 'connected') {
setIsExporting(true);
setExportProgress(-1);
showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...", "info");
try {
const file = new File([blob], `session_export.wav`, { type: 'audio/wav' });
@@ -28765,6 +28785,7 @@ const App = () => {
} catch (err) {
showToast("Lỗi xuất âm thanh: " + err.message, "error");
} finally {
setExportProgress(-1);
setIsExporting(false);
}
};
@@ -34563,6 +34584,7 @@ STRICT CONSTRAINTS:
exportSettings: exportSettings,
setExportSettings: setExportSettings,
isExporting: isExporting,
exportProgress: exportProgress,
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",
File diff suppressed because one or more lines are too long