master strip: L/R volume + LINK + MUTE that, bo soloBtn
This commit is contained in:
@@ -829,10 +829,25 @@ class PythonRenderEngine:
|
||||
else:
|
||||
logger.warning("[RenderEngine] Masterbus FX cần RENDER_ENGINE=bridge — bỏ qua")
|
||||
|
||||
# Master bus volume (spec §2 Step 4: master gain stage after chain)
|
||||
master_vol_db = master.get("volume_db", 0.0)
|
||||
if master_vol_db:
|
||||
master_buffer *= 10 ** (master_vol_db / 20.0)
|
||||
# Master bus volume (spec §2 Step 4: master gain stage after chain).
|
||||
# Per-channel L/R faders: volumeL_db/volumeR_db (link_lr -> R theo L);
|
||||
# MUTE zeroes output; fallback volume_db cho project cu.
|
||||
if master.get("muted") or master.get("mute"):
|
||||
master_buffer *= 0.0
|
||||
else:
|
||||
vol_l = master.get("volumeL_db", master.get("volume_db", 0.0))
|
||||
if master.get("link_lr"):
|
||||
vol_r = vol_l
|
||||
else:
|
||||
vol_r = master.get("volumeR_db", vol_l)
|
||||
if master_buffer.shape[0] >= 2:
|
||||
if vol_l:
|
||||
master_buffer[0] *= 10 ** (vol_l / 20.0)
|
||||
if vol_r:
|
||||
master_buffer[1] *= 10 ** (vol_r / 20.0)
|
||||
else:
|
||||
if vol_l:
|
||||
master_buffer *= 10 ** (vol_l / 20.0)
|
||||
|
||||
# Normalization to prevent clipping
|
||||
max_peak = np.max(np.abs(master_buffer))
|
||||
|
||||
@@ -35,6 +35,23 @@
|
||||
"tracks": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/Track" }
|
||||
},
|
||||
"master": {
|
||||
"type": "object",
|
||||
"description": "Master bus strip: per-channel L/R faders + LINK + MUTE",
|
||||
"properties": {
|
||||
"volume_db": { "type": "number", "default": 0.0, "description": "Legacy single fader (fallback khi khong co volumeL_db)" },
|
||||
"volumeL_db": { "type": "number", "default": 0.0 },
|
||||
"volumeR_db": { "type": "number", "default": 0.0 },
|
||||
"link_lr": { "type": "boolean", "default": false, "description": "LINK: keo L/R cung luc (R theo L)" },
|
||||
"muted": { "type": "boolean", "default": false, "description": "MUTE master: zero output" },
|
||||
"mute": { "type": "boolean", "default": false },
|
||||
"bypass": { "type": "boolean", "default": false },
|
||||
"fx_chain": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/FXPlugin" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "is_root", "tracks"]
|
||||
|
||||
+167
-46
@@ -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
@@ -303,6 +303,58 @@ def test_55_fader_after_master_chain(monkeypatch, tmp_path, fx_bridge):
|
||||
assert 0.4 < p6 / p0 < 0.6, f"fader -6dB phải ~1/2 đỉnh: {p6:.4f}/{p0:.4f}"
|
||||
assert 0.8 < p6 / base < 1.2, f"fader-6 phải ≈ base: {p6:.4f} vs {base:.4f}"
|
||||
|
||||
# ── 5.8 Master L/R volume + LINK + MUTE ─────────────────────────────────
|
||||
def test_58_master_lr_link_mute(monkeypatch, tmp_path, fx_bridge):
|
||||
"""Master strip L/R: volumeL_db/volumeR_db tách kênh, link_lr → R theo L,
|
||||
muted → zero output. Fallback volume_db (project cũ) vẫn chạy."""
|
||||
monkeypatch.setenv("SF_FX_BRIDGE_PATH", fx_bridge)
|
||||
if not os.path.isfile(DAW_BRIDGE):
|
||||
pytest.skip("daw_vst_bridge.exe chưa build")
|
||||
monkeypatch.setenv("SF_BRIDGE_PATH", DAW_BRIDGE)
|
||||
from app.api.v1 import plugins as pl
|
||||
monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "bridge")
|
||||
from app.core.render_engine import PythonRenderEngine
|
||||
engine = PythonRenderEngine()
|
||||
|
||||
def project(master):
|
||||
track = _midi_track()
|
||||
track["volume_db"] = -20.0 # tránh normalize (>1.0)
|
||||
return {
|
||||
"metadata": {"bpm": 120.0, "time_signature_numerator": 4},
|
||||
"main_session": {
|
||||
"length_bars": 1.0,
|
||||
"tracks": [track],
|
||||
"master": dict({"fx_chain": [], "bypass": False}, **master),
|
||||
},
|
||||
"section_store": {},
|
||||
}
|
||||
|
||||
def render(name, master):
|
||||
out = str(tmp_path / name)
|
||||
engine.render_project(project(master), out)
|
||||
data, _ = _read_wav(out)
|
||||
return data
|
||||
|
||||
base = render("base.wav", {"volumeL_db": 0.0, "volumeR_db": 0.0, "link_lr": True})
|
||||
baseL = float(np.max(np.abs(base[0])))
|
||||
baseR = float(np.max(np.abs(base[1])))
|
||||
assert baseL > 0
|
||||
|
||||
lr = render("lr.wav", {"volumeL_db": -6.0, "volumeR_db": 0.0, "link_lr": False})
|
||||
assert 0.4 < float(np.max(np.abs(lr[0]))) / baseL < 0.6, f"L -6dB phải ~1/2 đỉnh: {float(np.max(np.abs(lr[0]))):.4f}/{baseL:.4f}"
|
||||
assert abs(float(np.max(np.abs(lr[1]))) - baseR) / baseR < 0.15, "R phải giữ nguyên khi link_lr=false"
|
||||
|
||||
link = render("link.wav", {"volumeL_db": -6.0, "volumeR_db": -3.0, "link_lr": True})
|
||||
ratio = float(np.max(np.abs(link[1]))) / float(np.max(np.abs(link[0])))
|
||||
assert 0.9 < ratio < 1.1, f"link_lr: R phải theo L ({ratio:.3f})"
|
||||
|
||||
muted = render("muted.wav", {"volumeL_db": 0.0, "volumeR_db": 0.0,
|
||||
"link_lr": True, "muted": True})
|
||||
assert float(np.max(np.abs(muted))) < 1e-6, "MUTE master phải zero output"
|
||||
|
||||
legacy = render("legacy.wav", {"volume_db": -6.0})
|
||||
assert 0.4 < float(np.max(np.abs(legacy[0]))) / baseL < 0.6 and 0.4 < float(np.max(np.abs(legacy[1]))) / baseR < 0.6, "fallback volume_db (project cũ) phải áp cả 2 kênh"
|
||||
|
||||
|
||||
# ── 5.6 PDC ───────────────────────────────────────────────────────────────
|
||||
def test_56_pdc_aligns_tracks(monkeypatch, tmp_path, fx_bridge):
|
||||
|
||||
Reference in New Issue
Block a user