diff --git a/README.md b/README.md index 589b9aa..61ca930 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ A simple audio player for all your audio playing needs, based on HTML5 audio element.Supports most popular formats. -| Formats | Support | -| ------- | ------- | -| .mp3 | [✓] | -| .aac | [✓] | -| .mp4 | [✓] | -| .m3u8 (hls) | [✓] | +| Formats | Support | +| ----------- | ------- | +| .mp3 | [✓] | +| .aac | [✓] | +| .mp4 | [✓] | +| .m3u8 (hls) | [✓] | For a comprehensive list of formats support visit [MDN audio codec guide](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Audio_codecs) @@ -35,7 +35,7 @@ For a comprehensive list of formats support visit [MDN audio codec guide](https: - Casting support - Dash media playback - DRM -- Equalizer +- ~~Equalizer~~ [✓] Done - Updates to APIs for better DX - React hooks to easily get started with React. - Ads Support @@ -159,6 +159,43 @@ directly on the HTML5 audio element. const instance = AudioX.getAudioInstance(); ``` +### Setting up the equalizer + +--- + +``` +// Getting the Presets +const presets = audio.getPresets(); // will return array of pre-tuned filters + +// Sample Preset +[ + { + "id": "preset_default", + "name": "Default", + "gains": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] + } +] + +// Setting a Preset +audio.setPreset(id); + +// example: +audio.setPreset('preset_default'); // will set default preset + + +// Custom EQ Setting + +const gainsValue = preset[index].gains; +gainsValue[index] = value; // value ranges from -10 to 10 +audio.setCustomEQ(gainsValue); + +// Example +const gainsValue = preset[0].gains; // default preset gains [0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] +gainsValue[0] = 2.5; // updated gain values [2.5, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] +audio.setCustomEQ(gainsValue); + +``` + ### Author --- diff --git a/src/adapters/equalizer.ts b/src/adapters/equalizer.ts new file mode 100644 index 0000000..e4d4c1a --- /dev/null +++ b/src/adapters/equalizer.ts @@ -0,0 +1,126 @@ +import { AudioX } from 'audio'; +import { bands, presets } from 'constants/equalizer'; +import { isValidArray } from 'helpers/common'; + +import { EqualizerStatus, Preset } from 'types/equalizer.types'; + +class Equalizer { + private static _instance: Equalizer; + private audioCtx: AudioContext; + private audioCtxStatus: EqualizerStatus; + private eqFilterBands: BiquadFilterNode[]; + + constructor() { + if (Equalizer._instance) { + console.warn( + 'Instantiation failed: cannot create multiple instance of Equalizer returning existing instance' + ); + return Equalizer._instance; + } + + if (this.audioCtx === undefined && typeof AudioContext !== 'undefined') { + if (typeof AudioContext !== 'undefined') { + this.audioCtx = new AudioContext(); + this.audioCtxStatus = 'ACTIVE'; + this.init(); + } else if (typeof (window as any).webkitAudioContext !== 'undefined') { + this.audioCtx = new (window as any).webkitAudioContext(); + this.audioCtxStatus = 'ACTIVE'; + this.init(); + } else { + throw new Error('Web Audio API is not supported in this browser.'); + } + } else { + console.log('Equalizer not initialized, AudioContext failed'); + this.audioCtxStatus = 'FAILED'; + } + + // context state at this time is `undefined` in iOS8 Safari + if ( + this.audioCtxStatus === 'ACTIVE' && + this.audioCtx.state === 'suspended' + ) { + var resume = () => { + this.audioCtx.resume(); + setTimeout(() => { + if (this.audioCtx.state === 'running') { + document.body.removeEventListener('click', resume, false); + } + }, 0); + }; + + document.body.addEventListener('click', resume, false); + } + + Equalizer._instance = this; + } + + init() { + try { + const audioInstance = AudioX.getAudioInstance(); + const audioSource = this.audioCtx.createMediaElementSource(audioInstance); + + const equalizerBands = bands.map((band) => { + const filter = this.audioCtx.createBiquadFilter(); + filter.type = band.type; + filter.frequency.value = band.frequency; + filter.gain.value = band.gain; + filter.Q.value = 1; + return filter; + }); + + const gainNode = this.audioCtx.createGain(); + gainNode.gain.value = 1; //Normalize sound output + + audioSource.connect(equalizerBands[0]); + + for (let i = 0; i < equalizerBands.length - 1; i++) { + equalizerBands[i].connect(equalizerBands[i + 1]); + } + + equalizerBands[equalizerBands.length - 1].connect(gainNode); + gainNode.connect(this.audioCtx.destination); + + this.audioCtxStatus = 'ACTIVE'; + this.eqFilterBands = equalizerBands; + } catch (error) { + this.audioCtxStatus = 'FAILED'; + } + } + + setPreset(id: keyof Preset) { + const preset = presets.find((el) => el.id === id); + console.log({ preset }); + if ( + !this.eqFilterBands || + this.eqFilterBands.length !== preset?.gains.length + ) { + console.error('Invalid data provided.'); + return; + } + for (let i = 0; i < this.eqFilterBands.length; i++) { + this.eqFilterBands[i].gain.value = preset?.gains[i]; + } + } + + static getPresets() { + return presets; + } + + status() { + if (this.audioCtx.state === 'suspended') { + this.audioCtx.resume(); + } + return this.audioCtxStatus; + } + + setCustomEQ(gains: number[]) { + if (isValidArray(gains)) { + this.eqFilterBands.forEach((band: BiquadFilterNode, index: number) => { + band.gain.value = gains[index]; + }); + } + } +} + +export { Equalizer }; diff --git a/src/audio.ts b/src/audio.ts index 96023a5..da226cd 100644 --- a/src/audio.ts +++ b/src/audio.ts @@ -1,3 +1,4 @@ +import { Equalizer } from 'adapters/equalizer'; import HlsAdapter from 'adapters/hls'; import { AUDIO_X_CONSTANTS, PLAYBACK_STATE } from 'constants/common'; import { BASE_EVENT_CALLBACK_MAP } from 'events/baseEvents'; @@ -15,6 +16,7 @@ import { import { READY_STATE } from 'states/audioState'; import { EventListenersList } from 'types'; import { AudioInit, MediaTrack, PlaybackRate } from 'types/audio.types'; +import { EqualizerStatus, Preset } from 'types/equalizer.types'; let audioInstance: HTMLAudioElement; const notifier = ChangeNotifier; @@ -23,6 +25,9 @@ class AudioX { private _audio: HTMLAudioElement; private isPlayLogEnabled: Boolean; private static _instance: AudioX; + private eqStatus: EqualizerStatus = 'IDEAL'; + private isEqEnabled: boolean = false; + private eqInstance: Equalizer; constructor() { if (AudioX._instance) { @@ -40,6 +45,7 @@ class AudioX { AudioX._instance = this; this._audio = new Audio(); + audioInstance = this._audio; } /** @@ -72,14 +78,16 @@ class AudioX { showNotificationActions = false, enablePlayLog = false, enableHls = false, + enableEQ = false, + crossOrigin = 'anonymous', hlsConfig = {} } = initProps; this._audio?.setAttribute('id', 'audio_x_instance'); this._audio.preload = preloadStrategy; this._audio.autoplay = autoPlay; + this._audio.crossOrigin = crossOrigin; this.isPlayLogEnabled = enablePlayLog; - audioInstance = this._audio; if (useDefaultEventListeners || customEventListeners == null) { attachDefaultEventListeners(BASE_EVENT_CALLBACK_MAP, enablePlayLog); @@ -89,6 +97,10 @@ class AudioX { attachMediaSessionHandlers(); } + if (enableEQ) { + this.isEqEnabled = enableEQ; + } + if (enableHls) { const hls = new HlsAdapter(); hls.init(hlsConfig, enablePlayLog); @@ -132,6 +144,18 @@ class AudioX { audioInstance.load(); } + attachEq() { + if (this.isEqEnabled && this.eqStatus === 'IDEAL') { + try { + const eq = new Equalizer(); + this.eqStatus = eq.status(); + this.eqInstance = eq; + } catch (e) { + console.log('failed to enable equalizer'); + } + } + } + async play() { const isSourceAvailable = audioInstance.src !== ''; if ( @@ -164,6 +188,7 @@ class AudioX { this.addMedia(mediaTrack).then(() => { if (audioInstance.HAVE_ENOUGH_DATA === READY_STATE.HAVE_ENOUGH_DATA) { setTimeout(async () => { + this.attachEq(); await this.play(); }, 950); } @@ -251,6 +276,18 @@ class AudioX { attachCustomEventListeners(eventListenersList); } + getPresets() { + return Equalizer.getPresets(); + } + + setPreset(id: keyof Preset) { + this.eqInstance.setPreset(id); + } + + setCustomEQ(gains: number[]) { + this.eqInstance.setCustomEQ(gains); + } + get id() { return audioInstance?.getAttribute('id'); } diff --git a/src/constants/equalizer.ts b/src/constants/equalizer.ts new file mode 100644 index 0000000..0aa6f09 --- /dev/null +++ b/src/constants/equalizer.ts @@ -0,0 +1,199 @@ +import { Band, Preset } from 'types/equalizer.types'; + +const bands: Band[] = [ + { frequency: 31, type: 'lowshelf', gain: 0 }, // Band 0: 31 Hz - Low Shelf Filter + { frequency: 63, type: 'peaking', gain: 0 }, // Band 1: 63 Hz - Peaking Filter + { frequency: 125, type: 'peaking', gain: 0 }, // Band 2: 125 Hz - Peaking Filter + { frequency: 250, type: 'peaking', gain: 0 }, // Band 3: 250 Hz - Peaking Filter + { frequency: 500, type: 'peaking', gain: 0 }, // Band 4: 500 Hz - Peaking Filter + { frequency: 1000, type: 'peaking', gain: 0 }, // Band 5: 1 kHz - Peaking Filter + { frequency: 2000, type: 'peaking', gain: 0 }, // Band 6: 2 kHz - Peaking Filter + { frequency: 4000, type: 'peaking', gain: 0 }, // Band 7: 4 kHz - Peaking Filter + { frequency: 8000, type: 'peaking', gain: 0 }, // Band 8: 8 kHz - Peaking Filter + { frequency: 16000, type: 'highshelf', gain: 0 } // Band 9: 16 kHz - High Shelf Filter +]; + +const presets: Preset[] = [ + { + id: 'preset_default', + name: 'Default', + gains: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + }, + { + id: 'preset_live', + name: 'Live', + gains: [-1.0, 1.0, 3.0, 4.0, 4.0, 4.0, 3.0, 2.0, 2.0, 2.0] + }, + { + id: 'preset_acoustic', + name: 'Acoustic', + gains: [6.0, 6.0, 4.0, 1.0, 3.0, 3.0, 4.0, 5.0, 4.0, 1.5] + }, + { + id: 'preset_classical', + name: 'Classical', + gains: [6.0, 5.0, 4.0, 3.0, -1.0, -1.0, 0.0, 2.0, 4.0, 5.0] + }, + { + id: 'preset_piano', + name: 'Piano', + gains: [4.0, 2.0, 0.0, 3.5, 4.0, 1.5, 5.0, 6.0, 4.0, 4.5] + }, + { + id: 'preset_lounge', + name: 'Lounge', + gains: [-3.0, -1.5, 0.0, 1.0, 5.5, 1.0, 0.0, -1.5, 2.0, 0.5] + }, + { + id: 'preset_spoken_word', + name: 'Spoken Word', + gains: [-2.0, 0.0, 0.0, 1.0, 5.0, 6.5, 7.0, 6.0, 3.0, 0.0] + }, + { + id: 'preset_jazz', + name: 'Jazz', + gains: [5.5, 4.0, 1.0, 2.0, -1.5, -1.5, 0.0, 1.0, 4.0, 5.5] + }, + { + id: 'preset_pop', + name: 'Pop', + gains: [0.5, 2.4, 3.8, 4.3, 3.0, 0.0, -0.5, -0.5, 0.5, 0.5] + }, + { + id: 'preset_dance', + name: 'Dance', + gains: [5.0, 10.0, 6.5, 0.0, 2.0, 4.5, 7.5, 7.0, 5.5, 0.0] + }, + { + id: 'preset_latin', + name: 'Latin', + gains: [3.5, 1.5, 0.0, 0.0, -1.5, -1.5, -1.5, 0.0, 4.0, 6.5] + }, + { + id: 'preset_rnb', + name: 'RnB', + gains: [3.5, 10.5, 8.5, 1.0, -3.0, -1.5, 3.0, 3.5, 4.0, 5.0] + }, + { + id: 'preset_hiphop', + name: 'HipHop', + gains: [7.0, 6.0, 1.0, 4.0, -1.0, -0.5, 1.0, -0.5, 2.0, 4.0] + }, + { + id: 'preset_electronic', + name: 'Electronic', + gains: [6.0, 5.5, 1.0, 0.0, -2.0, 2.0, 1.0, 1.5, 5.5, 6.5] + }, + { + id: 'preset_techno', + name: 'Techno', + gains: [3.8, 2.4, 0.0, -2.4, -1.9, 0.0, 3.8, 4.8, 4.8, 4.3] + }, + { + id: 'preset_deep', + name: 'Deep', + gains: [6.0, 5.0, 1.5, 0.5, 4.0, 3.0, 1.5, -2.0, -5.0, -6.5] + }, + { + id: 'preset_club', + name: 'Club', + gains: [0.0, 0.0, 3.8, 2.4, 2.4, 2.4, 1.0, 0.0, 0.0, 0.0] + }, + { + id: 'preset_rock', + name: 'Rock', + gains: [7.0, 5.5, 4.0, 1.0, -0.5, 0.0, 0.5, 3.0, 4.5, 6.5] + }, + { + id: 'preset_rock_soft', + name: 'Rock Soft', + gains: [1.5, 0.0, 0.0, -0.5, 0.0, 1.0, 3.8, 4.8, 5.7, 6.2] + }, + { + id: 'preset_ska', + name: 'Ska', + gains: [-0.5, -1.5, -1.0, 0.0, 1.0, 2.0, 3.8, 4.3, 5.2, 4.3] + }, + { + id: 'preset_reggae', + name: 'Reggae', + gains: [0.0, 0.0, 0.0, -2.4, 0.0, 2.5, 2.5, 0.0, 0.0, 0.0] + }, + { + id: 'preset_country', + name: 'Country', + gains: [3.0, 2.0, 1.0, 0.0, -1.0, 0.0, 2.0, 3.0, 4.0, 4.0] + }, + { + id: 'preset_funk', + name: 'Funk', + gains: [4.0, 5.0, 3.0, 0.0, -1.0, 0.0, 2.0, 4.0, 5.0, 5.0] + }, + { + id: 'preset_blues', + name: 'Blues', + gains: [2.0, 1.0, 0.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 3.0] + }, + { + id: 'preset_metal', + name: 'Metal', + gains: [8.0, 7.0, 6.0, 4.0, 2.0, 1.0, 0.0, 2.0, 4.0, 6.0] + }, + { + id: 'preset_indie', + name: 'Indie', + gains: [2.0, 3.0, 2.0, 1.0, 0.0, -1.0, -2.0, 0.0, 3.0, 4.0] + }, + { + id: 'preset_chill', + name: 'Chill', + gains: [1.0, 1.0, 0.0, -1.0, -2.0, -1.0, 1.0, 2.0, 3.0, 2.0] + }, + { + id: 'preset_world', + name: 'World', + gains: [3.0, 2.0, 0.0, -2.0, -1.0, 1.0, 3.0, 4.0, 5.0, 3.0] + }, + { + id: 'preset_alternative', + name: 'Alternative', + gains: [3.0, 2.0, 1.0, 0.0, -1.0, -2.0, 1.0, 3.0, 4.0, 3.0] + }, + { + id: 'preset_ambient', + name: 'Ambient', + gains: [0.0, -1.0, -2.0, -3.0, -2.0, 0.0, 1.0, 2.0, 3.0, 2.0] + }, + { + id: 'preset_mellow', + name: 'Mellow', + gains: [1.0, 1.0, 0.0, -1.0, -2.0, -1.0, 1.0, 2.0, 3.0, 1.0] + }, + { + id: 'preset_grunge', + name: 'Grunge', + gains: [5.0, 4.0, 3.0, 2.0, 1.0, 0.0, 0.0, 2.0, 4.0, 5.0] + }, + { + id: 'preset_soul', + name: 'Soul', + gains: [3.0, 3.0, 2.0, 1.0, 0.0, -1.0, 0.0, 2.0, 3.0, 3.0] + }, + { + id: 'preset_folk', + name: 'Folk', + gains: [2.0, 1.0, 0.0, -1.0, -2.0, -1.0, 1.0, 2.0, 3.0, 2.0] + }, + { + id: 'preset_trap', + name: 'Trap', + gains: [7.0, 6.0, 3.0, 1.0, -2.0, -1.0, 1.0, 3.0, 6.0, 7.0] + }, + { + id: 'preset_dubstep', + name: 'Dubstep', + gains: [6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 3.0, 5.0, 6.0] + } +]; + +export { bands, presets }; diff --git a/src/index.ts b/src/index.ts index 63353ac..51410c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,8 @@ import type { AudioEvents, AudioInit, AudioState, + Band, + EqualizerStatus, ErrorEvents, EventListenerCallbackMap, EventListenersList, @@ -16,7 +18,8 @@ import type { NetworkState, PlayBackState, PlaybackRate, - ReadyState, + Preset, + ReadyState } from 'types'; export { @@ -28,6 +31,8 @@ export { AudioInit, AudioState, AudioX, + Band, + EqualizerStatus, ErrorEvents, EventListenerCallbackMap, EventListenersList, @@ -37,5 +42,6 @@ export { NetworkState, PlayBackState, PlaybackRate, - ReadyState, + Preset, + ReadyState }; diff --git a/src/types/audio.types.ts b/src/types/audio.types.ts index 9c63e3a..3069e7c 100644 --- a/src/types/audio.types.ts +++ b/src/types/audio.types.ts @@ -1,5 +1,5 @@ +import { HlsConfig } from 'types/hls.js'; import { EventListenerCallbackMap } from './audioEvents.types'; -import { HlsConfig } from './hls.js'; export type InitMode = 'REACT' | 'VANILLA'; export type PlaybackRate = 1.0 | 1.25 | 1.5 | 1.75 | 2.0 | 2.5 | 3.0; @@ -37,6 +37,8 @@ export interface AudioInit { autoPlay?: boolean; enablePlayLog?: boolean; enableHls?: boolean; + enableEQ?: boolean; + crossOrigin?: string; hlsConfig?: HlsConfig | {}; } diff --git a/src/types/equalizer.types.ts b/src/types/equalizer.types.ts new file mode 100644 index 0000000..c5cfa14 --- /dev/null +++ b/src/types/equalizer.types.ts @@ -0,0 +1,13 @@ +export interface Band { + frequency: number; + type: BiquadFilterType; + gain: number; +} + +export interface Preset { + id: string | number; + name: string; + gains: number[]; +} + +export type EqualizerStatus = 'ACTIVE' | 'FAILED' | 'IDEAL'; diff --git a/src/types/index.ts b/src/types/index.ts index 32229f6..c2e30a9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,14 +6,15 @@ import { MediaArtwork, MediaTrack, PlaybackRate, - PlayBackState, + PlayBackState } from './audio.types'; import { AudioEvents, EventListenerCallbackMap, - EventListenersList, + EventListenersList } from './audioEvents.types'; import { ReadyState } from './audioState.types'; +import { Band, EqualizerStatus, Preset } from './equalizer.types'; import { ErrorEvents } from './errorEvents.types'; import { NetworkState } from './networkState.types'; @@ -22,6 +23,8 @@ export type { AudioEvents, AudioInit, AudioState, + Band, + EqualizerStatus, ErrorEvents, EventListenerCallbackMap, EventListenersList, @@ -31,5 +34,6 @@ export type { NetworkState, PlaybackRate, PlayBackState, - ReadyState, + Preset, + ReadyState };