D3+D4: export stage PCM16/24 + TPDF dither + noise shaping, resample 44.1/48/96k, normalize -1dBTP/-14LUFS, mono side/mid check; Export modal mastering options + /plugins/render options (test 9 case, 23 passed)

This commit is contained in:
2026-08-23 07:47:27 +07:00
parent 35076f680b
commit 5d37c0683b
8 changed files with 372 additions and 16 deletions
+85 -6
View File
@@ -11971,7 +11971,7 @@ const InteractiveEqPro = ({ track, params, onChange, getModule, applyTo, spectru
);
};
const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExporting, onExport, onBounce }) => {
const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExporting, onExport, onBounce, onRenderMastering }) => {
if (!open) return null;
return (
<div className="fixed inset-0 z-[300] bg-black/70 backdrop-blur-sm flex items-center justify-center p-6" onClick={onClose}>
@@ -12011,15 +12011,15 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">SR (Hz)</label>
<select value={exportSettings.sampleRate} onChange={e => setExportSettings(p => ({ ...p, sampleRate: e.target.value }))}
className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
<option value="22500">22500</option>
<option value="44100">44100</option>
<option value="48000">48000</option>
<option value="96000">96000</option>
</select>
</div>
<div>
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Bit</label>
<select value={exportSettings.bitDepth} onChange={e => setExportSettings(p => ({ ...p, bitDepth: e.target.value }))}
className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
<option value="8">8</option>
<option value="16">16</option>
<option value="24">24</option>
<option value="float">32 float</option>
@@ -12050,6 +12050,34 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
</div>
<div></div>
</div>
<div className="grid grid-cols-2 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.dither}
onChange={e => setExportSettings(p => ({ ...p, dither: e.target.checked }))}
className="accent-cyan-500 w-3.5 h-3.5" />
<span className="text-xs text-zinc-300">Dither TPDF (PCM 16/24)</span>
</label>
<label className="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" checked={!!exportSettings.normalize}
onChange={e => setExportSettings(p => ({ ...p, normalize: e.target.checked }))}
className="accent-cyan-500 w-3.5 h-3.5" />
<span className="text-xs text-zinc-300">Normalize</span>
</label>
<label className="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" checked={!!exportSettings.monoCheck}
onChange={e => setExportSettings(p => ({ ...p, monoCheck: e.target.checked }))}
className="accent-cyan-500 w-3.5 h-3.5" />
<span className="text-xs text-zinc-300">Cảnh báo mono (side/mid)</span>
</label>
<div>
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Normalize target</label>
<select value={exportSettings.normalizeTarget} onChange={e => setExportSettings(p => ({ ...p, normalizeTarget: e.target.value }))}
className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
<option value="-1dbtp">-1 dBTP</option>
<option value="-14lufs">-14 LUFS</option>
</select>
</div>
</div>
<div className="flex items-center gap-2 pt-1 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.loop}
@@ -12066,7 +12094,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
</label>
)}
</div>
<div className="grid grid-cols-2 gap-2 pt-1">
<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">
@@ -12076,6 +12104,11 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
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>
</div>
</div>
</div>
@@ -17738,7 +17771,11 @@ const App = () => {
quality: '44khz',
channels: 'stereo',
loop: false,
loopBars: 4
loopBars: 4,
dither: false,
normalize: false,
normalizeTarget: '-1dbtp',
monoCheck: true
});
const [serverStatus, setServerStatus] = useState('checking...');
const _lan = window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.lan;
@@ -27282,6 +27319,47 @@ const App = () => {
}
};
// ── D4: Mastering render qua engine (D3 export stage) ──
// /api/v1/plugins/render — project schema + mastering chain qua native_bridge
// (VST3 masterbus), resample/normalize/dither PCM, mono side/mid check.
const renderMasteringExport = async () => {
if (isExporting) return;
if (serverStatus !== 'connected') { showToast('Cần kết nối server để render mastering.', 'warning'); return; }
setIsExporting(true);
try {
const schema = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted });
const isFloat = exportSettings.bitDepth === 'float';
const res = await window.SonicAPI.renderProject(schema, 'master_' + Date.now() + '.wav', {
bit_depth: isFloat ? 32 : parseInt(exportSettings.bitDepth) || 16,
sample_rate: parseInt(exportSettings.sampleRate) || 44100,
dither: !!exportSettings.dither,
normalize: !!exportSettings.normalize,
normalize_target: exportSettings.normalizeTarget || '-1dbtp',
mono_check: !!exportSettings.monoCheck
});
const lufsTxt = (res.lufs !== null && res.lufs !== undefined) ? res.lufs + ' LUFS' : '-- LUFS';
const tpTxt = (res.true_peak_db !== null && res.true_peak_db !== undefined) ? res.true_peak_db + ' dBTP' : '-- dBTP';
showToast(`Mastering xong — ${lufsTxt} / ${tpTxt}${res.mono_warning ? ' • ' + res.mono_warning : ''}`, 'success', 'Tải về', async () => {
try {
const br = await fetch(res.url);
if (!br.ok) throw new Error('HTTP ' + br.status);
const b = await br.blob();
const sp = await saveExportViaDialog((projectName || 'Project') + '_master.wav', b);
if (sp) showToast(`Đã lưu → ${sp}`, 'success');
} catch (e) {
const a = document.createElement('a');
a.href = res.url;
a.download = 'master.wav';
a.click();
}
});
} catch (e) {
showToast('Render mastering lỗi: ' + (e.message || e), 'error');
} finally {
setIsExporting(false);
}
};
// ── Realtime Bounce Export (đầy đủ MIDI + FX Rack + Mastering) ──
// MIDI/soundfont chạy qua FluidSynth (ScriptProcessor) — không render được
// trong OfflineAudioContext. Vì vậy bounce REALTIME: chạy lại toàn bộ project
@@ -33675,7 +33753,8 @@ STRICT CONSTRAINTS:
setExportSettings: setExportSettings,
isExporting: isExporting,
onExport: triggerWavExport,
onBounce: triggerBounceExport
onBounce: triggerBounceExport,
onRenderMastering: renderMasteringExport
}), 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
@@ -122,7 +122,7 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
deleteBackup: (backupId) => apiRequest(`/api/v1/projects/cloud/backups/${backupId}`, { method: 'DELETE' }),
cleanupBackups: (keep) => apiRequest('/api/v1/projects/cloud/backups/cleanup', { method: 'POST', body: JSON.stringify({ keep }) }),
renderProject: (projectJson, outputFilename) => apiRequest('/api/v1/plugins/render', { method: 'POST', body: JSON.stringify({ project_json: projectJson, output_filename: outputFilename }) }),
renderProject: (projectJson, outputFilename, options) => apiRequest('/api/v1/plugins/render', { method: 'POST', body: JSON.stringify(Object.assign({ project_json: projectJson, output_filename: outputFilename }, options || {})) }),
// Ctrl-S desktop: lưu project ra THƯ MỤC CỦA HỆ ĐIỀU HÀNH
// (Documents/SonicForgeDAW/Projects — Windows; ~/SonicForgeDAW/Projects — Linux)
saveProjectToDisk: (name, dataJson) => apiRequest('/api/v1/projects/save-to-disk', { method: 'POST', body: JSON.stringify({ name, data_json: dataJson }) }),