fix: câm toàn cục (IN peak có OUT peak không) - WaveShaper luôn identity curve không null + clamp tần số biquad dưới Nyquist + watchdog tự bypass master chain khi hỏng

This commit is contained in:
2026-08-03 12:30:56 +07:00
parent 94b2d2ef41
commit 7325fbfc45
4 changed files with 59 additions and 22 deletions
+35 -14
View File
@@ -128,12 +128,12 @@ function applyMasteringSettings(s) {
const boostLinear = (s.maximizerActive) ? Math.pow(10, clamp(s.maxGain, -60, 30) / 20) : 1.0;
masterBus.maximizerBoostGain.gain.setTargetAtTime(boostLinear, now, 0.01);
// Soft Clipper
// Soft Clipper (identity passthrough when off never null curve)
if (s.maximizerActive && s.maxSoftClip > 0) {
const k = 1 + (clamp(s.maxSoftClip, 0, 100) / 100) * 10;
masterBus.maximizerSoftClipper.curve = makeDistortionCurve(k);
} else {
masterBus.maximizerSoftClipper.curve = null;
masterBus.maximizerSoftClipper.curve = new Float32Array([-1, 1]);
}
// Upward Compressor
@@ -147,37 +147,45 @@ function applyMasteringSettings(s) {
function initMasterBus(ctx) {
if (masterBus) return masterBus;
// Clamp every filter frequency below Nyquist (0.45 * sampleRate). A biquad
// with frequency Nyquist gets NaN coefficients "BiquadFilterNode: state
// is bad" the whole mastering chain outputs silence (IN peak yes, OUT no).
// Low sample-rate devices (8/11/16 kHz audio drivers) would otherwise break
// the 10 kHz highshelf / 6 kHz imager crossover filters.
const maxFilterFreq = (ctx.sampleRate || 44100) * 0.45;
const clampF = v => Math.max(20, Math.min(v, maxFilterFreq));
// Create EQ filters
const eqLowFilter = ctx.createBiquadFilter();
eqLowFilter.type = 'lowshelf';
eqLowFilter.frequency.value = 100;
eqLowFilter.frequency.value = clampF(100);
const eqMid1Filter = ctx.createBiquadFilter();
eqMid1Filter.type = 'peaking';
eqMid1Filter.frequency.value = 822;
eqMid1Filter.frequency.value = clampF(822);
eqMid1Filter.Q.value = 0.7;
const eqMid2Filter = ctx.createBiquadFilter();
eqMid2Filter.type = 'peaking';
eqMid2Filter.frequency.value = 3200;
eqMid2Filter.frequency.value = clampF(3200);
eqMid2Filter.Q.value = 1.2;
const eqHighFilter = ctx.createBiquadFilter();
eqHighFilter.type = 'highshelf';
eqHighFilter.frequency.value = 10000;
eqHighFilter.frequency.value = clampF(10000);
// Create Stereo Imager nodes
const imagerInput = ctx.createGain();
const imagerOutput = ctx.createGain();
// Imager Crossover Filters
const f1_lp = ctx.createBiquadFilter(); f1_lp.type = 'lowpass'; f1_lp.frequency.value = 100;
const f2_hp = ctx.createBiquadFilter(); f2_hp.type = 'highpass'; f2_hp.frequency.value = 100;
const f2_lp = ctx.createBiquadFilter(); f2_lp.type = 'lowpass'; f2_lp.frequency.value = 1000;
const f3_hp = ctx.createBiquadFilter(); f3_hp.type = 'highpass'; f3_hp.frequency.value = 1000;
const f3_lp = ctx.createBiquadFilter(); f3_lp.type = 'lowpass'; f3_lp.frequency.value = 6000;
const f4_hp = ctx.createBiquadFilter(); f4_hp.type = 'highpass'; f4_hp.frequency.value = 6000;
const f1_lp = ctx.createBiquadFilter(); f1_lp.type = 'lowpass'; f1_lp.frequency.value = clampF(100);
const f2_hp = ctx.createBiquadFilter(); f2_hp.type = 'highpass'; f2_hp.frequency.value = clampF(100);
const f2_lp = ctx.createBiquadFilter(); f2_lp.type = 'lowpass'; f2_lp.frequency.value = clampF(1000);
const f3_hp = ctx.createBiquadFilter(); f3_hp.type = 'highpass'; f3_hp.frequency.value = clampF(1000);
const f3_lp = ctx.createBiquadFilter(); f3_lp.type = 'lowpass'; f3_lp.frequency.value = clampF(6000);
const f4_hp = ctx.createBiquadFilter(); f4_hp.type = 'highpass'; f4_hp.frequency.value = clampF(6000);
const split1 = ctx.createChannelSplitter(2);
const split2 = ctx.createChannelSplitter(2);
@@ -235,7 +243,10 @@ function initMasterBus(ctx) {
// Maximizer nodes
const maximizerBoostGain = ctx.createGain();
const maximizerSoftClipper = ctx.createWaveShaper();
maximizerSoftClipper.curve = null;
// NEVER leave the curve null: a WaveShaper with a null/identity curve can
// output silence in some engines, which would kill the whole mastering path.
// Use an explicit linear identity table for passthrough.
maximizerSoftClipper.curve = new Float32Array([-1, 1]);
maximizerSoftClipper.oversample = '4x';
const upwardCompressor = ctx.createDynamicsCompressor();
@@ -8541,6 +8552,16 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
renderMeter(masterBus && masterBus.inputAnalyser, inMeterCanvasRef, 'inPeakText');
renderMeter(masterBus && masterBus.outputAnalyser, outMeterCanvasRef, 'outPeakText');
// Safety watchdog: if the mastering chain is broken (signal in, silence
// out e.g. a biquad in a bad state), fall back to the direct routing so
// audio is NEVER globally silent. The user can re-enable mastering after.
const masterInPk = getPeakLevel(masterBus && masterBus.inputAnalyser);
const masterOutPk = getPeakLevel(masterBus && masterBus.outputAnalyser);
if (masterBus && masterBus.masteringActive && masterInPk > 0.01 && masterOutPk < 0.001) {
console.warn('[Mastering] Chain broken (signal in, no signal out) — bypassing mastering to restore audio.');
toggleMasteringOnMaster(false, false);
}
// Wave Observer Oscilloscope Rendering
const woCanvas = woCanvasRef.current;
if (woCanvas) {