feat(export): thanh tien do offline export (render track VST -> encode -> save) trong ExportModal
This commit is contained in:
+23
-1
@@ -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;
|
if (!open) return null;
|
||||||
const pickDir = async () => {
|
const pickDir = async () => {
|
||||||
try {
|
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>
|
<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>
|
</label>
|
||||||
</div>
|
</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">
|
<div className="grid grid-cols-2 gap-2 pt-1">
|
||||||
<button onClick={onExport} disabled={isExporting}
|
<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">
|
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]);
|
}, [selectionStart, selectionEnd, bpm]);
|
||||||
const [subTabHeight, setSubTabHeight] = useState(96);
|
const [subTabHeight, setSubTabHeight] = useState(96);
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
|
const [exportProgress, setExportProgress] = useState(-1);
|
||||||
const [projectName, setProjectName] = useState(() => localStorage.getItem('sonic_project_name') || '');
|
const [projectName, setProjectName] = useState(() => localStorage.getItem('sonic_project_name') || '');
|
||||||
const [currentProjectId, setCurrentProjectId] = useState(() => localStorage.getItem('sonic_project_id') || null);
|
const [currentProjectId, setCurrentProjectId] = useState(() => localStorage.getItem('sonic_project_id') || null);
|
||||||
const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false);
|
const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false);
|
||||||
@@ -28025,6 +28036,7 @@ const App = () => {
|
|||||||
if (!exportSettings.loop && allOnServer && serverStatus === 'connected') {
|
if (!exportSettings.loop && allOnServer && serverStatus === 'connected') {
|
||||||
// Use server-side export
|
// Use server-side export
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
|
setExportProgress(-1);
|
||||||
showToast("Đang gửi yêu cầu xuất âm thanh đến máy chủ...", "info");
|
showToast("Đang gửi yêu cầu xuất âm thanh đến máy chủ...", "info");
|
||||||
try {
|
try {
|
||||||
const sessionId = `session_${Date.now()}`;
|
const sessionId = `session_${Date.now()}`;
|
||||||
@@ -28464,6 +28476,7 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
const clientSideExport = async (activeTracks, loopInfo) => {
|
const clientSideExport = async (activeTracks, loopInfo) => {
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
|
setExportProgress(5);
|
||||||
showToast("Đang trộn âm thanh đa kênh (Offline Mixdown)...", "info");
|
showToast("Đang trộn âm thanh đa kênh (Offline Mixdown)...", "info");
|
||||||
try {
|
try {
|
||||||
const targetRate = parseInt(exportSettings.sampleRate);
|
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
|
// trên tổng → master VST chain cuối chain. Đúng thứ tự live: track fx
|
||||||
// → (sum) → master fx → out.
|
// → (sum) → master fx → out.
|
||||||
let mix = null;
|
let mix = null;
|
||||||
|
let vstTi = 0;
|
||||||
for (const t of activeTracks) {
|
for (const t of activeTracks) {
|
||||||
|
setExportProgress(10 + Math.round((vstTi / Math.max(1, activeTracks.length)) * 40));
|
||||||
|
vstTi++;
|
||||||
const chain = activeVstChain(t.vstFxChain || []);
|
const chain = activeVstChain(t.vstFxChain || []);
|
||||||
const tctx = new OfflineAudioContext(2, Math.ceil(targetRate * Math.max(0.1, durationLimit)), targetRate);
|
const tctx = new OfflineAudioContext(2, Math.ceil(targetRate * Math.max(0.1, durationLimit)), targetRate);
|
||||||
const tNodes = {};
|
const tNodes = {};
|
||||||
@@ -28626,6 +28642,7 @@ const App = () => {
|
|||||||
});
|
});
|
||||||
renderedBuffer = await offlineCtx.startRendering();
|
renderedBuffer = await offlineCtx.startRendering();
|
||||||
}
|
}
|
||||||
|
setExportProgress(60);
|
||||||
const numExportCh = renderedBuffer.numberOfChannels;
|
const numExportCh = renderedBuffer.numberOfChannels;
|
||||||
const exportLength = renderedBuffer.length;
|
const exportLength = renderedBuffer.length;
|
||||||
const bytesPerSample = bitDepth / 8;
|
const bytesPerSample = bitDepth / 8;
|
||||||
@@ -28653,6 +28670,7 @@ const App = () => {
|
|||||||
view.setUint32(40, exportLength * bytesPerSample * numExportCh, true);
|
view.setUint32(40, exportLength * bytesPerSample * numExportCh, true);
|
||||||
let offset = 44;
|
let offset = 44;
|
||||||
for (let i = 0; i < exportLength; i++) {
|
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++) {
|
for (let ch = 0; ch < numExportCh; ch++) {
|
||||||
const chData = renderedBuffer.getChannelData(ch);
|
const chData = renderedBuffer.getChannelData(ch);
|
||||||
const sample = Math.max(-1, Math.min(1, chData[i]));
|
const sample = Math.max(-1, Math.min(1, chData[i]));
|
||||||
@@ -28675,8 +28693,10 @@ const App = () => {
|
|||||||
type: 'audio/wav'
|
type: 'audio/wav'
|
||||||
});
|
});
|
||||||
const wavName = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
|
const wavName = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
|
||||||
|
setExportProgress(95);
|
||||||
if ((exportSettings.format === 'mp3' || exportSettings.format === 'ogg') && serverStatus === 'connected') {
|
if ((exportSettings.format === 'mp3' || exportSettings.format === 'ogg') && serverStatus === 'connected') {
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
|
setExportProgress(-1);
|
||||||
showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...", "info");
|
showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...", "info");
|
||||||
try {
|
try {
|
||||||
const file = new File([blob], `session_export.wav`, { type: 'audio/wav' });
|
const file = new File([blob], `session_export.wav`, { type: 'audio/wav' });
|
||||||
@@ -28765,6 +28785,7 @@ const App = () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast("Lỗi xuất âm thanh: " + err.message, "error");
|
showToast("Lỗi xuất âm thanh: " + err.message, "error");
|
||||||
} finally {
|
} finally {
|
||||||
|
setExportProgress(-1);
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -34563,6 +34584,7 @@ STRICT CONSTRAINTS:
|
|||||||
exportSettings: exportSettings,
|
exportSettings: exportSettings,
|
||||||
setExportSettings: setExportSettings,
|
setExportSettings: setExportSettings,
|
||||||
isExporting: isExporting,
|
isExporting: isExporting,
|
||||||
|
exportProgress: exportProgress,
|
||||||
onExport: () => exportSettings.realtime ? triggerBounceExport() : triggerWavExport()
|
onExport: () => exportSettings.realtime ? triggerBounceExport() : triggerWavExport()
|
||||||
}), instrumentSelectorTrackId && /*#__PURE__*/React.createElement("div", {
|
}), instrumentSelectorTrackId && /*#__PURE__*/React.createElement("div", {
|
||||||
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
|
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
@@ -51,7 +51,7 @@
|
|||||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
<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/undoRedoEngine.js?v=202607290941"></script>
|
||||||
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
|
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
|
||||||
<script src="/static/js/app.precompiled.js?v=202608232330" defer></script>
|
<script src="/static/js/app.precompiled.js?v=202608232345" defer></script>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
Reference in New Issue
Block a user