diff --git a/app/core/native_render.py b/app/core/native_render.py
index 99ee9c6..d0a413d 100644
--- a/app/core/native_render.py
+++ b/app/core/native_render.py
@@ -249,7 +249,7 @@ def render_fx_chain(input_wav, fx_chain, sample_rate, out_path=None):
"preset_b64": fx.get("preset_b64", "") or "",
"bypass": bool(fx.get("bypass"))})
elif typ in ("eq", "eqpro", "imager", "maximizer", "compressor",
- "limiter", "exciter", "rebalance"):
+ "limiter", "exciter", "rebalance", "multiband", "deesser"):
# Builtin DSP — chain hợp nhất (spec PLAN_DAW_A.md Phase 1). C++
# RenderFxJob xử lý theo id/params (Phase 2); giữ NGUYÊN thứ tự.
slots.append({"type": "builtin", "id": typ,
diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx
index 0c74040..0249950 100644
--- a/app/static/js/app.jsx
+++ b/app/static/js/app.jsx
@@ -13506,12 +13506,16 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
maximizer: { name: 'Maximizer', sub: 'IRC IV True Peak', icon: 'gauge', color: '#34d399' },
compressor: { name: 'Bus Compressor', sub: 'Glue & Punch', icon: 'compress', color: '#fbbf24' },
limiter: { name: 'Brickwall Limiter', sub: 'True-Peak 20:1', icon: 'shield-half', color: '#f43f5e' },
+ multiband: { name: 'Multiband Compressor', sub: '3-Band LR4', icon: 'layers', color: '#a3e635' },
+ deesser: { name: 'De-Esser', sub: 'Sibilance tamer', icon: 'mic-2', color: '#fb923c' },
exciter: { name: 'Harmonic Exciter', sub: 'Saturation & Air', icon: 'wand-2', color: '#c084fc' },
rebalance: { name: 'Master Rebalance', sub: 'M/S Balance', icon: 'sliders-horizontal', color: '#38bdf8' },
vst3: { name: 'VST FX', sub: 'VST3 plugin', icon: 'plug', color: '#2dd4bf' }
};
- const chainFlag = (type) => type === 'eq' ? 'eqActive' : type === 'eqpro' ? 'eqproActive' : type === 'imager' ? 'imagerActive' : type === 'maximizer' ? 'maximizerActive' : type === 'compressor' ? 'compActive' : type === 'limiter' ? 'limActive' : type === 'exciter' ? 'excActive' : type === 'vst3' ? 'vstFxActive' : 'rebalActive';
+ const chainFlag = (type) => type === 'eq' ? 'eqActive' : type === 'eqpro' ? 'eqproActive' : type === 'imager' ? 'imagerActive' : type === 'maximizer' ? 'maximizerActive' : type === 'compressor' ? 'compActive' : type === 'limiter' ? 'limActive' : type === 'exciter' ? 'excActive' : type === 'vst3' ? 'vstFxActive' : type === 'multiband' ? 'mbActive' : type === 'deesser' ? 'deessActive' : 'rebalActive';
const chainActive = (type) => !!ozState[chainFlag(type)];
+ const mb = (ozState.chain || []).find(m => m.type === 'multiband');
+ const de = (ozState.chain || []).find(m => m.type === 'deesser');
const toggleChainModule = (modId) => {
setOzState(prev => {
@@ -13563,6 +13567,8 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
const id = 'mod_' + type + '_' + Date.now();
const entry = { id, type, name: meta.name, active: true };
if (type === 'eqpro') entry.params = { amount: 100, bands: JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS)) };
+ if (type === 'multiband') entry.params = { lf_cross: 200, hf_cross: 4000, low_thr: -24, low_ratio: 3, low_makeup: 0, mid_thr: -24, mid_ratio: 3, mid_makeup: 0, high_thr: -24, high_ratio: 3, high_makeup: 0 };
+ if (type === 'deesser') entry.params = { threshold: -30, ratio: 40, freq: 6000, makeup: 0 };
setOzState(prev => ({
...prev,
chain: [...(prev.chain || []), entry],
@@ -13996,6 +14002,86 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
+ {/* VIEW: MULTIBAND COMPRESSOR */}
+
+
+
+
Multiband Compressor
+
LR4 crossover · low/mid/high
+
+
+
+
+ mb && updateChainEntryParams(mb.id, { lf_cross: v })} />
+
+
+ mb && updateChainEntryParams(mb.id, { hf_cross: v })} />
+
+
+ Low < LF
Mid giữa 2 crossover
High > HF · Release 250ms
+
+
+ mb && updateChainEntryParams(mb.id, { low_thr: v })} />
+
+
+ mb && updateChainEntryParams(mb.id, { low_ratio: v })} />
+
+
+ mb && updateChainEntryParams(mb.id, { low_makeup: v })} />
+
+
+ mb && updateChainEntryParams(mb.id, { mid_thr: v })} />
+
+
+ mb && updateChainEntryParams(mb.id, { mid_ratio: v })} />
+
+
+ mb && updateChainEntryParams(mb.id, { mid_makeup: v })} />
+
+
+ mb && updateChainEntryParams(mb.id, { high_thr: v })} />
+
+
+ mb && updateChainEntryParams(mb.id, { high_ratio: v })} />
+
+
+ mb && updateChainEntryParams(mb.id, { high_makeup: v })} />
+
+
+
+
+
+ {/* VIEW: DE-ESSER */}
+
+
+
+
De-Esser
+
Detect sibilance @freq · envelope release 150ms
+
+
+
+
+ de && updateChainEntryParams(de.id, { threshold: v })} />
+
+
+ de && updateChainEntryParams(de.id, { ratio: v })} />
+
+
+ de && updateChainEntryParams(de.id, { freq: v })} />
+
+
+ de && updateChainEntryParams(de.id, { makeup: v })} />
+
+
+
+
+
{/* VIEW: HARMONIC EXCITER */}
@@ -14204,7 +14290,9 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
{ t: 'rebalance', c: 'text-sky-400', i: 'sliders-horizontal', d: 'Cân bằng Mid/Side (Vocal/Bass vs stereo width).' },
{ t: 'eq', c: 'text-cyan-400', i: 'activity', d: 'EQ 4-band (lowshelf, 2× peaking, highshelf) + presets.' },
{ t: 'imager', c: 'text-fuchsia-400', i: 'radio', d: 'Stereo width 4-band M/S + vectorscope/correlation.' },
- { t: 'maximizer', c: 'text-emerald-400', i: 'gauge', d: 'Maximizer: boost, soft clip, upward comp, ceiling.' }
+ { t: 'maximizer', c: 'text-emerald-400', i: 'gauge', d: 'Maximizer: boost, soft clip, upward comp, ceiling.' },
+ { t: 'multiband', c: 'text-lime-400', i: 'layers', d: 'Compressor 3 băng tần low/mid/high với crossover LR4.' },
+ { t: 'deesser', c: 'text-orange-400', i: 'mic-2', d: 'Giảm sibilance (âm xuýt) theo tần số phát hiện.' }
].map(m => (