From 969da9a579c34438a5f97b7c57de022b0a92fb4e Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Mon, 5 Jan 2026 21:23:53 -0600 Subject: [PATCH] feat(audio): better ToneGenerator with high-quality audio processing, including dynamics compression, lowpass filtering, and improved stereo separation --- meshchatx/src/frontend/js/ToneGenerator.js | 103 +++++++++++++++++---- 1 file changed, 85 insertions(+), 18 deletions(-) diff --git a/meshchatx/src/frontend/js/ToneGenerator.js b/meshchatx/src/frontend/js/ToneGenerator.js index 352785d..06cd484 100644 --- a/meshchatx/src/frontend/js/ToneGenerator.js +++ b/meshchatx/src/frontend/js/ToneGenerator.js @@ -19,6 +19,22 @@ export default class ToneGenerator { _initAudioContext() { if (!this.audioCtx) { this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + + // Create a high-quality output chain + this.masterCompressor = this.audioCtx.createDynamicsCompressor(); + this.masterCompressor.threshold.setValueAtTime(-24, this.audioCtx.currentTime); + this.masterCompressor.knee.setValueAtTime(30, this.audioCtx.currentTime); + this.masterCompressor.ratio.setValueAtTime(12, this.audioCtx.currentTime); + this.masterCompressor.attack.setValueAtTime(0.003, this.audioCtx.currentTime); + this.masterCompressor.release.setValueAtTime(0.25, this.audioCtx.currentTime); + + this.masterFilter = this.audioCtx.createBiquadFilter(); + this.masterFilter.type = "lowpass"; + this.masterFilter.frequency.setValueAtTime(4000, this.audioCtx.currentTime); + this.masterFilter.Q.setValueAtTime(0.7, this.audioCtx.currentTime); + + this.masterCompressor.connect(this.masterFilter); + this.masterFilter.connect(this.audioCtx.destination); } } @@ -31,31 +47,63 @@ export default class ToneGenerator { const play = () => { const osc1 = this.audioCtx.createOscillator(); const osc2 = this.audioCtx.createOscillator(); + const sub = this.audioCtx.createOscillator(); + + const panner1 = this.audioCtx.createStereoPanner(); + const panner2 = this.audioCtx.createStereoPanner(); const gain = this.audioCtx.createGain(); + const subGain = this.audioCtx.createGain(); - osc1.frequency.value = 440; - osc2.frequency.value = 480; - gain.gain.value = this.volume; + // Main frequencies + osc1.type = "sine"; + osc1.frequency.setValueAtTime(440, this.audioCtx.currentTime); + + osc2.type = "sine"; + osc2.frequency.setValueAtTime(480, this.audioCtx.currentTime); - osc1.connect(gain); - osc2.connect(gain); - gain.connect(this.audioCtx.destination); + // Sub layer for depth and "feel" + sub.type = "sine"; + sub.frequency.setValueAtTime(40, this.audioCtx.currentTime); + subGain.gain.setValueAtTime(0.05, this.audioCtx.currentTime); + + // Stereo separation for quality headphones + panner1.pan.setValueAtTime(-0.4, this.audioCtx.currentTime); + panner2.pan.setValueAtTime(0.4, this.audioCtx.currentTime); + + osc1.connect(panner1); + panner1.connect(gain); + osc2.connect(panner2); + panner2.connect(gain); + + sub.connect(subGain); + subGain.connect(gain); + + gain.gain.setValueAtTime(0, this.audioCtx.currentTime); + gain.gain.linearRampToValueAtTime(this.volume, this.audioCtx.currentTime + 0.1); + + gain.connect(this.masterCompressor); osc1.start(); osc2.start(); + sub.start(); - this.oscillator = [osc1, osc2]; + this.oscillator = [osc1, osc2, sub]; this.gainNode = gain; - // Stop after 2 seconds + // Stop after 2 seconds with a smooth fade-out setTimeout(() => { - if (this.oscillator === osc1 || (Array.isArray(this.oscillator) && this.oscillator.includes(osc1))) { + if (Array.isArray(this.oscillator) && this.oscillator.includes(osc1)) { gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.5); setTimeout(() => { osc1.stop(); osc2.stop(); + sub.stop(); osc1.disconnect(); osc2.disconnect(); + sub.disconnect(); + subGain.disconnect(); + panner1.disconnect(); + panner2.disconnect(); gain.disconnect(); }, 500); } @@ -76,25 +124,44 @@ export default class ToneGenerator { const play = () => { const osc = this.audioCtx.createOscillator(); + const sub = this.audioCtx.createOscillator(); const gain = this.audioCtx.createGain(); + const subGain = this.audioCtx.createGain(); - osc.frequency.value = 480; - gain.gain.value = this.volume; + osc.type = "sine"; + osc.frequency.setValueAtTime(480, this.audioCtx.currentTime); + + sub.type = "sine"; + sub.frequency.setValueAtTime(40, this.audioCtx.currentTime); + subGain.gain.setValueAtTime(0.05, this.audioCtx.currentTime); osc.connect(gain); - gain.connect(this.audioCtx.destination); + sub.connect(subGain); + subGain.connect(gain); + + gain.gain.setValueAtTime(0, this.audioCtx.currentTime); + gain.gain.linearRampToValueAtTime(this.volume, this.audioCtx.currentTime + 0.05); + + gain.connect(this.masterCompressor); osc.start(); + sub.start(); - this.oscillator = osc; + this.oscillator = [osc, sub]; this.gainNode = gain; - // Stop after 0.5 seconds + // Stop after 0.5 seconds with a short fade setTimeout(() => { - if (this.oscillator === osc) { - osc.stop(); - osc.disconnect(); - gain.disconnect(); + if (Array.isArray(this.oscillator) && this.oscillator.includes(osc)) { + gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.1); + setTimeout(() => { + osc.stop(); + sub.stop(); + osc.disconnect(); + sub.disconnect(); + subGain.disconnect(); + gain.disconnect(); + }, 100); } }, 500);