From 7325fbfc45ac254afbf9cfd0582df22ebc282d09 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 3 Aug 2026 12:30:56 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20c=C3=A2m=20to=C3=A0n=20c=E1=BB=A5c=20(IN?= =?UTF-8?q?=20peak=20c=C3=B3=20OUT=20peak=20kh=C3=B4ng)=20-=20WaveShaper?= =?UTF-8?q?=20lu=C3=B4n=20identity=20curve=20kh=C3=B4ng=20null=20+=20clamp?= =?UTF-8?q?=20t=E1=BA=A7n=20s=E1=BB=91=20biquad=20d=C6=B0=E1=BB=9Bi=20Nyqu?= =?UTF-8?q?ist=20+=20watchdog=20t=E1=BB=B1=20bypass=20master=20chain=20khi?= =?UTF-8?q?=20h=E1=BB=8Fng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 49 +++++++++++++++++++++++--------- app/static/js/app.precompiled.js | 25 +++++++++++----- app/templates/index.html | 2 +- wiki.md | 5 ++++ 4 files changed, 59 insertions(+), 22 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index a6e28ac..41e50bd 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -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) { diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 7173325..f3527ec 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -24,19 +24,27 @@ return Math.min(hi,Math.max(lo,n));};// 1. EQ Settings // Chromium's "BiquadFilterNode: state is bad"). masterBus.eqLowFilter.gain.cancelScheduledValues(now);masterBus.eqLowFilter.gain.setTargetAtTime(s.eqActive?clamp(s.eqLowGain,-24,24):0,now,0.05);masterBus.eqMid1Filter.gain.cancelScheduledValues(now);masterBus.eqMid1Filter.gain.setTargetAtTime(s.eqActive?clamp(s.eqMid1Gain,-24,24):0,now,0.05);masterBus.eqMid2Filter.gain.cancelScheduledValues(now);masterBus.eqMid2Filter.gain.setTargetAtTime(s.eqActive?clamp(s.eqMid2Gain,-24,24):0,now,0.05);masterBus.eqHighFilter.gain.cancelScheduledValues(now);masterBus.eqHighFilter.gain.setTargetAtTime(s.eqActive?clamp(s.eqHighGain,-24,24):0,now,0.05);// 2. Imager Settings (Mid/Side matrix width control for each band) const updateImagerBand=(w,active,gainLL,gainRL,gainLR,gainRR)=>{const widthVal=s.imagerActive&&active?clamp(w,-100,100):0;const g1=1+widthVal/200;const g2=-widthVal/200;gainLL.gain.setTargetAtTime(g1,now,0.01);gainRR.gain.setTargetAtTime(g1,now,0.01);gainRL.gain.setTargetAtTime(g2,now,0.01);gainLR.gain.setTargetAtTime(g2,now,0.01);};updateImagerBand(s.w1,true,masterBus.gainLL1,masterBus.gainRL1,masterBus.gainLR1,masterBus.gainRR1);updateImagerBand(s.w2,true,masterBus.gainLL2,masterBus.gainRL2,masterBus.gainLR2,masterBus.gainRR2);updateImagerBand(s.w3,true,masterBus.gainLL3,masterBus.gainRL3,masterBus.gainLR3,masterBus.gainRR3);updateImagerBand(s.w4,true,masterBus.gainLL4,masterBus.gainRL4,masterBus.gainLR4,masterBus.gainRR4);// 3. Maximizer Settings -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 -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;}// Upward Compressor +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 (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=new Float32Array([-1,1]);}// Upward Compressor 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 -const ceilingVal=s.maximizerActive?clamp(s.ceiling,-60,0):-0.1;masterBus.maximizerCompressor.threshold.setTargetAtTime(ceilingVal,now,0.01);}function initMasterBus(ctx){if(masterBus)return masterBus;// Create EQ filters -const eqLowFilter=ctx.createBiquadFilter();eqLowFilter.type='lowshelf';eqLowFilter.frequency.value=100;const eqMid1Filter=ctx.createBiquadFilter();eqMid1Filter.type='peaking';eqMid1Filter.frequency.value=822;eqMid1Filter.Q.value=0.7;const eqMid2Filter=ctx.createBiquadFilter();eqMid2Filter.type='peaking';eqMid2Filter.frequency.value=3200;eqMid2Filter.Q.value=1.2;const eqHighFilter=ctx.createBiquadFilter();eqHighFilter.type='highshelf';eqHighFilter.frequency.value=10000;// Create Stereo Imager nodes +const ceilingVal=s.maximizerActive?clamp(s.ceiling,-60,0):-0.1;masterBus.maximizerCompressor.threshold.setTargetAtTime(ceilingVal,now,0.01);}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=clampF(100);const eqMid1Filter=ctx.createBiquadFilter();eqMid1Filter.type='peaking';eqMid1Filter.frequency.value=clampF(822);eqMid1Filter.Q.value=0.7;const eqMid2Filter=ctx.createBiquadFilter();eqMid2Filter.type='peaking';eqMid2Filter.frequency.value=clampF(3200);eqMid2Filter.Q.value=1.2;const eqHighFilter=ctx.createBiquadFilter();eqHighFilter.type='highshelf';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 split1=ctx.createChannelSplitter(2);const split2=ctx.createChannelSplitter(2);const split3=ctx.createChannelSplitter(2);const split4=ctx.createChannelSplitter(2);const merge1=ctx.createChannelMerger(2);const merge2=ctx.createChannelMerger(2);const merge3=ctx.createChannelMerger(2);const merge4=ctx.createChannelMerger(2);const gainLL1=ctx.createGain();const gainRL1=ctx.createGain();const gainLR1=ctx.createGain();const gainRR1=ctx.createGain();const gainLL2=ctx.createGain();const gainRL2=ctx.createGain();const gainLR2=ctx.createGain();const gainRR2=ctx.createGain();const gainLL3=ctx.createGain();const gainRL3=ctx.createGain();const gainLR3=ctx.createGain();const gainRR3=ctx.createGain();const gainLL4=ctx.createGain();const gainRL4=ctx.createGain();const gainLR4=ctx.createGain();const gainRR4=ctx.createGain();// Connections for Imager DSP +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);const split3=ctx.createChannelSplitter(2);const split4=ctx.createChannelSplitter(2);const merge1=ctx.createChannelMerger(2);const merge2=ctx.createChannelMerger(2);const merge3=ctx.createChannelMerger(2);const merge4=ctx.createChannelMerger(2);const gainLL1=ctx.createGain();const gainRL1=ctx.createGain();const gainLR1=ctx.createGain();const gainRR1=ctx.createGain();const gainLL2=ctx.createGain();const gainRL2=ctx.createGain();const gainLR2=ctx.createGain();const gainRR2=ctx.createGain();const gainLL3=ctx.createGain();const gainRL3=ctx.createGain();const gainLR3=ctx.createGain();const gainRR3=ctx.createGain();const gainLL4=ctx.createGain();const gainRL4=ctx.createGain();const gainLR4=ctx.createGain();const gainRR4=ctx.createGain();// Connections for Imager DSP imagerInput.connect(f1_lp);imagerInput.connect(f2_hp);f2_hp.connect(f2_lp);imagerInput.connect(f3_hp);f3_hp.connect(f3_lp);imagerInput.connect(f4_hp);// Band 1 f1_lp.connect(split1);split1.connect(gainLL1,0);split1.connect(gainLR1,0);split1.connect(gainRL1,1);split1.connect(gainRR1,1);gainLL1.connect(merge1,0,0);gainRL1.connect(merge1,0,0);gainLR1.connect(merge1,0,1);gainRR1.connect(merge1,0,1);merge1.connect(imagerOutput);// Band 2 f2_lp.connect(split2);split2.connect(gainLL2,0);split2.connect(gainLR2,0);split2.connect(gainRL2,1);split2.connect(gainRR2,1);gainLL2.connect(merge2,0,0);gainRL2.connect(merge2,0,0);gainLR2.connect(merge2,0,1);gainRR2.connect(merge2,0,1);merge2.connect(imagerOutput);// Band 3 f3_lp.connect(split3);split3.connect(gainLL3,0);split3.connect(gainLR3,0);split3.connect(gainRL3,1);split3.connect(gainRR3,1);gainLL3.connect(merge3,0,0);gainRL3.connect(merge3,0,0);gainLR3.connect(merge3,0,1);gainRR3.connect(merge3,0,1);merge3.connect(imagerOutput);// Band 4 f4_hp.connect(split4);split4.connect(gainLL4,0);split4.connect(gainLR4,0);split4.connect(gainRL4,1);split4.connect(gainRR4,1);gainLL4.connect(merge4,0,0);gainRL4.connect(merge4,0,0);gainLR4.connect(merge4,0,1);gainRR4.connect(merge4,0,1);merge4.connect(imagerOutput);// Maximizer nodes -const maximizerBoostGain=ctx.createGain();const maximizerSoftClipper=ctx.createWaveShaper();maximizerSoftClipper.curve=null;maximizerSoftClipper.oversample='4x';const upwardCompressor=ctx.createDynamicsCompressor();upwardCompressor.threshold.value=-30;upwardCompressor.knee.value=10;upwardCompressor.ratio.value=4;upwardCompressor.attack.value=0.01;upwardCompressor.release.value=0.1;const upwardGain=ctx.createGain();upwardGain.gain.value=0.0;const upwardSummingGain=ctx.createGain();maximizerBoostGain.connect(maximizerSoftClipper);maximizerSoftClipper.connect(upwardSummingGain);maximizerBoostGain.connect(upwardCompressor);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;upwardSummingGain.connect(maximizerCompressor);// Setup Analysers +const maximizerBoostGain=ctx.createGain();const maximizerSoftClipper=ctx.createWaveShaper();// 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();upwardCompressor.threshold.value=-30;upwardCompressor.knee.value=10;upwardCompressor.ratio.value=4;upwardCompressor.attack.value=0.01;upwardCompressor.release.value=0.1;const upwardGain=ctx.createGain();upwardGain.gain.value=0.0;const upwardSummingGain=ctx.createGain();maximizerBoostGain.connect(maximizerSoftClipper);maximizerSoftClipper.connect(upwardSummingGain);maximizerBoostGain.connect(upwardCompressor);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;upwardSummingGain.connect(maximizerCompressor);// Setup Analysers const inputAnalyser=ctx.createAnalyser();inputAnalyser.fftSize=2048;const outputAnalyser=ctx.createAnalyser();outputAnalyser.fftSize=2048;// Global fader / output const output=ctx.createGain();output.gain.value=1.0;const analyser=ctx.createAnalyser();analyser.fftSize=256;const leftAnalyser=ctx.createAnalyser();leftAnalyser.fftSize=2048;const rightAnalyser=ctx.createAnalyser();rightAnalyser.fftSize=2048;const splitter=ctx.createChannelSplitter(2);output.connect(splitter);splitter.connect(leftAnalyser,0);splitter.connect(rightAnalyser,1);masterBus={input:ctx.createGain(),compressor:ctx.createDynamicsCompressor(),analyser,output,masteringActive:false,// Analysers for metering inputAnalyser,outputAnalyser,leftAnalyser,rightAnalyser,// Mastering nodes @@ -253,7 +261,10 @@ const MasteringModal=({isOpen,onClose,masteringSettings,setMasteringSettings})=> const woCanvasRef=React.useRef(null);const woLeftHistoryRef=React.useRef(new Float32Array(400).fill(0));const woRightHistoryRef=React.useRef(new Float32Array(400).fill(0));const woLeftMeterRef=React.useRef(null);const woRightMeterRef=React.useRef(null);const[woPaused,setWoPaused]=React.useState(false);const[woChannel,setWoChannel]=React.useState('stereo');const[woMode,setWoMode]=React.useState('waveform');const[woDuration,setWoDuration]=React.useState(2.0);const[woZoom,setWoZoom]=React.useState(0.0);const woPausedRef=React.useRef(woPaused);woPausedRef.current=woPaused;const woChannelRef=React.useRef(woChannel);woChannelRef.current=woChannel;const woModeRef=React.useRef(woMode);woModeRef.current=woMode;const woDurationRef=React.useRef(woDuration);woDurationRef.current=woDuration;const woZoomRef=React.useRef(woZoom);woZoomRef.current=woZoom;const ozStateRef=React.useRef(ozState);ozStateRef.current=ozState;function startAudioDemo(){getAudioContext();const ctx=audioCtx;if(ctx.state==='suspended')ctx.resume();stopAudioDemo();const sampleRate=ctx.sampleRate;const bufferSize=sampleRate*4;const buffer=ctx.createBuffer(2,bufferSize,sampleRate);const left=buffer.getChannelData(0);const right=buffer.getChannelData(1);for(let i=0;i{if(!isOpen)return;getAudioContext();function resizeAll(){const resizeCanvas=ref=>{const el=ref.current;if(el){el.width=el.clientWidth;el.height=el.clientHeight;}};resizeCanvas(eqCanvasRef);resizeCanvas(imagerCanvasRef);resizeCanvas(inMeterCanvasRef);resizeCanvas(outMeterCanvasRef);resizeCanvas(woCanvasRef);}resizeAll();window.addEventListener('resize',resizeAll);const fftData=new Uint8Array(1024);function getPeakLevel(analyser){if(!analyser)return 0;const bufferLength=analyser.fftSize;const dataArray=new Float32Array(bufferLength);analyser.getFloatTimeDomainData(dataArray);let maxVal=0;for(let i=0;imaxVal){maxVal=val;}}return maxVal;}function renderFrame(){animFrameRef.current=requestAnimationFrame(renderFrame);const s=ozStateRef.current;// EQ Spectrum const eqCanvas=eqCanvasRef.current;if(eqCanvas){const w=eqCanvas.width,h=eqCanvas.height;const eqCtx=eqCanvas.getContext('2d');eqCtx.clearRect(0,0,w,h);eqCtx.strokeStyle='rgba(51, 65, 85, 0.3)';eqCtx.lineWidth=1;eqCtx.font='9px JetBrains Mono';eqCtx.fillStyle='#475569';const freqs=[20,50,100,200,500,1000,2000,5000,10000,20000];freqs.forEach(f=>{const x=Math.log10(f/20)/Math.log10(20000/20)*w;eqCtx.beginPath();eqCtx.moveTo(x,0);eqCtx.lineTo(x,h);eqCtx.stroke();if(f>=1000)eqCtx.fillText(`${f/1000}k`,x+3,h-6);else eqCtx.fillText(`${f}`,x+3,h-6);});if(masterBus&&masterBus.outputAnalyser){masterBus.outputAnalyser.getByteFrequencyData(fftData);eqCtx.fillStyle='rgba(56, 189, 248, 0.15)';const barWidth=w/128;for(let i=0;i<128;i++){const val=fftData[i*4]/255;eqCtx.fillRect(i*barWidth,h-val*h,barWidth-1,val*h);}}eqCtx.strokeStyle='#38bdf8';eqCtx.lineWidth=2.5;eqCtx.beginPath();for(let x=0;x0.001){ic.fillStyle='#38bdf8';const maxRadius=ih/3.2*Math.min(1.0,outPeak*1.5);for(let i=0;i<40;i++){const angle=(Math.random()-0.5)*(Math.PI/2)+-Math.PI/2;const radius=Math.random()*maxRadius;const x=iw/2+Math.cos(angle)*radius*(1+s.w3/100);const y=ih/2+Math.sin(angle)*radius;ic.fillRect(x,y,2,2);}}}// I/O Meters -const renderMeter=(analyser,ctxRef,textId)=>{const canvas=ctxRef.current;if(!canvas)return;const w=canvas.width,h=canvas.height;const ctx=canvas.getContext('2d');ctx.clearRect(0,0,w,h);const peak=getPeakLevel(analyser);const barH=Math.min(1.0,peak)*h;const grad=ctx.createLinearGradient(0,h,0,0);grad.addColorStop(0,'#38bdf8');grad.addColorStop(0.7,'#f59e0b');grad.addColorStop(1,'#ef4444');ctx.fillStyle=grad;ctx.fillRect(2,h-barH,w-4,barH);const el=document.getElementById(textId);if(el){if(peak>0){const dbVal=20*Math.log10(peak);el.innerText=dbVal<-90?'-inf dB':`${dbVal.toFixed(1)} dB`;}else{el.innerText='-inf dB';}}};renderMeter(masterBus&&masterBus.inputAnalyser,inMeterCanvasRef,'inPeakText');renderMeter(masterBus&&masterBus.outputAnalyser,outMeterCanvasRef,'outPeakText');// Wave Observer Oscilloscope Rendering +const renderMeter=(analyser,ctxRef,textId)=>{const canvas=ctxRef.current;if(!canvas)return;const w=canvas.width,h=canvas.height;const ctx=canvas.getContext('2d');ctx.clearRect(0,0,w,h);const peak=getPeakLevel(analyser);const barH=Math.min(1.0,peak)*h;const grad=ctx.createLinearGradient(0,h,0,0);grad.addColorStop(0,'#38bdf8');grad.addColorStop(0.7,'#f59e0b');grad.addColorStop(1,'#ef4444');ctx.fillStyle=grad;ctx.fillRect(2,h-barH,w-4,barH);const el=document.getElementById(textId);if(el){if(peak>0){const dbVal=20*Math.log10(peak);el.innerText=dbVal<-90?'-inf dB':`${dbVal.toFixed(1)} dB`;}else{el.innerText='-inf dB';}}};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){const w=woCanvas.width,h=woCanvas.height;const woCtx=woCanvas.getContext('2d');woCtx.clearRect(0,0,w,h);// Draw grid woCtx.strokeStyle='rgba(51, 65, 85, 0.2)';woCtx.lineWidth=1;woCtx.font='8px JetBrains Mono, monospace';woCtx.fillStyle='#475569';const centerY=h/2;const gridLines=[-0.75,-0.5,-0.25,0,0.25,0.5,0.75];gridLines.forEach(g=>{const y=centerY+g*centerY;woCtx.beginPath();woCtx.moveTo(0,y);woCtx.lineTo(w,y);woCtx.stroke();});// Vertical lines const ticksCount=10;for(let i=1;i<=ticksCount;i++){const x=i/(ticksCount+1)*w;woCtx.beginPath();woCtx.moveTo(x,0);woCtx.lineTo(x,h);woCtx.stroke();}// dB labels on left side diff --git a/app/templates/index.html b/app/templates/index.html index 7b69ea7..7163bf7 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -24,7 +24,7 @@ - +