Gap 7: master volume automation lane — render_engine automation.volume_db (dB interp -30..+3 sau fader truoc normalize), schema master.automation, UI AUTOM panel MasterStripConsole + realtime scheduling tren masterBus.output.gain (play/seek/stop), serialize/parse, test_65 (177 passed)
This commit is contained in:
+118
-3
@@ -1178,6 +1178,37 @@ function initMasterBus(ctx) {
|
||||
function setMasterVolume(linear) {
|
||||
if (masterBus) masterBus.output.gain.setValueAtTime(linear, audioCtx.currentTime);
|
||||
}
|
||||
// Gap 7: master volume automation lane — schedule envelope tren master output
|
||||
// gain (dB tuyen tinh, clamp -30..+3 nhu render_engine._apply_master_volume_automation).
|
||||
// Trang thai tu React qua window.__masterAutoOn/__masterAutoVol (pattern
|
||||
// window.__currentZoom). playheadSec = vi tri timeline giay luc bat dau nghe.
|
||||
function applyMasterAutomationSchedule(playheadSec) {
|
||||
const bus = masterBus;
|
||||
if (!bus || !bus.output || !bus.output.gain) return;
|
||||
const ctx = audioCtx || getAudioContext();
|
||||
const now = ctx.currentTime;
|
||||
const env = (window.__masterAutoOn && Array.isArray(window.__masterAutoVol))
|
||||
? window.__masterAutoVol.filter(p => p && typeof p.time === 'number' && typeof p.db === 'number').slice().sort((a, b) => a.time - b.time)
|
||||
: [];
|
||||
bus.output.gain.cancelScheduledValues(now);
|
||||
if (!env.length) { bus.output.gain.setValueAtTime(1, now); return; }
|
||||
const ph = playheadSec || 0;
|
||||
let idx = 0;
|
||||
while (idx < env.length && env[idx].time < ph) idx++;
|
||||
let initDb;
|
||||
if (idx === 0) initDb = env[0].db;
|
||||
else if (idx >= env.length) initDb = env[env.length - 1].db;
|
||||
else {
|
||||
const a = env[idx - 1], b = env[idx];
|
||||
const span = b.time - a.time || 1;
|
||||
initDb = a.db + (b.db - a.db) * Math.max(0, Math.min(1, (ph - a.time) / span));
|
||||
}
|
||||
bus.output.gain.setValueAtTime(Math.pow(10, Math.max(-30, Math.min(3, initDb)) / 20), now);
|
||||
for (let k = idx; k < env.length; k++) {
|
||||
const t = now + (env[k].time - ph);
|
||||
if (t > now) bus.output.gain.linearRampToValueAtTime(Math.pow(10, Math.max(-30, Math.min(3, env[k].db)) / 20), t);
|
||||
}
|
||||
}
|
||||
|
||||
// Master L/R faders + LINK + MUTE - per-channel gain stage (output -> volSplit ->
|
||||
// volLGain/volRGain -> volMerge -> muteGain -> analyser). time===undefined ->
|
||||
@@ -2549,7 +2580,7 @@ const RefCompareModal = ({ open, onClose }) => {
|
||||
};
|
||||
|
||||
// ── Master Strip Console Component (from md/47_MASTER_STRIP_CONSOLE.md) ──
|
||||
const MasterStripConsole = ({ masterVolume, setMasterVolume, masterVolL, setMasterVolL, masterVolR, setMasterVolR, masterLink, setMasterLink, masterMuted, setMasterMuted, showMasteringModal, setShowMasteringModal, masteringSettings, setMasteringSettings, isPlaying }) => {
|
||||
const MasterStripConsole = ({ masterVolume, setMasterVolume, masterVolL, setMasterVolL, masterVolR, setMasterVolR, masterLink, setMasterLink, masterMuted, setMasterMuted, masterAutoVol, setMasterAutoVol, masterAutoOn, setMasterAutoOn, showMasteringModal, setShowMasteringModal, masteringSettings, setMasteringSettings, isPlaying }) => {
|
||||
const [isTestPlaying, setIsTestPlaying] = React.useState(false);
|
||||
const [isMono, setIsMono] = React.useState(false);
|
||||
const [pan, setPan] = React.useState(0.0);
|
||||
@@ -2565,6 +2596,18 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, masterVolL, setMast
|
||||
const panStartYRef = React.useRef(0);
|
||||
const startPanValRef = React.useRef(0);
|
||||
const [refOpen, setRefOpen] = React.useState(false);
|
||||
const [autoOpen, setAutoOpen] = React.useState(false);
|
||||
const addAutoPoint = () => {
|
||||
const pts = (masterAutoVol || []).slice();
|
||||
pts.push({ time: Math.round(pts.reduce((m, p) => Math.max(m, p.time || 0), 0) + 1), db: 0 });
|
||||
setMasterAutoVol(pts);
|
||||
};
|
||||
const updAutoPoint = (i, key, val) => {
|
||||
const pts = (masterAutoVol || []).slice();
|
||||
pts[i] = Object.assign({}, pts[i], { [key]: val });
|
||||
setMasterAutoVol(pts);
|
||||
};
|
||||
const delAutoPoint = (i) => setMasterAutoVol((masterAutoVol || []).filter((p, j) => j !== i));
|
||||
|
||||
const ensureAudio = () => {
|
||||
getAudioContext();
|
||||
@@ -2864,6 +2907,41 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, masterVolL, setMast
|
||||
)
|
||||
)
|
||||
),
|
||||
React.createElement("div", { className: "flex flex-col gap-0.5 my-1 shrink-0" },
|
||||
React.createElement("div", { className: "flex items-center justify-between gap-1" },
|
||||
React.createElement("button", {
|
||||
id: "automBtn",
|
||||
onClick: function() { setAutoOpen(function(p) { return !p; }); },
|
||||
className: "btn-daw rounded px-1 py-0.5 text-[8px] font-bold" + (autoOpen ? " btn-teal-active" : ""),
|
||||
title: "Master Volume Automation (Gap 7)"
|
||||
}, "AUTOM"),
|
||||
React.createElement("button", {
|
||||
onClick: function() { setMasterAutoOn(function(p) { return !p; }); },
|
||||
className: "text-[8px] px-1 py-0.5 rounded font-mono" + (masterAutoOn ? " text-emerald-400 bg-emerald-500/10" : " text-slate-500"),
|
||||
title: "Bat/tat master volume automation"
|
||||
}, masterAutoOn ? "ON" : "OFF")
|
||||
),
|
||||
autoOpen && React.createElement("div", { className: "flex flex-col gap-0.5 bg-slate-950 border border-slate-800 rounded p-1 max-h-32 overflow-y-auto" },
|
||||
(masterAutoVol || []).map(function(pt, i) {
|
||||
return React.createElement("div", { key: i, className: "flex items-center gap-1" },
|
||||
React.createElement("input", {
|
||||
type: "number", step: "0.1", min: "0", value: pt.time,
|
||||
onChange: function(e) { updAutoPoint(i, 'time', parseFloat(e.target.value) || 0); },
|
||||
className: "w-12 bg-slate-900 border border-slate-700 rounded px-0.5 text-[8px] font-mono text-slate-200"
|
||||
}),
|
||||
React.createElement("span", { className: "text-[8px] text-slate-500" }, "s"),
|
||||
React.createElement("input", {
|
||||
type: "number", step: "0.5", min: "-30", max: "3", value: pt.db,
|
||||
onChange: function(e) { updAutoPoint(i, 'db', parseFloat(e.target.value) || 0); },
|
||||
className: "w-12 bg-slate-900 border border-slate-700 rounded px-0.5 text-[8px] font-mono text-slate-200"
|
||||
}),
|
||||
React.createElement("span", { className: "text-[8px] text-slate-500" }, "dB"),
|
||||
React.createElement("button", { onClick: function() { delAutoPoint(i); }, className: "text-[8px] text-red-400 hover:text-red-300 ml-auto" }, "x")
|
||||
);
|
||||
}),
|
||||
React.createElement("button", { onClick: addAutoPoint, className: "text-[8px] text-cyan-400 hover:text-cyan-300 mt-0.5" }, "+ ADD POINT")
|
||||
)
|
||||
),
|
||||
React.createElement("div", { className: "text-center text-[10px] font-bold font-mono text-slate-200 shrink-0" },
|
||||
masterVolDisplay
|
||||
),
|
||||
@@ -11564,7 +11642,7 @@ 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) || (masterState && (masterState.volL !== 0 || masterState.volR !== 0 || masterState.muted || !masterState.link)) ? {
|
||||
...((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)) || (Array.isArray(window.__masterAutoVol) && window.__masterAutoVol.length > 0) ? {
|
||||
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,
|
||||
@@ -11573,7 +11651,10 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
||||
link_lr: masterState ? !!masterState.link : false,
|
||||
muted: masterState ? !!masterState.muted : false,
|
||||
mute: masterState ? !!masterState.muted : false,
|
||||
bypass: !masteringSettings.masterConnected || !!masteringSettings.vstBypass
|
||||
bypass: !masteringSettings.masterConnected || !!masteringSettings.vstBypass,
|
||||
...((Array.isArray(window.__masterAutoVol) && window.__masterAutoVol.length > 0) ? {
|
||||
automation: { volume_db: window.__masterAutoVol.map(p => ({ time: p.time, db: p.db })) }
|
||||
} : {})
|
||||
}
|
||||
} : {})
|
||||
},
|
||||
@@ -11700,6 +11781,8 @@ const deserializeProjectFromSchema = (schemaObj) => {
|
||||
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),
|
||||
masterAutoVol: (_masterBus.automation && Array.isArray(_masterBus.automation.volume_db)) ? _masterBus.automation.volume_db.filter(p => p && typeof p.time === 'number' && typeof p.db === 'number') : [],
|
||||
masterAutoOn: !!(_masterBus.automation && Array.isArray(_masterBus.automation.volume_db) && _masterBus.automation.volume_db.length),
|
||||
zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0
|
||||
};
|
||||
};
|
||||
@@ -17961,6 +18044,17 @@ const App = () => {
|
||||
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 [masterAutoVol, setMasterAutoVol] = useState([]); // Gap 7: master volume automation [{time s, db}]
|
||||
const [masterAutoOn, setMasterAutoOn] = useState(false); // Gap 7: automation active
|
||||
React.useEffect(() => {
|
||||
window.__masterAutoVol = masterAutoVol;
|
||||
window.__masterAutoOn = masterAutoOn;
|
||||
}, [masterAutoVol, masterAutoOn]);
|
||||
React.useEffect(() => {
|
||||
if (isPlaying && typeof applyMasterAutomationSchedule === 'function') {
|
||||
try { applyMasterAutomationSchedule(currentTimeRef.current || 0); } catch (e) { console.warn('[Auto] reschedule error:', e); }
|
||||
}
|
||||
}, [masterAutoVol, masterAutoOn, isPlaying]);
|
||||
const [masterVU, setMasterVU] = useState(0); // 0-1
|
||||
const [masterMeterPeak, setMasterMeterPeak] = useState(0);
|
||||
const [rightSidebarWidth, setRightSidebarWidth] = useState(320);
|
||||
@@ -19156,6 +19250,8 @@ const App = () => {
|
||||
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);
|
||||
if (result.masterAutoVol !== undefined) setMasterAutoVol(result.masterAutoVol);
|
||||
if (result.masterAutoOn !== undefined) setMasterAutoOn(result.masterAutoOn);
|
||||
} else {
|
||||
restoredTracks = (parsed.tracks || []).map(function(t) {
|
||||
var rest = Object.assign({}, t); delete rest.height;
|
||||
@@ -19229,6 +19325,8 @@ const App = () => {
|
||||
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);
|
||||
if (result.masterAutoVol !== undefined) setMasterAutoVol(result.masterAutoVol);
|
||||
if (result.masterAutoOn !== undefined) setMasterAutoOn(result.masterAutoOn);
|
||||
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
|
||||
setProjectName(tmpName);
|
||||
localStorage.setItem('sonic_project_name', tmpName);
|
||||
@@ -19338,6 +19436,8 @@ const App = () => {
|
||||
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);
|
||||
if (result.masterAutoVol !== undefined) setMasterAutoVol(result.masterAutoVol);
|
||||
if (result.masterAutoOn !== undefined) setMasterAutoOn(result.masterAutoOn);
|
||||
} else {
|
||||
restoredTracks = (proj.tracks || []).map(t => {
|
||||
const { height: _h, ...rest } = t;
|
||||
@@ -19557,6 +19657,8 @@ const App = () => {
|
||||
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);
|
||||
if (result.masterAutoVol !== undefined) setMasterAutoVol(result.masterAutoVol);
|
||||
if (result.masterAutoOn !== undefined) setMasterAutoOn(result.masterAutoOn);
|
||||
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
|
||||
setProjectName(tmpName);
|
||||
localStorage.setItem('sonic_project_name', tmpName);
|
||||
@@ -22932,6 +23034,7 @@ const App = () => {
|
||||
} else {
|
||||
startTrackPlayback(pt);
|
||||
}
|
||||
try { applyMasterAutomationSchedule(pt); } catch (e) {}
|
||||
}
|
||||
} else {
|
||||
pendingRescheduleRef.current = null;
|
||||
@@ -24082,6 +24185,8 @@ const App = () => {
|
||||
// loop restart (rAF callback → React commit trễ hơn setTimeout 0) bị drop
|
||||
// → loop pass 2+ câm. Set NGAY tại scheduler.
|
||||
isPlayingRef.current = true;
|
||||
// Gap 7: master volume automation — schedule envelope tu playhead.
|
||||
try { applyMasterAutomationSchedule(offsetTime || 0); } catch (e) { console.warn('[Auto] schedule error:', e); }
|
||||
// ⚠️ FIX: đồng bộ mastering NGAY khi play — MIDI item phải qua mastering
|
||||
// FX (khi bật).
|
||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
|
||||
@@ -24684,6 +24789,8 @@ const App = () => {
|
||||
const stopAllPlayback = (fade) => {
|
||||
try {
|
||||
isPlayingRef.current = false;
|
||||
// Gap 7: xoa automation schedule — tra gain master ve 1.0 (fader rieng).
|
||||
try { if (masterBus && masterBus.output && masterBus.output.gain) { const _actx = getAudioContext(); masterBus.output.gain.cancelScheduledValues(_actx.currentTime); masterBus.output.gain.setValueAtTime(1, _actx.currentTime); } } catch (e) { }
|
||||
// Realtime VST FX: dừng session + khôi phục output→analyser.
|
||||
try { fxRtStop(); } catch (e) { console.warn('[FX RT] stop error:', e); }
|
||||
// Realtime VST FX per-track: dừng mọi session track + restore chain trực tiếp.
|
||||
@@ -27917,6 +28024,8 @@ const App = () => {
|
||||
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);
|
||||
if (result.masterAutoVol !== undefined) setMasterAutoVol(result.masterAutoVol);
|
||||
if (result.masterAutoOn !== undefined) setMasterAutoOn(result.masterAutoOn);
|
||||
} else {
|
||||
restored = (proj.tracks || []).map(t => ({
|
||||
...t,
|
||||
@@ -30531,6 +30640,8 @@ STRICT CONSTRAINTS:
|
||||
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);
|
||||
if (result.masterAutoVol !== undefined) setMasterAutoVol(result.masterAutoVol);
|
||||
if (result.masterAutoOn !== undefined) setMasterAutoOn(result.masterAutoOn);
|
||||
} else {
|
||||
restoredTracks = (parsed.tracks || []).map(function (t) {
|
||||
const rest = Object.assign({}, t); delete rest.height;
|
||||
@@ -33371,6 +33482,10 @@ STRICT CONSTRAINTS:
|
||||
setMasterLink: setMasterLink,
|
||||
masterMuted: masterMuted,
|
||||
setMasterMuted: setMasterMuted,
|
||||
masterAutoVol: masterAutoVol,
|
||||
setMasterAutoVol: setMasterAutoVol,
|
||||
masterAutoOn: masterAutoOn,
|
||||
setMasterAutoOn: setMasterAutoOn,
|
||||
showMasteringModal: showMasteringModal,
|
||||
setShowMasteringModal: setShowMasteringModal,
|
||||
masteringSettings: masteringSettings,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user