170 lines
4.8 KiB
TypeScript
170 lines
4.8 KiB
TypeScript
declare global {
|
|
interface Window {
|
|
webkitAudioContext: typeof AudioContext;
|
|
}
|
|
}
|
|
|
|
export interface AudioEffectPreset {
|
|
name: string;
|
|
gains: number[]; // Gains for different frequency bands
|
|
frequencies: number[]; // Center frequencies for each band
|
|
}
|
|
|
|
export const presets: { [key: string]: AudioEffectPreset } = {
|
|
normal: {
|
|
name: "Normal",
|
|
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
frequencies: [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
|
|
},
|
|
bassBoost: {
|
|
name: "Bass Boost",
|
|
gains: [7, 5, 3, 2, 0, 0, 0, 0, 0, 0],
|
|
frequencies: [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
|
|
},
|
|
trebleBoost: {
|
|
name: "Treble Boost",
|
|
gains: [0, 0, 0, 0, 0, 0, 2, 3, 5, 7],
|
|
frequencies: [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
|
|
},
|
|
vocalBoost: {
|
|
name: "Vocal Boost",
|
|
gains: [0, 0, 0, 2, 4, 4, 2, 0, 0, 0],
|
|
frequencies: [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
|
|
}
|
|
};
|
|
|
|
export class AudioEffects {
|
|
private context: AudioContext;
|
|
private source: MediaElementAudioSourceNode | null = null;
|
|
private destination: AudioDestinationNode;
|
|
private filters: BiquadFilterNode[] = [];
|
|
private gainNode: GainNode;
|
|
private crossfadeGainNode: GainNode;
|
|
private analyser: AnalyserNode;
|
|
private replayGainNode: GainNode;
|
|
private currentPreset: string = 'normal';
|
|
|
|
constructor(audioElement: HTMLAudioElement) {
|
|
// Properly type the AudioContext initialization
|
|
this.context = new (window.AudioContext || window.webkitAudioContext || AudioContext)();
|
|
this.destination = this.context.destination;
|
|
this.gainNode = this.context.createGain();
|
|
this.crossfadeGainNode = this.context.createGain();
|
|
this.analyser = this.context.createAnalyser();
|
|
this.replayGainNode = this.context.createGain();
|
|
|
|
// Initialize ReplayGain node
|
|
this.replayGainNode.gain.value = 1.0;
|
|
|
|
// Create the audio processing chain
|
|
this.setupAudioChain(audioElement);
|
|
|
|
// Initialize EQ filters
|
|
this.setupEqualizer();
|
|
}
|
|
|
|
private setupAudioChain(audioElement: HTMLAudioElement) {
|
|
// Disconnect any existing source
|
|
if (this.source) {
|
|
this.source.disconnect();
|
|
}
|
|
|
|
// Create new source from audio element
|
|
this.source = this.context.createMediaElementSource(audioElement);
|
|
|
|
// Connect the audio processing chain
|
|
this.source
|
|
.connect(this.replayGainNode)
|
|
.connect(this.gainNode)
|
|
.connect(this.crossfadeGainNode);
|
|
|
|
// Connect filters in series
|
|
let lastNode: AudioNode = this.crossfadeGainNode;
|
|
this.filters.forEach(filter => {
|
|
lastNode.connect(filter);
|
|
lastNode = filter;
|
|
});
|
|
|
|
// Connect to analyser and destination
|
|
lastNode.connect(this.analyser);
|
|
this.analyser.connect(this.destination);
|
|
}
|
|
|
|
private setupEqualizer() {
|
|
// Create 10-band EQ
|
|
presets.normal.frequencies.forEach((freq, index) => {
|
|
const filter = this.context.createBiquadFilter();
|
|
filter.type = 'peaking';
|
|
filter.frequency.value = freq;
|
|
filter.Q.value = 1.0;
|
|
filter.gain.value = 0;
|
|
this.filters.push(filter);
|
|
});
|
|
}
|
|
|
|
public setPreset(presetName: string) {
|
|
if (presets[presetName]) {
|
|
this.currentPreset = presetName;
|
|
presets[presetName].gains.forEach((gain, index) => {
|
|
if (this.filters[index]) {
|
|
this.filters[index].gain.setValueAtTime(gain, this.context.currentTime);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public getCurrentPreset(): string {
|
|
return this.currentPreset;
|
|
}
|
|
|
|
public setVolume(volume: number) {
|
|
if (this.gainNode) {
|
|
this.gainNode.gain.setValueAtTime(volume, this.context.currentTime);
|
|
}
|
|
}
|
|
|
|
public setCrossfadeTime(seconds: number) {
|
|
if (this.crossfadeGainNode) {
|
|
const now = this.context.currentTime;
|
|
this.crossfadeGainNode.gain.setValueAtTime(1, now);
|
|
this.crossfadeGainNode.gain.linearRampToValueAtTime(0, now + seconds);
|
|
}
|
|
}
|
|
|
|
public startCrossfade() {
|
|
if (this.crossfadeGainNode) {
|
|
this.crossfadeGainNode.gain.value = 1;
|
|
}
|
|
}
|
|
|
|
public setReplayGain(gain: number) {
|
|
if (this.replayGainNode) {
|
|
// Clamp gain between -12dB and +12dB for safety
|
|
const clampedGain = Math.max(-12, Math.min(12, gain));
|
|
const gainValue = Math.pow(10, clampedGain / 20); // Convert dB to linear gain
|
|
this.replayGainNode.gain.setValueAtTime(gainValue, this.context.currentTime);
|
|
}
|
|
}
|
|
|
|
public getAnalyserNode(): AnalyserNode {
|
|
return this.analyser;
|
|
}
|
|
|
|
public async resume() {
|
|
if (this.context.state === 'suspended') {
|
|
await this.context.resume();
|
|
}
|
|
}
|
|
|
|
public disconnect() {
|
|
if (this.source) {
|
|
this.source.disconnect();
|
|
}
|
|
this.filters.forEach(filter => filter.disconnect());
|
|
this.gainNode.disconnect();
|
|
this.crossfadeGainNode.disconnect();
|
|
this.analyser.disconnect();
|
|
this.replayGainNode.disconnect();
|
|
}
|
|
}
|