Skip to content

Commit

Permalink
Merge pull request #23 from afkcodes/feature/equalizer
Browse files Browse the repository at this point in the history
Feature/equalizer
  • Loading branch information
afkcodes authored Dec 23, 2023
2 parents 383c86c + 253849b commit 46ec051
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 14 deletions.
51 changes: 44 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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

---
Expand Down
126 changes: 126 additions & 0 deletions src/adapters/equalizer.ts
Original file line number Diff line number Diff line change
@@ -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 };
39 changes: 38 additions & 1 deletion src/audio.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -40,6 +45,7 @@ class AudioX {

AudioX._instance = this;
this._audio = new Audio();
audioInstance = this._audio;
}

/**
Expand Down Expand Up @@ -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);
Expand All @@ -89,6 +97,10 @@ class AudioX {
attachMediaSessionHandlers();
}

if (enableEQ) {
this.isEqEnabled = enableEQ;
}

if (enableHls) {
const hls = new HlsAdapter();
hls.init(hlsConfig, enablePlayLog);
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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');
}
Expand Down
Loading

0 comments on commit 46ec051

Please sign in to comment.