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() {
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);