feat(audio): better ToneGenerator with high-quality audio processing, including dynamics compression, lowpass filtering, and improved stereo separation
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user