ngx-audio-wave
Version:
Very simple audio wave system
205 lines (198 loc) • 16.8 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, input, signal, inject, PLATFORM_ID, DestroyRef, viewChild, Component, ChangeDetectionStrategy, NgModule } from '@angular/core';
import { HttpClient, provideHttpClient, withFetch } from '@angular/common/http';
import { isPlatformBrowser } from '@angular/common';
import { interval, finalize } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
class NgxAudioWaveService {
samples = 50;
/**
* Filters the AudioBuffer retrieved from an external source
* @param {AudioBuffer} audioBuffer the AudioBuffer from drawAudio()
* @returns {Array} an array of floating point numbers
*/
filterData(audioBuffer) {
const rawData = audioBuffer.getChannelData(0); // We only need to work with one channel of data
const blockSize = Math.floor(rawData.length / this.samples); // the number of samples in each subdivision
const filteredData = [];
for (let i = 0; i < this.samples; i++) {
let blockStart = blockSize * i; // the location of the first sample in the block
let sum = 0;
for (let j = 0; j < blockSize; j++) {
sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block
}
filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
}
return filteredData;
}
/**
* Normalizes the audio data to make a cleaner illustration
* @param {Array} filteredData the data from filterData()
* @returns {Array} an normalized array of floating point numbers
*/
normalizeData(filteredData) {
const multiplier = Math.pow(Math.max(...filteredData), -1);
return filteredData.map(n => n * multiplier);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxAudioWaveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxAudioWaveService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxAudioWaveService, decorators: [{
type: Injectable
}] });
class NgxAudioWaveComponent {
color = input('#1e90ff');
audioSrc = input.required();
height = input(25);
gap = input(5);
rounded = input(true);
hideBtn = input(false);
_error = signal(false);
_exactPlayedPercent = signal(0);
_exactCurrentTime = signal(0);
_isPause = signal(true);
_isLoading = signal(true);
_exactDuration = signal(0);
_normalizedData = signal([]);
// injecting
platformId = inject(PLATFORM_ID);
isPlatformBrowser = isPlatformBrowser(this.platformId);
httpClient = inject(HttpClient);
audioWaveService = inject(NgxAudioWaveService);
destroyRef = inject(DestroyRef);
audioRef = viewChild.required('audioRef');
get exactPlayedPercent() {
return this._exactPlayedPercent();
}
get exactDuration() {
return this._exactDuration();
}
get exactCurrentTime() {
return this._exactCurrentTime();
}
get isPause() {
return this._isPause();
}
get isLoading() {
return this._isLoading();
}
get playedPercent() {
return Math.round(this._exactPlayedPercent());
}
get currentTime() {
return Math.round(this._exactCurrentTime());
}
get duration() {
return Math.round(this._exactDuration());
}
get width() {
return this.audioWaveService.samples * this.gap();
}
ngAfterViewInit() {
if (this.isPlatformBrowser) {
this.fetchAudio(this.audioSrc());
this.startInterval();
}
}
ngOnDestroy() {
this.stop();
}
play(time = 0) {
if (!this.isPlatformBrowser)
return;
const audio = this.audioRef().nativeElement;
void audio.play();
if (time) {
audio.currentTime = time;
}
}
pause() {
if (!this.isPlatformBrowser)
return;
const audio = this.audioRef().nativeElement;
audio.pause();
}
stop() {
if (!this.isPlatformBrowser)
return;
const audio = this.audioRef().nativeElement;
audio.currentTime = 0;
this.pause();
}
calculatePercent(total, value) {
return (value / total) * 100 || 0;
}
setTime(mouseEvent) {
const offsetX = mouseEvent.offsetX;
const width = this.width;
const clickPercent = this.calculatePercent(width, offsetX);
const time = (clickPercent * this._exactDuration()) / 100;
void this.play(time);
}
startInterval() {
interval(100)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
const audio = this.audioRef().nativeElement;
if (audio) {
const percent = this.calculatePercent(this._exactDuration(), audio.currentTime);
this._exactPlayedPercent.set(percent < 100 ? percent : 100);
this._exactCurrentTime.set(audio.currentTime);
this._isPause.set(audio.paused);
}
});
}
fetchAudio(audioSrc) {
this._isLoading.set(true);
this.httpClient
.get(audioSrc, { responseType: 'arraybuffer' })
.pipe(finalize(() => {
this._isLoading.set(false);
}), takeUntilDestroyed(this.destroyRef))
.subscribe({
next: async (arrayBuffer) => {
try {
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
this._exactDuration.set(audioBuffer.duration);
const filteredData = this.audioWaveService.filterData(audioBuffer);
this._normalizedData.set(this.audioWaveService.normalizeData(filteredData));
}
catch (e) {
this._error.set(true);
}
},
error: (error) => {
console.error(error);
this._error.set(true);
}
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxAudioWaveComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: NgxAudioWaveComponent, isStandalone: false, selector: "ngx-audio-wave", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, audioSrc: { classPropertyName: "audioSrc", publicName: "audioSrc", isSignal: true, isRequired: true, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, hideBtn: { classPropertyName: "hideBtn", publicName: "hideBtn", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "audioRef", first: true, predicate: ["audioRef"], descendants: true, isSignal: true }], ngImport: i0, template: "<audio #audioRef [src]=\"audioSrc()\"></audio>\n\n<div [style.--ngx-audio-wave-color]=\"color()\"\n class=\"ngx-audio-wave-wrapper\">\n @if (!_error()) {\n @if (!hideBtn()) {\n @if (_isPause()) {\n <button class=\"ngx-audio-wave-btn\" (click)=\"play()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path\n d=\"m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z\"/>\n </svg>\n </button>\n } @else {\n <button class=\"ngx-audio-wave-btn\" (click)=\"pause()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path\n d=\"M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z\"/>\n </svg>\n </button>\n }\n }\n\n <div class=\"ngx-audio-wave\" [style.height]=\"height() + 'px'\" [style.width]=\"width + 'px'\">\n @if (!_isLoading()) {\n <svg (click)=\"setTime($event)\"\n [attr.viewBox]=\"'0 0 ' + width + ' ' + height()\"\n class=\"real\">\n @for (rect of _normalizedData(); track rect; let index = $index) {\n <rect\n [attr.height]=\"rect * height()\"\n [attr.width]=\"2\"\n [attr.x]=\"index * gap()\"\n [attr.y]=\"height() - (rect * height())\"\n [attr.rx]=\"rounded() ? 1 : 0\"\n [attr.ry]=\"rounded() ? 1 : 0\">\n </rect>\n }\n </svg>\n\n <div class=\"fake\" [style.clip-path]=\"'inset(0px ' + (100 - _exactPlayedPercent()) + '% 0px 0px)'\">\n <svg [attr.viewBox]=\"'0 0 ' + width + ' ' + height()\">\n @for (rect of _normalizedData(); track rect; let index = $index) {\n <rect\n [attr.height]=\"rect * height()\"\n [attr.width]=\"2\"\n [attr.x]=\"index * gap()\"\n [attr.y]=\"height() - (rect * height())\"\n [attr.rx]=\"rounded() ? 1 : 0\"\n [attr.ry]=\"rounded() ? 1 : 0\">\n </rect>\n }\n </svg>\n </div>\n } @else {\n <div class=\"ngx-audio-wave-loading\">\n <span></span>\n <span></span>\n <span></span>\n </div>\n }\n </div>\n } @else {\n Some errors occured\n }\n</div>\n", styles: [".ngx-audio-wave-wrapper{display:flex;align-items:center;gap:1rem;min-height:3rem}.ngx-audio-wave{position:relative;cursor:pointer}.ngx-audio-wave svg{position:absolute;left:0;bottom:0}.ngx-audio-wave svg.real rect{opacity:.3}.ngx-audio-wave svg rect{fill:var(--ngx-audio-wave-color)}.ngx-audio-wave svg rect:hover{opacity:1}.ngx-audio-wave .fake{position:absolute;left:0;bottom:0;width:100%;height:100%;transition:clip-path .3s;pointer-events:none}.ngx-audio-wave-btn{border:none;width:2rem;height:2rem;background-color:#0000001a;border-radius:50%;padding:0;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;transition:background-color .3s;cursor:pointer}.ngx-audio-wave-btn:hover{background-color:#00000026}.ngx-audio-wave-btn:active{background-color:#0003}.ngx-audio-wave-loading{position:relative;overflow:hidden;height:2rem;display:flex;gap:.5rem}.ngx-audio-wave-loading span{display:block;height:100%;width:5px;background-color:var(--ngx-audio-wave-color);border-radius:4px;opacity:.1;animation:ngx-audio-wave-loading-anim .75s infinite}@keyframes ngx-audio-wave-loading-anim{0%{transform:translateY(-100%)}to{transform:translateY(100%)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxAudioWaveComponent, decorators: [{
type: Component,
args: [{ standalone: false, selector: 'ngx-audio-wave', changeDetection: ChangeDetectionStrategy.OnPush, template: "<audio #audioRef [src]=\"audioSrc()\"></audio>\n\n<div [style.--ngx-audio-wave-color]=\"color()\"\n class=\"ngx-audio-wave-wrapper\">\n @if (!_error()) {\n @if (!hideBtn()) {\n @if (_isPause()) {\n <button class=\"ngx-audio-wave-btn\" (click)=\"play()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path\n d=\"m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z\"/>\n </svg>\n </button>\n } @else {\n <button class=\"ngx-audio-wave-btn\" (click)=\"pause()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path\n d=\"M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z\"/>\n </svg>\n </button>\n }\n }\n\n <div class=\"ngx-audio-wave\" [style.height]=\"height() + 'px'\" [style.width]=\"width + 'px'\">\n @if (!_isLoading()) {\n <svg (click)=\"setTime($event)\"\n [attr.viewBox]=\"'0 0 ' + width + ' ' + height()\"\n class=\"real\">\n @for (rect of _normalizedData(); track rect; let index = $index) {\n <rect\n [attr.height]=\"rect * height()\"\n [attr.width]=\"2\"\n [attr.x]=\"index * gap()\"\n [attr.y]=\"height() - (rect * height())\"\n [attr.rx]=\"rounded() ? 1 : 0\"\n [attr.ry]=\"rounded() ? 1 : 0\">\n </rect>\n }\n </svg>\n\n <div class=\"fake\" [style.clip-path]=\"'inset(0px ' + (100 - _exactPlayedPercent()) + '% 0px 0px)'\">\n <svg [attr.viewBox]=\"'0 0 ' + width + ' ' + height()\">\n @for (rect of _normalizedData(); track rect; let index = $index) {\n <rect\n [attr.height]=\"rect * height()\"\n [attr.width]=\"2\"\n [attr.x]=\"index * gap()\"\n [attr.y]=\"height() - (rect * height())\"\n [attr.rx]=\"rounded() ? 1 : 0\"\n [attr.ry]=\"rounded() ? 1 : 0\">\n </rect>\n }\n </svg>\n </div>\n } @else {\n <div class=\"ngx-audio-wave-loading\">\n <span></span>\n <span></span>\n <span></span>\n </div>\n }\n </div>\n } @else {\n Some errors occured\n }\n</div>\n", styles: [".ngx-audio-wave-wrapper{display:flex;align-items:center;gap:1rem;min-height:3rem}.ngx-audio-wave{position:relative;cursor:pointer}.ngx-audio-wave svg{position:absolute;left:0;bottom:0}.ngx-audio-wave svg.real rect{opacity:.3}.ngx-audio-wave svg rect{fill:var(--ngx-audio-wave-color)}.ngx-audio-wave svg rect:hover{opacity:1}.ngx-audio-wave .fake{position:absolute;left:0;bottom:0;width:100%;height:100%;transition:clip-path .3s;pointer-events:none}.ngx-audio-wave-btn{border:none;width:2rem;height:2rem;background-color:#0000001a;border-radius:50%;padding:0;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;transition:background-color .3s;cursor:pointer}.ngx-audio-wave-btn:hover{background-color:#00000026}.ngx-audio-wave-btn:active{background-color:#0003}.ngx-audio-wave-loading{position:relative;overflow:hidden;height:2rem;display:flex;gap:.5rem}.ngx-audio-wave-loading span{display:block;height:100%;width:5px;background-color:var(--ngx-audio-wave-color);border-radius:4px;opacity:.1;animation:ngx-audio-wave-loading-anim .75s infinite}@keyframes ngx-audio-wave-loading-anim{0%{transform:translateY(-100%)}to{transform:translateY(100%)}}\n"] }]
}] });
class NgxAudioWaveModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxAudioWaveModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.0", ngImport: i0, type: NgxAudioWaveModule, declarations: [NgxAudioWaveComponent], exports: [NgxAudioWaveComponent] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxAudioWaveModule, providers: [NgxAudioWaveService, provideHttpClient(withFetch())] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxAudioWaveModule, decorators: [{
type: NgModule,
args: [{
declarations: [NgxAudioWaveComponent],
exports: [NgxAudioWaveComponent],
providers: [NgxAudioWaveService, provideHttpClient(withFetch())]
}]
}] });
/*
* Public API Surface of ngx-audio-wave
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxAudioWaveComponent, NgxAudioWaveModule };
//# sourceMappingURL=ngx-audio-wave.mjs.map