feat: Enhance audio settings with ReplayGain, crossfade, and equalizer presets; add AudioSettingsDialog component
This commit is contained in:
169
lib/audio-effects.ts
Normal file
169
lib/audio-effects.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ export interface Song {
|
||||
artistId: string;
|
||||
type: string;
|
||||
starred?: string;
|
||||
replayGain?: number;
|
||||
}
|
||||
|
||||
export interface Playlist {
|
||||
|
||||
16
lib/utils.ts
16
lib/utils.ts
@@ -4,3 +4,19 @@ import { twMerge } from "tailwind-merge"
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function constrain(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number, decimals: number = 2): string {
|
||||
if (!+bytes) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user