FIX: Piano roll tab không xuất âm thanh qua mastering chain được recovery nhưng âm thanh rất nhỏ

This commit is contained in:
2026-08-05 12:16:00 +07:00
parent 55d3464b1e
commit 8a9d6b3c27
4 changed files with 87 additions and 31 deletions
+64 -28
View File
@@ -336,9 +336,9 @@ function applyMasteringSettings(s) {
const upwardGainLinear = (s.maximizerActive && s.maxUpward > 0) ? (Math.pow(10, clamp(s.maxUpward, 0, 30) / 20) - 1.0) : 0.0;
masterBus.upwardGain.gain.setTargetAtTime(upwardGainLinear, now, 0.01);
// Limiter Threshold
// Limiter Threshold (WaveShaper ceiling _setCeiling rebuild curve)
const ceilingVal = s.maximizerActive ? clamp(s.ceiling, -60, 0) : -0.1;
masterBus.maximizerCompressor.threshold.setTargetAtTime(ceilingVal, now, 0.01);
if (masterBus.maximizerCompressor._setCeiling) masterBus.maximizerCompressor._setCeiling(ceilingVal);
// 4. Bus Compressor module (mastering_expand.md §II.2)
if (masterBus.compNode) {
@@ -348,11 +348,14 @@ function applyMasteringSettings(s) {
masterBus.compMakeup.gain.setTargetAtTime(compOn ? Math.pow(10, clamp(s.compMakeup, 0, 12) / 20) : 1.0, now, 0.02);
}
// 5. Brickwall Limiter module (ratio 20:1, knee 0)
// 5. Brickwall Limiter module (WaveShaper tanh threshold = mc clip; OFF = identity)
if (masterBus.limNode) {
const limOn = !!s.limActive;
masterBus.limNode.threshold.setTargetAtTime(limOn ? clamp(s.limThreshold, -24, 0) : 0, now, 0.02);
masterBus.limNode.ratio.setTargetAtTime(limOn ? 20 : 1, now, 0.02);
if (limOn) {
if (masterBus.limNode._setThreshold) masterBus.limNode._setThreshold(clamp(s.limThreshold, -24, 0));
} else {
try { masterBus.limNode.curve = new Float32Array([-1, 1]); } catch (e) {}
}
}
// 6. Harmonic Exciter module (dry/wet mix; dry stays 1.0 for subtle warmth)
@@ -501,12 +504,21 @@ function initMasterBus(ctx) {
upwardCompressor.connect(upwardGain);
upwardGain.connect(upwardSummingGain);
const maximizerCompressor = ctx.createDynamicsCompressor();
maximizerCompressor.threshold.value = -0.1;
maximizerCompressor.knee.value = 0.0;
maximizerCompressor.ratio.value = 20.0;
maximizerCompressor.attack.value = 0.001;
maximizerCompressor.release.value = 0.05;
// Brickwall Limiter ti ceiling: WaveShaper HARD CLIP (slope 1 không boost,
// clip chính xác ti ceiling) KHÔNG DynamicsCompressor (NaN trên bass
// transient chain state-bad CÂM + stuck).
const maximizerCompressor = ctx.createWaveShaper();
maximizerCompressor.oversample = '2x';
let _maxCeil = -0.1;
const _buildMaxCurve = (db) => {
const c = Math.pow(10, Math.max(-60, Math.min(0, db)) / 20);
const _c = new Float32Array(4096);
for (let _i = 0; _i < 4096; _i++) { const _x = (_i / 4095) * 2 - 1; _c[_i] = Math.max(-c, Math.min(c, _x)); }
maximizerCompressor.curve = _c;
_maxCeil = db;
};
_buildMaxCurve(-0.1);
maximizerCompressor._setCeiling = (db) => { if (db !== _maxCeil) _buildMaxCurve(db); };
upwardSummingGain.connect(maximizerCompressor);
@@ -525,14 +537,23 @@ function initMasterBus(ctx) {
compNode.connect(compMakeup);
compMakeup.connect(compOutput);
// Brickwall Limiter module (ratio 20:1, knee 0)
// Brickwall Limiter module (WaveShaper tanh soft-clip KHÔNG
// DynamicsCompressor: NaN trên bass transient chain stuck)
const limInput = ctx.createGain();
const limNode = ctx.createDynamicsCompressor();
limNode.threshold.value = -1.0;
limNode.knee.value = 0;
limNode.ratio.value = 20;
limNode.attack.value = 0.001;
limNode.release.value = 0.05;
const limNode = ctx.createWaveShaper();
limNode.oversample = '2x';
let _limLastThresh = null;
const _buildLimCurve = (db) => {
const tLin = Math.pow(10, Math.max(-24, Math.min(0, db)) / 20);
const k = 1 / Math.max(0.02, tLin);
const _c = new Float32Array(4096);
const _tk = Math.tanh(k);
for (let _i = 0; _i < 4096; _i++) { const _x = (_i / 4095) * 2 - 1; _c[_i] = Math.tanh(_x * k) / _tk; }
limNode.curve = _c;
_limLastThresh = db;
};
_buildLimCurve(-1.0);
limNode._setThreshold = (db) => { if (db !== _limLastThresh) _buildLimCurve(db); };
const limOutput = ctx.createGain();
limInput.connect(limNode);
limNode.connect(limOutput);
@@ -643,10 +664,12 @@ function initMasterBus(ctx) {
eqMid1Filter.connect(eqMid2Filter);
eqMid2Filter.connect(eqHighFilter);
// Setup default non-mastered routing:
// input -> compressor -> inputAnalyser -> outputAnalyser -> output -> analyser -> destination
masterBus.input.connect(masterBus.compressor);
masterBus.compressor.connect(masterBus.inputAnalyser);
// Setup default non-mastered routing (KHÔNG compressor mc đnh trong path):
// input -> inputAnalyser -> outputAnalyser -> output -> analyser -> destination
// Compressor mc đnh (ratio 12, threshold -24 LUÔN-ON) va (a) pump-down
// tín hiu âm nh/méo, va (b) phát NaN khi gp bass transient 11 biquad
// "state is bad" CÂM + stuck. Mastering chain có comp/lim module riêng khi bt.
masterBus.input.connect(masterBus.inputAnalyser);
masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
masterBus.outputAnalyser.connect(masterBus.output);
masterBus.output.connect(masterBus.analyser);
@@ -867,12 +890,25 @@ function createTrackFxModule(type, ctx, params) {
input.connect(comp); comp.connect(makeup); makeup.connect(output);
nodes = { comp, makeup };
} else if (type === 'limiter') {
const lim = ctx.createDynamicsCompressor();
lim.threshold.value = num(p.ceiling, -1.0);
lim.knee.value = 0; lim.ratio.value = 20;
lim.attack.value = 0.001; lim.release.value = 0.05;
input.connect(lim); lim.connect(output);
nodes = { lim };
// Brickwall Limiter bng WaveShaper tanh soft-clip KHÔNG DynamicsCompressor:
// Chromium compressor phát NaN vi bass transient mnh (pitch thp + vel cao
// đng lot) NaN vào master chain 11 biquad "state is bad" CÂM + stuck.
const shaper = ctx.createWaveShaper();
shaper.oversample = '2x';
const ceilingDb = Math.min(0, num(p.ceiling, -1.0));
const threshLin = Math.pow(10, ceilingDb / 20);
const k = 1 / Math.max(0.02, threshLin);
const _curve = new Float32Array(4096);
const _tanhK = Math.tanh(k);
for (let _i = 0; _i < 4096; _i++) {
const _x = (_i / 4095) * 2 - 1;
_curve[_i] = Math.tanh(_x * k) / _tanhK;
}
shaper.curve = _curve;
const makeup = ctx.createGain();
makeup.gain.value = 1.0;
input.connect(shaper); shaper.connect(makeup); makeup.connect(output);
nodes = { shaper, makeup };
} else if (type === 'exciter') {
const hp = ctx.createBiquadFilter();
hp.type = 'highpass'; hp.frequency.value = clampF(2000); hp.Q.value = 0.7;