master strip: L/R volume + LINK + MUTE that, bo soloBtn

This commit is contained in:
2026-08-22 21:41:54 +07:00
parent 4ae2f06c57
commit 2a5e2af04a
5 changed files with 290 additions and 74 deletions
+167 -46
View File
@@ -1069,10 +1069,27 @@ function initMasterBus(ctx) {
rightAnalyser.fftSize = 2048;
const splitter = ctx.createChannelSplitter(2);
output.connect(splitter);
splitter.connect(leftAnalyser, 0);
splitter.connect(rightAnalyser, 1);
// Master L/R volume + MUTE stage (spec §2: L/R fader + LINK + MUTE master).
// output -> volSplit -> volLGain/volRGain -> volMerge -> muteGain -> analyser;
// meters (splitter) lay sau muteGain -> MUTE that (VU ve 0).
const volSplit = ctx.createChannelSplitter(2);
const volLGain = ctx.createGain();
volLGain.gain.value = 1.0;
const volRGain = ctx.createGain();
volRGain.gain.value = 1.0;
const volMerge = ctx.createChannelMerger(2);
const muteGain = ctx.createGain();
muteGain.gain.value = 1.0;
output.connect(volSplit);
volSplit.connect(volLGain, 0);
volSplit.connect(volRGain, 1);
volLGain.connect(volMerge, 0, 0);
volRGain.connect(volMerge, 0, 1);
volMerge.connect(muteGain);
masterBus = {
input: ctx.createGain(),
compressor: ctx.createDynamicsCompressor(),
@@ -1082,6 +1099,11 @@ function initMasterBus(ctx) {
dryInput,
dryOutput,
splitter,
volSplit,
volLGain,
volRGain,
volMerge,
muteGain,
// Analysers for metering
inputAnalyser,
@@ -1120,7 +1142,8 @@ function initMasterBus(ctx) {
masterBus.input.connect(masterBus.inputAnalyser);
masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
masterBus.outputAnalyser.connect(masterBus.output);
masterBus.output.connect(masterBus.analyser);
masterBus.muteGain.connect(masterBus.analyser);
masterBus.muteGain.connect(masterBus.splitter);
masterBus.analyser.connect(ctx.destination);
window.masterBus = masterBus;
@@ -1146,6 +1169,25 @@ function setMasterVolume(linear) {
if (masterBus) masterBus.output.gain.setValueAtTime(linear, audioCtx.currentTime);
}
// Master L/R faders + LINK + MUTE - per-channel gain stage (output -> volSplit ->
// volLGain/volRGain -> volMerge -> muteGain -> analyser). time===undefined ->
// live (smoothing); co time -> offline (setValueAtTime). -50dB ~ -inf.
function applyMasterVolumesTo(bus, volL, volR, muted, time) {
if (!bus || !bus.volLGain || !bus.volRGain || !bus.muteGain) return;
var gl = volL <= -50 ? 0 : Math.pow(10, volL / 20);
var gr = volR <= -50 ? 0 : Math.pow(10, volR / 20);
if (time === undefined) {
var now = audioCtx.currentTime;
bus.volLGain.gain.setTargetAtTime(gl, now, 0.01);
bus.volRGain.gain.setTargetAtTime(gr, now, 0.01);
bus.muteGain.gain.setTargetAtTime(muted ? 0 : 1, now, 0.01);
} else {
bus.volLGain.gain.setValueAtTime(gl, time);
bus.volRGain.gain.setValueAtTime(gr, time);
bus.muteGain.gain.setValueAtTime(muted ? 0 : 1, time);
}
}
let _lastMasteringActive = null;
let _lastMasteringSig = null;
let _lastChainSig = null;
@@ -2331,9 +2373,8 @@ const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
};
// ── Master Strip Console Component (from md/47_MASTER_STRIP_CONSOLE.md) ──
const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal, setShowMasteringModal, masteringSettings, setMasteringSettings, isPlaying }) => {
const MasterStripConsole = ({ masterVolume, setMasterVolume, masterVolL, setMasterVolL, masterVolR, setMasterVolR, masterLink, setMasterLink, masterMuted, setMasterMuted, showMasteringModal, setShowMasteringModal, masteringSettings, setMasteringSettings, isPlaying }) => {
const [isTestPlaying, setIsTestPlaying] = React.useState(false);
const [isMuted, setIsMuted] = React.useState(false);
const [isMono, setIsMono] = React.useState(false);
const [pan, setPan] = React.useState(0.0);
const [panText, setPanText] = React.useState('center');
@@ -2352,14 +2393,27 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
getAudioContext();
};
const handleFaderChange = (val) => {
setMasterVolume(val);
const handleFaderChangeL = (val) => {
val = Math.max(-60, Math.min(12, val));
ensureAudio();
if (masterBus && masterBus.output) {
const linear = val <= -50 ? 0 : Math.pow(10, val / 20);
masterBus.output.gain.setTargetAtTime(linear, audioCtx.currentTime, 0.01);
}
setMasterVolL(val);
if (masterLink) setMasterVolR(Math.max(-60, Math.min(12, val + (masterVolR - masterVolL))));
};
const handleFaderChangeR = (val) => {
val = Math.max(-60, Math.min(12, val));
ensureAudio();
setMasterVolR(val);
if (masterLink) setMasterVolL(Math.max(-60, Math.min(12, val - (masterVolR - masterVolL))));
};
// Double-click reset: ca 2 ve 0
const handleFaderChange = (val) => { handleFaderChangeL(val); handleFaderChangeR(val); };
React.useEffect(() => {
applyMasterVolumesTo(masterBus, masterVolL, masterVolR, masterMuted);
}, [masterVolL, masterVolR, masterMuted]);
const masterVolDisplay = masterLink
? (masterVolL <= -50 ? '-inf' : (masterVolL > 0 ? '+' : '') + masterVolL.toFixed(1))
: 'L' + (masterVolL <= -50 ? '-inf' : masterVolL.toFixed(1)) + ' R' + (masterVolR <= -50 ? '-inf' : masterVolR.toFixed(1));
const handlePanPointerDown = (e) => {
isPanDraggingRef.current = true;
@@ -2444,7 +2498,7 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
return () => {
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
};
}, [masterVolume, isMuted, isMono]);
}, [masterVolL, masterVolR, masterMuted, isMono]);
return React.createElement("div", {
className: "flex flex-col items-stretch w-[300px] shrink-0 strip-bg rounded-lg p-2 text-slate-300 select-none shadow-2xl relative overflow-hidden"
@@ -2496,7 +2550,7 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
pan > 0 ? 'R' + Math.round(pan * 100) : ''
),
React.createElement("span", { ref: React.createRef ? null : null, className: "text-[10px] font-bold font-mono text-slate-200 ml-8", onDoubleClick: function() { handleFaderChange(0); } },
masterVolume <= -50 ? '-inf' : (masterVolume > 0 ? '+' : '') + masterVolume.toFixed(1)
masterVolDisplay
)
)
),
@@ -2525,28 +2579,28 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
React.createElement("span", { ref: peakRRef }, "-inf")
)
),
React.createElement("div", { className: "w-16 flex items-stretch gap-1 bg-slate-900/60 p-1 rounded border border-slate-800" },
React.createElement("div", { className: "w-36 flex items-stretch gap-0.5 bg-slate-900/60 p-1 rounded border border-slate-800" },
React.createElement("div", {
className: "flex-1 flex flex-col items-center justify-center relative fader-track rounded",
onWheel: function(e) {
e.preventDefault();
var delta = e.deltaY > 0 ? -0.5 : 0.5;
handleFaderChange(Math.max(-60, Math.min(12, masterVolume + delta)));
handleFaderChangeL(masterVolL + (e.deltaY > 0 ? -0.5 : 0.5));
}
},
React.createElement("div", { className: "w-0.5 h-full bg-slate-700 absolute left-1/2 -translate-x-1/2 top-0" }),
React.createElement("input", {
id: "masterFader",
id: "masterFaderL",
type: "range",
min: "-60",
max: "12",
step: "0.5",
value: masterVolume,
value: masterVolL,
className: "fader-slider w-full z-10",
orient: "vertical",
onChange: function(e) { handleFaderChange(parseFloat(e.target.value)); },
onDoubleClick: function() { handleFaderChange(0); }
})
onChange: function(e) { handleFaderChangeL(parseFloat(e.target.value)); },
onDoubleClick: function() { handleFaderChangeL(0); }
}),
React.createElement("span", { className: "text-[7px] font-mono text-slate-500 leading-none mt-0.5" }, "L")
),
React.createElement("div", { className: "relative w-5 text-[7px] font-mono text-slate-500 select-none overflow-hidden" },
React.createElement("span", { className: "absolute", style: {top: '0%', right: '2px'} }, "+12"),
@@ -2557,8 +2611,30 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
React.createElement("span", { className: "absolute", style: {top: '50%', right: '2px'} }, "-24"),
React.createElement("span", { className: "absolute", style: {top: '66.7%', right: '2px'} }, "-36"),
React.createElement("span", { className: "absolute", style: {top: '91.7%', right: '2px'} }, "-54")
),
React.createElement("div", {
className: "flex-1 flex flex-col items-center justify-center relative fader-track rounded",
onWheel: function(e) {
e.preventDefault();
handleFaderChangeR(masterVolR + (e.deltaY > 0 ? -0.5 : 0.5));
}
},
React.createElement("div", { className: "w-0.5 h-full bg-slate-700 absolute left-1/2 -translate-x-1/2 top-0" }),
React.createElement("input", {
id: "masterFaderR",
type: "range",
min: "-60",
max: "12",
step: "0.5",
value: masterVolR,
className: "fader-slider w-full z-10",
orient: "vertical",
onChange: function(e) { handleFaderChangeR(parseFloat(e.target.value)); },
onDoubleClick: function() { handleFaderChangeR(0); }
}),
React.createElement("span", { className: "text-[7px] font-mono text-slate-500 leading-none mt-0.5" }, "R")
)
),
),
React.createElement("div", { className: "w-8 flex flex-col justify-between text-[10px] font-bold" },
React.createElement("button", {
id: "monoBtn",
@@ -2571,15 +2647,16 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
),
React.createElement("button", {
id: "muteBtn",
onClick: function() { setIsMuted(function(p) { return !p; }); },
className: "btn-daw h-[18px] rounded text-amber-500 font-bold hover:text-amber-400" + (isMuted ? " btn-mute-active" : ""),
title: "Mute Master Output"
onClick: function() { setMasterMuted(function(p) { return !p; }); },
className: "btn-daw h-[18px] rounded text-amber-500 font-bold hover:text-amber-400" + (masterMuted ? " btn-mute-active" : ""),
title: "Mute Master Output (that: zero output)"
}, "M"),
React.createElement("button", {
id: "soloBtn",
className: "btn-daw h-[18px] rounded text-yellow-400 font-bold hover:text-yellow-300",
title: "Solo Master"
}, "S"),
id: "linkBtn",
onClick: function() { setMasterLink(function(p) { return !p; }); },
className: "btn-daw h-[18px] rounded text-xs font-bold" + (masterLink ? " btn-teal-active" : ""),
title: "LINK: keo L/R cung luc (giu delta)"
}, "LK"),
React.createElement("button", { className: "btn-daw h-[18px] rounded text-slate-400 hover:text-slate-200", title: "Route Matrix" },
React.createElement("i", { className: "fa-solid fa-diagram-project text-[9px]" })
),
@@ -2606,7 +2683,7 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
)
),
React.createElement("div", { className: "text-center text-[10px] font-bold font-mono text-slate-200 shrink-0" },
masterVolume <= -50 ? '-inf' : (masterVolume > 0 ? '+' : '') + masterVolume.toFixed(1)
masterVolDisplay
),
React.createElement("div", { className: "shrink-0 border-t border-slate-800 flex flex-col items-center" },
React.createElement("div", { className: "flex justify-between w-full text-[9px] font-mono py-0.5" },
@@ -11196,7 +11273,7 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore, visite
});
};
const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsList, sessionTabsList, masteringSettings, masterVolume) => {
const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsList, sessionTabsList, masteringSettings, masterVolume, masterState) => {
const secondsPerBar = (60.0 / parseFloat(bpmVal || 120)) * 4;
const mainTracks = serializeTracksList(tracksList, secondsPerBar);
@@ -11303,11 +11380,15 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
})(),
auto_compute_length: true,
tracks: mainTracks,
...((masteringSettings && Array.isArray(masteringSettings.vstFxChain) && masteringSettings.vstFxChain.length > 0) || (typeof masterVolume === 'number' && isFinite(masterVolume) && masterVolume !== 0) ? {
...((masteringSettings && Array.isArray(masteringSettings.vstFxChain) && masteringSettings.vstFxChain.length > 0) || (typeof masterVolume === 'number' && isFinite(masterVolume) && masterVolume !== 0) || (masterState && (masterState.volL !== 0 || masterState.volR !== 0 || masterState.muted || !masterState.link)) ? {
master: {
fx_chain: masteringSettings.vstFxChain.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '', bypass: !!s.bypass })),
volume_db: (typeof masterVolume === 'number' && isFinite(masterVolume)) ? masterVolume : 0,
mute: false,
volumeL_db: (masterState && typeof masterState.volL === 'number' && isFinite(masterState.volL)) ? masterState.volL : ((typeof masterVolume === 'number' && isFinite(masterVolume)) ? masterVolume : 0),
volumeR_db: (masterState && typeof masterState.volR === 'number' && isFinite(masterState.volR)) ? masterState.volR : ((typeof masterVolume === 'number' && isFinite(masterVolume)) ? masterVolume : 0),
link_lr: masterState ? !!masterState.link : false,
muted: masterState ? !!masterState.muted : false,
mute: masterState ? !!masterState.muted : false,
bypass: !masteringSettings.masterConnected || !!masteringSettings.vstBypass
}
} : {})
@@ -11429,6 +11510,10 @@ const deserializeProjectFromSchema = (schemaObj) => {
};
})(),
masterVolume: (typeof _masterBus.volume_db === 'number' && isFinite(_masterBus.volume_db)) ? _masterBus.volume_db : 0,
masterVolL: (typeof _masterBus.volumeL_db === 'number' && isFinite(_masterBus.volumeL_db)) ? _masterBus.volumeL_db : ((typeof _masterBus.volume_db === 'number' && isFinite(_masterBus.volume_db)) ? _masterBus.volume_db : 0),
masterVolR: (typeof _masterBus.volumeR_db === 'number' && isFinite(_masterBus.volumeR_db)) ? _masterBus.volumeR_db : ((typeof _masterBus.volumeL_db === 'number' && isFinite(_masterBus.volumeL_db)) ? _masterBus.volumeL_db : ((typeof _masterBus.volume_db === 'number' && isFinite(_masterBus.volume_db)) ? _masterBus.volume_db : 0)),
masterLink: !!_masterBus.link_lr,
masterMuted: !!(_masterBus.muted || _masterBus.mute),
zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0
};
};
@@ -17560,7 +17645,11 @@ const App = () => {
var saved = localStorage.getItem('studio_media_explorer_height');
return saved ? parseInt(saved) : 240;
}());
const [masterVolume, setMasterVolume] = useState(0); // dB
const [masterVolume, setMasterVolume] = useState(0); // dB (legacy single fader)
const [masterVolL, setMasterVolL] = useState(0); // dB - master L fader
const [masterVolR, setMasterVolR] = useState(0); // dB - master R fader
const [masterLink, setMasterLink] = useState(true); // LINK L/R
const [masterMuted, setMasterMuted] = useState(false); // MUTE master
const [masterVU, setMasterVU] = useState(0); // 0-1
const [masterMeterPeak, setMasterMeterPeak] = useState(0);
const [rightSidebarWidth, setRightSidebarWidth] = useState(320);
@@ -18457,7 +18546,7 @@ const App = () => {
// Render offline toàn project qua native_bridge (VST FX masterbus + track) → WAV.
// Dùng cho nút "Nghe thử" trong FX Rack / Mastering — preview = âm export.
window.__renderProjectPreview = async () => {
const schema = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
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 });
try {
const res = await window.SonicAPI.renderProject(schema, 'preview_' + Date.now() + '.wav');
return res;
@@ -18747,6 +18836,10 @@ const App = () => {
if (result.zoom > 1 && setZoom) setZoom(result.zoom);
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
if (result.masterVolL !== undefined && result.masterVolL !== null) setMasterVolL(result.masterVolL);
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
} else {
restoredTracks = (parsed.tracks || []).map(function(t) {
var rest = Object.assign({}, t); delete rest.height;
@@ -18816,6 +18909,10 @@ const App = () => {
if (result.subTabs && result.subTabs.length > 0) setSubTabs(result.subTabs);
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
if (result.masterVolL !== undefined && result.masterVolL !== null) setMasterVolL(result.masterVolL);
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
setProjectName(tmpName);
localStorage.setItem('sonic_project_name', tmpName);
@@ -18921,6 +19018,10 @@ const App = () => {
});
}
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
if (result.masterVolL !== undefined && result.masterVolL !== null) setMasterVolL(result.masterVolL);
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
} else {
restoredTracks = (proj.tracks || []).map(t => {
const { height: _h, ...rest } = t;
@@ -19046,7 +19147,7 @@ const App = () => {
} : null
}));
window.SonicStorage.scheduleTempAutoSave(() => {
return serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
return 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 });
});
}, [tracks, subTabs, sessionTabs, masteringSettings, masterVolume]);
@@ -19058,7 +19159,7 @@ const App = () => {
useEffect(() => {
const buildTempPayload = () => {
try {
const schemaObj = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
const schemaObj = 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 cid = (window.SonicStorage && window.SonicStorage.getClientId) ? window.SonicStorage.getClientId() : '';
return JSON.stringify({ data_json: JSON.stringify(schemaObj), client_id: cid });
} catch (e) { return null; }
@@ -19136,6 +19237,10 @@ const App = () => {
// không được revert chain đang sửa (ví dụ vừa xóa VST).
if (result.masteringSettings && !masteringModalOpenRef.current) setMasteringSettings(result.masteringSettings);
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
if (result.masterVolL !== undefined && result.masterVolL !== null) setMasterVolL(result.masterVolL);
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
setProjectName(tmpName);
localStorage.setItem('sonic_project_name', tmpName);
@@ -19166,14 +19271,14 @@ const App = () => {
if (currentProjectId.startsWith('local_')) {
const finalName = projectName || 'Dự án mới';
const localId = currentProjectId;
const schemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
const schemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted });
const dataStr = JSON.stringify(schemaObj);
localStorage.setItem('sonic_local_project_data', dataStr);
localStorage.setItem('sonic_project_id', localId);
localStorage.setItem('sonic_project_name', finalName);
} else if (currentUser && window.SonicAPI) {
try {
const schemaObj = serializeProjectToSchema(currentProjectId, projectName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
const schemaObj = serializeProjectToSchema(currentProjectId, projectName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted });
const dataJson = JSON.stringify(schemaObj);
window.SonicAPI.updateCloudProject(currentProjectId, projectName || 'Dự án mới', dataJson).catch(() => {});
} catch (e) {
@@ -26288,7 +26393,7 @@ const App = () => {
try {
if (window.SonicStorage) {
const nextTracks = (tracks || []).map(t => t.id === trackId ? { ...t, color } : t);
window.SonicStorage.scheduleTempAutoSave(() => serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, nextTracks, subTabs, sessionTabs, masteringSettings, masterVolume));
window.SonicStorage.scheduleTempAutoSave(() => serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, nextTracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted }));
if (window.SonicStorage.flushTempAutoSave) window.SonicStorage.flushTempAutoSave();
}
} catch (e) {}
@@ -27282,7 +27387,7 @@ const App = () => {
const handleSaveLocalProject = (name) => {
const finalName = name || projectName || 'Dự án mới';
const localId = currentProjectId && currentProjectId.startsWith('local_') ? currentProjectId : 'local_' + Date.now();
const projectSchemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
const projectSchemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted });
const dataStr = JSON.stringify(projectSchemaObj);
localStorage.setItem('sonic_local_project_data', dataStr);
localStorage.setItem('sonic_project_id', localId);
@@ -27305,7 +27410,7 @@ const App = () => {
const handleSaveCloudProject = async (name, existingProjectId) => {
const finalName = name || projectName || 'Dự án mới';
var useProjectId = existingProjectId || currentProjectId;
const projectSchemaObj = serializeProjectToSchema(useProjectId || 'project_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
const projectSchemaObj = serializeProjectToSchema(useProjectId || 'project_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted });
// Diagnostic: count source vs serialized items
var srcMidi = 0, srcClip = 0, srcSec = 0;
tracks.forEach(function(t) { srcMidi += (t.midiItems||[]).length; srcClip += (t.clips||[]).length; srcSec += (t.sections||[]).length; });
@@ -27368,7 +27473,7 @@ const App = () => {
const handleGlobalSave = async () => {
try {
const finalName = projectName || 'Dự án mới';
const schemaObj = serializeProjectToSchema(currentProjectId || 'proj_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
const schemaObj = serializeProjectToSchema(currentProjectId || 'proj_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted });
const dataJson = JSON.stringify(schemaObj);
const runtime = (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.runtime) || '';
if (runtime === 'desktop' && window.SonicAPI && window.SonicAPI.saveProjectToDisk) {
@@ -27406,7 +27511,7 @@ const App = () => {
}, []);
const handleSaveAsCloud = async (newName) => {
const projectSchemaObj = serializeProjectToSchema('project_' + Date.now(), newName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
const projectSchemaObj = serializeProjectToSchema('project_' + Date.now(), newName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted });
const dataJson = JSON.stringify(projectSchemaObj);
try {
const res = await window.SonicAPI.saveCloudProject(newName, dataJson);
@@ -27426,7 +27531,7 @@ const App = () => {
const handleSaveCloud = handleSaveProject;
const handleExportSFS = (customName = null) => {
const finalName = customName || projectName || 'Dự án SonicForge';
const projectSchemaObj = serializeProjectToSchema(currentProjectId || 'proj_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
const projectSchemaObj = serializeProjectToSchema(currentProjectId || 'proj_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted });
window.SonicStorage.exportProjectToSFS(projectSchemaObj);
showToast("Đã xuất dự án (.sfs) thành công!", "success");
};
@@ -27450,6 +27555,10 @@ const App = () => {
restoredSubTabs = result.subTabs;
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
if (result.masterVolL !== undefined && result.masterVolL !== null) setMasterVolL(result.masterVolL);
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
} else {
restored = (proj.tracks || []).map(t => ({
...t,
@@ -27584,7 +27693,7 @@ const App = () => {
try {
if (masterBus && masterBus.output) {
const mv = (typeof masterVolume === 'number' && isFinite(masterVolume)) ? masterVolume : 0;
masterBus.output.gain.setValueAtTime(mv <= -50 ? 0 : Math.pow(10, mv / 20), 0);
applyMasterVolumesTo(masterBus, masterVolL, masterLink ? masterVolL : masterVolR, !!masterMuted, 0);
}
} catch (e) { console.warn('offline main-out volume error:', e); }
let renderedBuffer;
@@ -27616,7 +27725,7 @@ const App = () => {
try {
if (masterBus && masterBus.output) {
const mv = (typeof masterVolume === 'number' && isFinite(masterVolume)) ? masterVolume : 0;
masterBus.output.gain.setValueAtTime(mv <= -50 ? 0 : Math.pow(10, mv / 20), 0);
applyMasterVolumesTo(masterBus, masterVolL, masterLink ? masterVolL : masterVolR, !!masterMuted, 0);
}
} catch (e) { console.warn('offline main-out volume error (vst):', e); }
const src = mctx.createBufferSource();
@@ -30060,6 +30169,10 @@ STRICT CONSTRAINTS:
if (result.zoom > 1 && setZoom) setZoom(result.zoom);
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
if (result.masterVolL !== undefined && result.masterVolL !== null) setMasterVolL(result.masterVolL);
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
} else {
restoredTracks = (parsed.tracks || []).map(function (t) {
const rest = Object.assign({}, t); delete rest.height;
@@ -32892,6 +33005,14 @@ STRICT CONSTRAINTS:
}, /*#__PURE__*/React.createElement(MasterStripConsole, {
masterVolume: masterVolume,
setMasterVolume: setMasterVolume,
masterVolL: masterVolL,
setMasterVolL: setMasterVolL,
masterVolR: masterVolR,
setMasterVolR: setMasterVolR,
masterLink: masterLink,
setMasterLink: setMasterLink,
masterMuted: masterMuted,
setMasterMuted: setMasterMuted,
showMasteringModal: showMasteringModal,
setShowMasteringModal: setShowMasteringModal,
masteringSettings: masteringSettings,
File diff suppressed because one or more lines are too long