Skip to content

Commit

Permalink
Merge pull request #48 from afkcodes/feature/eq-config-updates
Browse files Browse the repository at this point in the history
Feature/eq config updates
  • Loading branch information
afkcodes authored Sep 6, 2024
2 parents 093aa3e + 3c0c7da commit 9e3af64
Show file tree
Hide file tree
Showing 13 changed files with 238 additions and 57 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# audio_x

![NPM Version](https://img.shields.io/npm/v/audio_x) ![NPM Downloads](https://img.shields.io/npm/dm/audio_x)

---

A simple audio player for all your audio playing needs, based on the HTML5 audio element. Supports most popular formats.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "audio_x",
"version": "1.0.9",
"version": "1.0.10-beta.13",
"description": "The audio player for the gen-x",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
111 changes: 98 additions & 13 deletions src/adapters/equalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class Equalizer {
private audioCtx: AudioContext;
private audioCtxStatus: EqualizerStatus;
private eqFilterBands: BiquadFilterNode[];
private bassBoostFilter: BiquadFilterNode;
private compressor: DynamicsCompressorNode;

/**
* Creates an instance of Equalizer or returns the existing instance.
Expand All @@ -31,10 +33,15 @@ class Equalizer {
* @private
*/
private initializeAudioContext() {
const audioContextOptions = { latencyHint: 'playback' };
if (typeof AudioContext !== 'undefined') {
this.audioCtx = new AudioContext();
this.audioCtx = new AudioContext(
audioContextOptions as AudioContextOptions
);
} else if (typeof (window as any).webkitAudioContext !== 'undefined') {
this.audioCtx = new (window as any).webkitAudioContext();
this.audioCtx = new (window as any).webkitAudioContext(
audioContextOptions
);
} else {
console.error('Web Audio API is not supported in this browser.');
}
Expand Down Expand Up @@ -77,21 +84,32 @@ class Equalizer {
filter.type = band.type;
filter.frequency.value = band.frequency;
filter.gain.value = band.gain;
filter.Q.value = 1;
filter.Q.value = band.q || 1; // Use a default Q of 1 if not specified
return filter;
});

const gainNode = this.audioCtx.createGain();
gainNode.gain.value = 1; // TODO: Normalize sound output

// Create a compressor for overall dynamic control
this.compressor = this.audioCtx.createDynamicsCompressor();
this.compressor.threshold.value = -24;
this.compressor.knee.value = 30;
this.compressor.ratio.value = 12;
this.compressor.attack.value = 0.003;
this.compressor.release.value = 0.25;

// Create the bass boost filter
this.bassBoostFilter = this.audioCtx.createBiquadFilter();
this.bassBoostFilter.type = 'lowshelf';
this.bassBoostFilter.frequency.value = 100;
this.bassBoostFilter.gain.value = 0;

// Connect the nodes
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);
equalizerBands[equalizerBands.length - 1].connect(this.bassBoostFilter);
this.bassBoostFilter.connect(this.compressor);
this.compressor.connect(this.audioCtx.destination);

this.audioCtxStatus = 'ACTIVE';
this.eqFilterBands = equalizerBands;
Expand Down Expand Up @@ -120,8 +138,10 @@ class Equalizer {
return;
}

const currentTime = this.audioCtx.currentTime;
this.eqFilterBands.forEach((band, index) => {
band.gain.value = preset.gains[index];
const targetGain = preset.gains[index];
band.gain.setTargetAtTime(targetGain, currentTime, 0.05);
});
}

Expand Down Expand Up @@ -149,14 +169,79 @@ class Equalizer {
* @param {number[]} gains - The gain values for each band.
*/
setCustomEQ(gains: number[]) {
if (isValidArray(gains)) {
if (isValidArray(gains) && gains.length === this.eqFilterBands.length) {
const currentTime = this.audioCtx.currentTime;
this.eqFilterBands.forEach((band: BiquadFilterNode, index: number) => {
band.gain.value = gains[index];
band.gain.setTargetAtTime(gains[index], currentTime, 0.05);
});
} else {
console.error('Invalid array of gains provided.');
}
}

/**
* Enables or disables bass boost.
* @param {boolean} enable - Whether to enable or disable bass boost.
* @param {number} gain - The gain value for bass boost.
*/
setBassBoost(enable: boolean, gain: number = 6) {
const currentTime = this.audioCtx.currentTime;
if (enable) {
this.bassBoostFilter.gain.setTargetAtTime(gain, currentTime, 0.05);
} else {
this.bassBoostFilter.gain.setTargetAtTime(0, currentTime, 0.05);
}
}

/**
* Adjusts the compressor settings.
* @param {Partial<DynamicsCompressorOptions>} options - The compressor options to adjust.
*/
setCompressorSettings(options: Partial<DynamicsCompressorOptions>) {
if (this.compressor) {
if (options.threshold !== undefined)
this.compressor.threshold.setTargetAtTime(
options.threshold,
this.audioCtx.currentTime,
0.01
);
if (options.knee !== undefined)
this.compressor.knee.setTargetAtTime(
options.knee,
this.audioCtx.currentTime,
0.01
);
if (options.ratio !== undefined)
this.compressor.ratio.setTargetAtTime(
options.ratio,
this.audioCtx.currentTime,
0.01
);
if (options.attack !== undefined)
this.compressor.attack.setTargetAtTime(
options.attack,
this.audioCtx.currentTime,
0.01
);
if (options.release !== undefined)
this.compressor.release.setTargetAtTime(
options.release,
this.audioCtx.currentTime,
0.01
);
}
}

/**
* Resets the equalizer to flat response.
*/
reset() {
const currentTime = this.audioCtx.currentTime;
this.eqFilterBands.forEach((band: BiquadFilterNode) => {
band.gain.setTargetAtTime(0, currentTime, 0.05);
});
this.bassBoostFilter.gain.setTargetAtTime(0, currentTime, 0.05);
}
}

export { Equalizer };
92 changes: 66 additions & 26 deletions src/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class AudioX {
enablePlayLog = false,
enableHls = false,
enableEQ = false,
crossOrigin = 'anonymous',
crossOrigin = null,
hlsConfig = {}
} = initProps;

Expand All @@ -99,6 +99,7 @@ class AudioX {
this._audio.autoplay = autoPlay;
this._audio.crossOrigin = crossOrigin;
this.isPlayLogEnabled = enablePlayLog;
this.isEqEnabled = enableEQ;

if (customEventListeners !== null) {
if (useDefaultEventListeners) {
Expand All @@ -117,28 +118,42 @@ class AudioX {
attachMediaSessionHandlers();
}

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

if (enableHls) {
const hls = new HlsAdapter();
hls.init(hlsConfig, enablePlayLog);
}
}

async addMedia(mediaTrack: MediaTrack) {
async addMedia(
mediaTrack: MediaTrack,
mediaFetchFn?: (mediaTrack: MediaTrack) => Promise<void>
) {
if (!mediaTrack) {
return;
}

if (mediaFetchFn && !mediaTrack.source.length) {
this._fetchFn = mediaFetchFn;
}

const queue = this.getQueue();
if (isValidArray(queue)) {
const index = queue.findIndex((track) => mediaTrack.id === track.id);
if (index > -1) {
this._currentQueueIndex = index;
}
}

const mediaType = mediaTrack.source.includes('.m3u8') ? 'HLS' : 'DEFAULT';

if (this.isPlayLogEnabled) {
calculateActualPlayedLength(audioInstance, 'TRACK_CHANGE');
}

if (mediaType === 'HLS') {
if (
mediaType === 'HLS' &&
!audioInstance.canPlayType('application/vnd.apple.mpegurl')
) {
const hls = new HlsAdapter();
const hlsInstance = hls.getHlsInstance();
if (hlsInstance) {
Expand All @@ -165,7 +180,7 @@ class AudioX {
}

attachEq() {
if (this.isEqEnabled && this.eqStatus === 'IDEAL') {
if (this.eqStatus === 'IDEAL') {
try {
const eq = new Equalizer();
this.eqStatus = eq.status();
Expand All @@ -192,6 +207,9 @@ class AudioX {
console.warn('cancelling current audio playback, track changed');
});
}
if (this.isEqEnabled) {
this.attachEq();
}
}

/**
Expand All @@ -209,23 +227,19 @@ class AudioX {
) {
const currentTrack =
mediaTrack || (this._queue.length > 0 ? this._queue[0] : undefined);
if (fetchFn && isValidFunction(fetchFn) && currentTrack) {
if (fetchFn && isValidFunction(fetchFn) && currentTrack?.source.length) {
this._fetchFn = fetchFn;
await fetchFn(currentTrack as MediaTrack);
}

if (this._queue && isValidArray(this._queue)) {
this._currentQueueIndex = this._queue.findIndex(
(track) => track.id === currentTrack?.id
);
}
try {
if (currentTrack) {
this.addMedia(currentTrack).then(() => {
if (audioInstance.HAVE_ENOUGH_DATA === READY_STATE.HAVE_ENOUGH_DATA) {
setTimeout(async () => {
this.attachEq();
await this.play();
if (this.isEqEnabled) {
this.attachEq();
}
}, 950);
}
});
Expand Down Expand Up @@ -329,14 +343,31 @@ class AudioX {
}

setPreset(id: keyof Preset) {
this.eqInstance.setPreset(id);
if (this.isEqEnabled) {
this.eqInstance.setPreset(id);
} else {
console.error('Equalizer not initialized, please set enableEq at init');
}
}

setCustomEQ(gains: number[]) {
this.eqInstance.setCustomEQ(gains);
if (this.isEqEnabled) {
this.eqInstance.setCustomEQ(gains);
} else {
console.error('Equalizer not initialized, please set enableEq at init');
}
}

setBassBoost(enabled: boolean, boost: number) {
if (this.isEqEnabled) {
this.eqInstance.setBassBoost(enabled, boost);
} else {
console.error('Equalizer not initialized, please set enableEq at init');
}
}

addQueue(queue: MediaTrack[], playbackType: QueuePlaybackType) {
this.clearQueue();
const playerQueue = isValidArray(queue) ? queue.slice() : [];
switch (playbackType) {
case 'DEFAULT':
Expand All @@ -353,27 +384,35 @@ class AudioX {
break;
}
handleQueuePlayback();
// Attaching MediaSession Handler again as this will make sure the next and previous button show up in notification
/* Attaching MediaSession Handler again as this will make sure that
the next and previous button show up in notification */
if (this.showNotificationsActions) {
attachMediaSessionHandlers();
}
}

playNext() {
if (this._queue.length > this._currentQueueIndex + 1) {
this._currentQueueIndex++;
const nextTrack = this._queue[this._currentQueueIndex];
const index = this._currentQueueIndex + 1;
if (this?._queue?.length > index) {
const nextTrack = this._queue[index];
this.addMediaAndPlay(nextTrack, this._fetchFn);
this._currentQueueIndex = index;
} else {
console.warn('Queue ended');
// stop the audio and end trigger queue ended
this.stop();
notifier.notify('AUDIO_STATE', {
playbackState: PLAYBACK_STATE.QUEUE_ENDED
});
}
}

playPrevious() {
if (this._currentQueueIndex > 0) {
this._currentQueueIndex--;
const previousTrack = this._queue[this._currentQueueIndex];
const index = this?._currentQueueIndex - 1;

if (index >= 0) {
const previousTrack = this?._queue[index];
this.addMediaAndPlay(previousTrack, this._fetchFn);
this._currentQueueIndex = index;
} else {
console.log('At the beginning of the queue');
}
Expand All @@ -382,6 +421,7 @@ class AudioX {
clearQueue() {
if (this._queue && isValidArray(this._queue)) {
this._queue = [];
this._currentQueueIndex = 0;
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/constants/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const PLAYBACK_STATE = Object.freeze({
STALLED: 'stalled',
ERROR: 'error',
TRACK_CHANGE: 'trackchanged',
DURATION_CHANGE: 'durationchanged'
DURATION_CHANGE: 'durationchanged',
QUEUE_ENDED: 'queueended'
});

const ERROR_MSG_MAP: ErrorMessageMap = Object.freeze({
Expand Down
Loading

0 comments on commit 9e3af64

Please sign in to comment.