Files
mice/lib/audio-effects.ts

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