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:
+85
-6
@@ -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
@@ -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 }) }),
|
||||
|
||||
Reference in New Issue
Block a user