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:
@@ -63,6 +63,22 @@ def _find_default_sf2() -> str:
|
|||||||
return p
|
return p
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def _apply_master_volume_automation(buf, sr, nodes):
|
||||||
|
"""Gap 7: master volume automation lane (master.automation.volume_db).
|
||||||
|
|
||||||
|
nodes: [{time: giây, db}] — envelope tuyến tính dB, clamp -30..+3 dB như
|
||||||
|
track automation (sub_tab_dsp). Áp TRÊN master_buffer (2, N) — trả copy."""
|
||||||
|
nodes = [n for n in (nodes or []) if n.get("time") is not None and n.get("db") is not None]
|
||||||
|
if not nodes:
|
||||||
|
return buf
|
||||||
|
nodes = sorted(nodes, key=lambda x: float(x["time"]))
|
||||||
|
n_samples = buf.shape[1]
|
||||||
|
times = np.array([float(n["time"]) for n in nodes])
|
||||||
|
dbs = np.clip(np.array([float(n["db"]) for n in nodes]), -30.0, 3.0)
|
||||||
|
sample_times = np.arange(n_samples) / sr
|
||||||
|
interp = np.interp(sample_times, times, dbs, left=dbs[0], right=dbs[-1])
|
||||||
|
return buf * (10.0 ** (interp / 20.0))
|
||||||
|
|
||||||
# ── Builtin custom FX (spec fx_chain_architecture §2 Step 2: Custom FX) ─────
|
# ── Builtin custom FX (spec fx_chain_architecture §2 Step 2: Custom FX) ─────
|
||||||
# Track FX chain slots: eq / eqpro / compressor / limiter / exciter / rebalance
|
# Track FX chain slots: eq / eqpro / compressor / limiter / exciter / rebalance
|
||||||
# (khớp tham số WebAudio track FX modules + EQ Pro bands) + legacy chorus/reverb
|
# (khớp tham số WebAudio track FX modules + EQ Pro bands) + legacy chorus/reverb
|
||||||
@@ -907,6 +923,14 @@ class PythonRenderEngine:
|
|||||||
if vol_l:
|
if vol_l:
|
||||||
master_buffer *= 10 ** (vol_l / 20.0)
|
master_buffer *= 10 ** (vol_l / 20.0)
|
||||||
|
|
||||||
|
# Gap 7: master volume automation — envelope áp SAU fader, TRƯỚC
|
||||||
|
# normalize (fader cuối; automation = lớp âm lượng trên fader).
|
||||||
|
_auto = master.get("automation") or {}
|
||||||
|
_auto_vol = _auto.get("volume_db") or []
|
||||||
|
if _auto_vol:
|
||||||
|
master_buffer = _apply_master_volume_automation(
|
||||||
|
master_buffer, self.sample_rate, _auto_vol)
|
||||||
|
|
||||||
# Normalization to prevent clipping
|
# Normalization to prevent clipping
|
||||||
max_peak = np.max(np.abs(master_buffer))
|
max_peak = np.max(np.abs(master_buffer))
|
||||||
if max_peak > 1.0:
|
if max_peak > 1.0:
|
||||||
|
|||||||
@@ -50,6 +50,22 @@
|
|||||||
"fx_chain": {
|
"fx_chain": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "$ref": "#/definitions/FXPlugin" }
|
"items": { "$ref": "#/definitions/FXPlugin" }
|
||||||
|
},
|
||||||
|
"automation": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Gap 7: master automation lanes (volume_db)",
|
||||||
|
"properties": {
|
||||||
|
"volume_db": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"time": { "type": "number", "description": "giây" },
|
||||||
|
"db": { "type": "number", "minimum": -30.0, "maximum": 3.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-3
@@ -1178,6 +1178,37 @@ function initMasterBus(ctx) {
|
|||||||
function setMasterVolume(linear) {
|
function setMasterVolume(linear) {
|
||||||
if (masterBus) masterBus.output.gain.setValueAtTime(linear, audioCtx.currentTime);
|
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 ->
|
// Master L/R faders + LINK + MUTE - per-channel gain stage (output -> volSplit ->
|
||||||
// volLGain/volRGain -> volMerge -> muteGain -> analyser). time===undefined ->
|
// 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) ──
|
// ── 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 [isTestPlaying, setIsTestPlaying] = React.useState(false);
|
||||||
const [isMono, setIsMono] = React.useState(false);
|
const [isMono, setIsMono] = React.useState(false);
|
||||||
const [pan, setPan] = React.useState(0.0);
|
const [pan, setPan] = React.useState(0.0);
|
||||||
@@ -2565,6 +2596,18 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, masterVolL, setMast
|
|||||||
const panStartYRef = React.useRef(0);
|
const panStartYRef = React.useRef(0);
|
||||||
const startPanValRef = React.useRef(0);
|
const startPanValRef = React.useRef(0);
|
||||||
const [refOpen, setRefOpen] = React.useState(false);
|
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 = () => {
|
const ensureAudio = () => {
|
||||||
getAudioContext();
|
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" },
|
React.createElement("div", { className: "text-center text-[10px] font-bold font-mono text-slate-200 shrink-0" },
|
||||||
masterVolDisplay
|
masterVolDisplay
|
||||||
),
|
),
|
||||||
@@ -11564,7 +11642,7 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
|||||||
})(),
|
})(),
|
||||||
auto_compute_length: true,
|
auto_compute_length: true,
|
||||||
tracks: mainTracks,
|
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: {
|
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 })),
|
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,
|
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,
|
link_lr: masterState ? !!masterState.link : false,
|
||||||
muted: masterState ? !!masterState.muted : false,
|
muted: masterState ? !!masterState.muted : false,
|
||||||
mute: 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)),
|
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,
|
masterLink: !!_masterBus.link_lr,
|
||||||
masterMuted: !!(_masterBus.muted || _masterBus.mute),
|
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
|
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 [masterVolR, setMasterVolR] = useState(0); // dB - master R fader
|
||||||
const [masterLink, setMasterLink] = useState(true); // LINK L/R
|
const [masterLink, setMasterLink] = useState(true); // LINK L/R
|
||||||
const [masterMuted, setMasterMuted] = useState(false); // MUTE master
|
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 [masterVU, setMasterVU] = useState(0); // 0-1
|
||||||
const [masterMeterPeak, setMasterMeterPeak] = useState(0);
|
const [masterMeterPeak, setMasterMeterPeak] = useState(0);
|
||||||
const [rightSidebarWidth, setRightSidebarWidth] = useState(320);
|
const [rightSidebarWidth, setRightSidebarWidth] = useState(320);
|
||||||
@@ -19156,6 +19250,8 @@ const App = () => {
|
|||||||
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
|
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
|
||||||
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
||||||
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
|
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 {
|
} else {
|
||||||
restoredTracks = (parsed.tracks || []).map(function(t) {
|
restoredTracks = (parsed.tracks || []).map(function(t) {
|
||||||
var rest = Object.assign({}, t); delete rest.height;
|
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.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
|
||||||
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
||||||
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
|
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';
|
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
|
||||||
setProjectName(tmpName);
|
setProjectName(tmpName);
|
||||||
localStorage.setItem('sonic_project_name', 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.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
|
||||||
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
||||||
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
|
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 {
|
} else {
|
||||||
restoredTracks = (proj.tracks || []).map(t => {
|
restoredTracks = (proj.tracks || []).map(t => {
|
||||||
const { height: _h, ...rest } = t;
|
const { height: _h, ...rest } = t;
|
||||||
@@ -19557,6 +19657,8 @@ const App = () => {
|
|||||||
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
|
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
|
||||||
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
||||||
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
|
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';
|
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
|
||||||
setProjectName(tmpName);
|
setProjectName(tmpName);
|
||||||
localStorage.setItem('sonic_project_name', tmpName);
|
localStorage.setItem('sonic_project_name', tmpName);
|
||||||
@@ -22932,6 +23034,7 @@ const App = () => {
|
|||||||
} else {
|
} else {
|
||||||
startTrackPlayback(pt);
|
startTrackPlayback(pt);
|
||||||
}
|
}
|
||||||
|
try { applyMasterAutomationSchedule(pt); } catch (e) {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pendingRescheduleRef.current = null;
|
pendingRescheduleRef.current = null;
|
||||||
@@ -24082,6 +24185,8 @@ const App = () => {
|
|||||||
// loop restart (rAF callback → React commit trễ hơn setTimeout 0) bị drop
|
// loop restart (rAF callback → React commit trễ hơn setTimeout 0) bị drop
|
||||||
// → loop pass 2+ câm. Set NGAY tại scheduler.
|
// → loop pass 2+ câm. Set NGAY tại scheduler.
|
||||||
isPlayingRef.current = true;
|
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
|
// ⚠️ FIX: đồng bộ mastering NGAY khi play — MIDI item phải qua mastering
|
||||||
// FX (khi bật).
|
// FX (khi bật).
|
||||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
|
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
|
||||||
@@ -24684,6 +24789,8 @@ const App = () => {
|
|||||||
const stopAllPlayback = (fade) => {
|
const stopAllPlayback = (fade) => {
|
||||||
try {
|
try {
|
||||||
isPlayingRef.current = false;
|
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.
|
// Realtime VST FX: dừng session + khôi phục output→analyser.
|
||||||
try { fxRtStop(); } catch (e) { console.warn('[FX RT] stop error:', e); }
|
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.
|
// 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.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
|
||||||
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
||||||
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
|
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 {
|
} else {
|
||||||
restored = (proj.tracks || []).map(t => ({
|
restored = (proj.tracks || []).map(t => ({
|
||||||
...t,
|
...t,
|
||||||
@@ -30531,6 +30640,8 @@ STRICT CONSTRAINTS:
|
|||||||
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
|
if (result.masterVolR !== undefined && result.masterVolR !== null) setMasterVolR(result.masterVolR);
|
||||||
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
if (result.masterLink !== undefined && result.masterLink !== null) setMasterLink(result.masterLink);
|
||||||
if (result.masterMuted !== undefined && result.masterMuted !== null) setMasterMuted(result.masterMuted);
|
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 {
|
} else {
|
||||||
restoredTracks = (parsed.tracks || []).map(function (t) {
|
restoredTracks = (parsed.tracks || []).map(function (t) {
|
||||||
const rest = Object.assign({}, t); delete rest.height;
|
const rest = Object.assign({}, t); delete rest.height;
|
||||||
@@ -33371,6 +33482,10 @@ STRICT CONSTRAINTS:
|
|||||||
setMasterLink: setMasterLink,
|
setMasterLink: setMasterLink,
|
||||||
masterMuted: masterMuted,
|
masterMuted: masterMuted,
|
||||||
setMasterMuted: setMasterMuted,
|
setMasterMuted: setMasterMuted,
|
||||||
|
masterAutoVol: masterAutoVol,
|
||||||
|
setMasterAutoVol: setMasterAutoVol,
|
||||||
|
masterAutoOn: masterAutoOn,
|
||||||
|
setMasterAutoOn: setMasterAutoOn,
|
||||||
showMasteringModal: showMasteringModal,
|
showMasteringModal: showMasteringModal,
|
||||||
setShowMasteringModal: setShowMasteringModal,
|
setShowMasteringModal: setShowMasteringModal,
|
||||||
masteringSettings: masteringSettings,
|
masteringSettings: masteringSettings,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -51,7 +51,7 @@
|
|||||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||||
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
|
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
|
||||||
<script src="/static/js/app.precompiled.js?v=202608212310" defer></script>
|
<script src="/static/js/app.precompiled.js?v=202608212325" defer></script>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -806,3 +806,54 @@ def test_64_soundfont_render_via_bridge(monkeypatch, tmp_path):
|
|||||||
engine.render_project(project, out)
|
engine.render_project(project, out)
|
||||||
data, _ = _read_wav(out)
|
data, _ = _read_wav(out)
|
||||||
assert float(np.max(np.abs(data))) > 0.01, "engine render soundfont im lặng — fallback oscillator?"
|
assert float(np.max(np.abs(data))) > 0.01, "engine render soundfont im lặng — fallback oscillator?"
|
||||||
|
|
||||||
|
# ── 5.11 Gap 7: master volume automation lane ─────────────────────────────
|
||||||
|
def test_65_master_volume_automation(monkeypatch, tmp_path, fx_bridge):
|
||||||
|
"""Gap 7: master.automation.volume_db [{time,db}] — envelope áp sau fader,
|
||||||
|
trước normalize: trước node 1s giữ nguyên, sau node 1s gain ~ -12dB (0.25)."""
|
||||||
|
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(auto_nodes):
|
||||||
|
track = _midi_track()
|
||||||
|
track["items"][0]["source_data"]["notes"][0]["duration_beats"] = 4.0 # phủ cả 2s
|
||||||
|
track["volume_db"] = -12.0 # tránh normalize (>1.0)
|
||||||
|
p = {
|
||||||
|
"metadata": {"bpm": 120.0, "time_signature_numerator": 4},
|
||||||
|
"main_session": {
|
||||||
|
"length_bars": 1.0,
|
||||||
|
"tracks": [track],
|
||||||
|
"master": {"fx_chain": [], "bypass": False, "volume_db": 0.0},
|
||||||
|
},
|
||||||
|
"section_store": {},
|
||||||
|
}
|
||||||
|
if auto_nodes is not None:
|
||||||
|
p["main_session"]["master"]["automation"] = {"volume_db": auto_nodes}
|
||||||
|
return p
|
||||||
|
|
||||||
|
def rms(path, start_s, dur_s):
|
||||||
|
data, sr = _read_wav(path)
|
||||||
|
i0, i1 = int(start_s * sr), int((start_s + dur_s) * sr)
|
||||||
|
return float(np.sqrt(np.mean(data[:, i0:i1] ** 2)))
|
||||||
|
|
||||||
|
out_base = str(tmp_path / "auto_base.wav")
|
||||||
|
out_env = str(tmp_path / "auto_env.wav")
|
||||||
|
engine.render_project(project(None), out_base)
|
||||||
|
# Hai đoạn phẳng + chuyển nhanh 0dB→-12dB quanh t=1s (interp dB tuyến tính
|
||||||
|
# giữa node liền kề; đoạn phẳng không bị ảnh hưởng bởi ramp lân cận).
|
||||||
|
engine.render_project(project([
|
||||||
|
{"time": 0.0, "db": 0.0}, {"time": 0.9, "db": 0.0},
|
||||||
|
{"time": 1.0, "db": -12.0}, {"time": 2.0, "db": -12.0},
|
||||||
|
]), out_env)
|
||||||
|
base0 = rms(out_base, 0.1, 0.6)
|
||||||
|
env0 = rms(out_env, 0.1, 0.6)
|
||||||
|
env1 = rms(out_env, 1.15, 0.6)
|
||||||
|
assert env0 == pytest.approx(base0, rel=0.05), f"đoạn 0dB phải giữ nguyên: {env0} vs {base0}"
|
||||||
|
ratio = env1 / max(env0, 1e-9)
|
||||||
|
assert 0.20 < ratio < 0.30, f"envelope -12dB → gain ~0.25: {ratio}"
|
||||||
|
|||||||
Reference in New Issue
Block a user