feat(audio): better ToneGenerator with high-quality audio processing, including dynamics compression, lowpass filtering, and improved stereo separation

This commit is contained in:
2026-01-05 21:23:53 -06:00
parent f0e567fe8a
commit 969da9a579

View File

@@ -19,6 +19,22 @@ export default class ToneGenerator {
_initAudioContext() { _initAudioContext() {
if (!this.audioCtx) { if (!this.audioCtx) {
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 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 play = () => {
const osc1 = this.audioCtx.createOscillator(); const osc1 = this.audioCtx.createOscillator();
const osc2 = 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 gain = this.audioCtx.createGain();
const subGain = this.audioCtx.createGain();
osc1.frequency.value = 440; // Main frequencies
osc2.frequency.value = 480; osc1.type = "sine";
gain.gain.value = this.volume; osc1.frequency.setValueAtTime(440, this.audioCtx.currentTime);
osc2.type = "sine";
osc2.frequency.setValueAtTime(480, this.audioCtx.currentTime);
osc1.connect(gain); // Sub layer for depth and "feel"
osc2.connect(gain); sub.type = "sine";
gain.connect(this.audioCtx.destination); 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(); osc1.start();
osc2.start(); osc2.start();
sub.start();
this.oscillator = [osc1, osc2]; this.oscillator = [osc1, osc2, sub];
this.gainNode = gain; this.gainNode = gain;
// Stop after 2 seconds // Stop after 2 seconds with a smooth fade-out
setTimeout(() => { 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); gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.5);
setTimeout(() => { setTimeout(() => {
osc1.stop(); osc1.stop();
osc2.stop(); osc2.stop();
sub.stop();
osc1.disconnect(); osc1.disconnect();
osc2.disconnect(); osc2.disconnect();
sub.disconnect();
subGain.disconnect();
panner1.disconnect();
panner2.disconnect();
gain.disconnect(); gain.disconnect();
}, 500); }, 500);
} }
@@ -76,25 +124,44 @@ export default class ToneGenerator {
const play = () => { const play = () => {
const osc = this.audioCtx.createOscillator(); const osc = this.audioCtx.createOscillator();
const sub = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain(); const gain = this.audioCtx.createGain();
const subGain = this.audioCtx.createGain();
osc.frequency.value = 480; osc.type = "sine";
gain.gain.value = this.volume; 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); 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(); osc.start();
sub.start();
this.oscillator = osc; this.oscillator = [osc, sub];
this.gainNode = gain; this.gainNode = gain;
// Stop after 0.5 seconds // Stop after 0.5 seconds with a short fade
setTimeout(() => { setTimeout(() => {
if (this.oscillator === osc) { if (Array.isArray(this.oscillator) && this.oscillator.includes(osc)) {
osc.stop(); gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.1);
osc.disconnect(); setTimeout(() => {
gain.disconnect(); osc.stop();
sub.stop();
osc.disconnect();
sub.disconnect();
subGain.disconnect();
gain.disconnect();
}, 100);
} }
}, 500); }, 500);