ngx-audio-wave
Version:
A modern, accessible audio wave visualization component for Angular 20+ with comprehensive keyboard navigation and screen reader support.
337 lines (331 loc) • 27.3 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, input, numberAttribute, booleanAttribute, signal, computed, inject, PLATFORM_ID, DestroyRef, viewChild, SecurityContext, ChangeDetectionStrategy, Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { isPlatformBrowser } from '@angular/common';
import { interval, finalize } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DomSanitizer } from '@angular/platform-browser';
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: "20.3.4", ngImport: i0, type: NgxAudioWaveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: NgxAudioWaveService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: NgxAudioWaveService, decorators: [{
type: Injectable
}] });
class NgxAudioWave {
// required inputs
audioSrc = input.required(...(ngDevMode ? [{ debugName: "audioSrc" }] : []));
// optional inputs
color = input('#1e90ff', ...(ngDevMode ? [{ debugName: "color" }] : []));
height = input(25, ...(ngDevMode ? [{ debugName: "height", transform: numberAttribute }] : [{ transform: numberAttribute }]));
gap = input(5, ...(ngDevMode ? [{ debugName: "gap", transform: numberAttribute }] : [{ transform: numberAttribute }]));
rounded = input(true, ...(ngDevMode ? [{ debugName: "rounded", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
hideBtn = input(false, ...(ngDevMode ? [{ debugName: "hideBtn", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
skip = input(5, ...(ngDevMode ? [{ debugName: "skip", transform: numberAttribute }] : [{ transform: numberAttribute }]));
volume = input(1, ...(ngDevMode ? [{ debugName: "volume", transform: numberAttribute }] : [{ transform: numberAttribute }]));
playbackRate = input(1, ...(ngDevMode ? [{ debugName: "playbackRate", transform: numberAttribute }] : [{ transform: numberAttribute }]));
loop = input(false, ...(ngDevMode ? [{ debugName: "loop", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
// accessibility inputs
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
playButtonLabel = input('Play audio', ...(ngDevMode ? [{ debugName: "playButtonLabel" }] : []));
pauseButtonLabel = input('Pause audio', ...(ngDevMode ? [{ debugName: "pauseButtonLabel" }] : []));
progressBarLabel = input('Audio progress bar', ...(ngDevMode ? [{ debugName: "progressBarLabel" }] : []));
// public state signals
isPaused = signal(true, ...(ngDevMode ? [{ debugName: "isPaused" }] : []));
isLoading = signal(true, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : []));
currentVolume = signal(1, ...(ngDevMode ? [{ debugName: "currentVolume" }] : []));
currentPlaybackRate = signal(1, ...(ngDevMode ? [{ debugName: "currentPlaybackRate" }] : []));
isLooping = signal(false, ...(ngDevMode ? [{ debugName: "isLooping" }] : []));
progressText = computed(() => {
const current = this.exactCurrentTime();
const duration = this.exactDuration();
const percent = this.exactPlayedPercent();
if (duration === 0) {
return 'Audio not loaded';
}
const currentMinutes = Math.floor(current / 60);
const currentSeconds = Math.floor(current % 60);
const durationMinutes = Math.floor(duration / 60);
const durationSeconds = Math.floor(duration % 60);
return `${currentMinutes}:${currentSeconds.toString().padStart(2, '0')} of ${durationMinutes}:${durationSeconds.toString().padStart(2, '0')} (${Math.round(percent)}% played)`;
}, ...(ngDevMode ? [{ debugName: "progressText" }] : []));
statusText = computed(() => {
if (this.isLoading()) {
return 'Loading audio';
}
if (this.hasError()) {
return 'Error loading audio';
}
if (this.isPaused()) {
return 'Audio paused';
}
return 'Audio playing';
}, ...(ngDevMode ? [{ debugName: "statusText" }] : []));
// public-exact
exactPlayedPercent = computed(() => {
const percent = this.calculatePercent(this.exactDuration(), this.exactCurrentTime());
return percent < 100 ? percent : 100;
}, ...(ngDevMode ? [{ debugName: "exactPlayedPercent" }] : []));
exactCurrentTime = signal(0, ...(ngDevMode ? [{ debugName: "exactCurrentTime" }] : []));
exactDuration = signal(0, ...(ngDevMode ? [{ debugName: "exactDuration" }] : []));
// deprecated signals
/** @deprecated This property will be removed in version 21.0.0. Use exactPlayedPercent instead. */
playedPercent = computed(() => Math.round(this.exactPlayedPercent()), ...(ngDevMode ? [{ debugName: "playedPercent" }] : []));
/** @deprecated This property will be removed in version 21.0.0. Use exactCurrentTime instead. */
currentTime = computed(() => Math.round(this.exactCurrentTime()), ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
/** @deprecated This property will be removed in version 21.0.0. Use exactDuration instead. */
duration = computed(() => Math.round(this.exactDuration()), ...(ngDevMode ? [{ debugName: "duration" }] : []));
// component internal signals
normalizedData = signal([], ...(ngDevMode ? [{ debugName: "normalizedData" }] : []));
clipPath = computed(() => `inset(0px ${100 - this.exactPlayedPercent()}% 0px 0px)`, ...(ngDevMode ? [{ debugName: "clipPath" }] : []));
width = computed(() => this.audioWaveService.samples * this.gap(), ...(ngDevMode ? [{ debugName: "width" }] : []));
// injecting
platformId = inject(PLATFORM_ID);
isPlatformBrowser = isPlatformBrowser(this.platformId);
domSanitizer = inject(DomSanitizer);
httpClient = inject(HttpClient);
audioWaveService = inject(NgxAudioWaveService);
destroyRef = inject(DestroyRef);
// view
audioRef = viewChild.required('audioRef');
// lifecycle hooks
ngAfterViewInit() {
if (this.isPlatformBrowser) {
this.fetchAudio(this.audioSrc());
this.setVolume(this.volume());
this.setPlaybackRate(this.playbackRate());
this.setLoop(this.loop());
this.startInterval();
}
}
ngOnDestroy() {
this.stop();
}
// playback control
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();
}
// volume
setVolume(volume) {
if (!this.isPlatformBrowser)
return;
const audio = this.audioRef().nativeElement;
const clampedVolume = Math.max(0, Math.min(1, volume));
audio.volume = clampedVolume;
this.currentVolume.set(clampedVolume);
}
mute() {
this.setVolume(0);
}
unmute() {
this.setVolume(this.volume());
}
toggleMute() {
if (this.currentVolume() === 0) {
this.unmute();
}
else {
this.mute();
}
}
// playback speed
setPlaybackRate(rate) {
if (!this.isPlatformBrowser)
return;
const audio = this.audioRef().nativeElement;
const clampedRate = Math.max(0.25, Math.min(4, rate)); // Ограничиваем от 0.25x до 4x
audio.playbackRate = clampedRate;
this.currentPlaybackRate.set(clampedRate);
}
resetPlaybackRate() {
this.setPlaybackRate(1);
}
increasePlaybackRate() {
const currentRate = this.currentPlaybackRate();
const newRate = Math.min(4, currentRate + 0.25);
this.setPlaybackRate(newRate);
}
decreasePlaybackRate() {
const currentRate = this.currentPlaybackRate();
const newRate = Math.max(0.25, currentRate - 0.25);
this.setPlaybackRate(newRate);
}
// loop
setLoop(loop) {
if (!this.isPlatformBrowser)
return;
const audio = this.audioRef().nativeElement;
audio.loop = loop;
this.isLooping.set(loop);
}
enableLoop() {
this.setLoop(true);
}
disableLoop() {
this.setLoop(false);
}
toggleLoop() {
this.setLoop(!this.isLooping());
}
// user interaction
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);
}
// private helpers
calculatePercent(total, value) {
return (value / total) * 100 || 0;
}
startInterval() {
interval(100)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
const audio = this.audioRef().nativeElement;
this.exactCurrentTime.set(audio.currentTime);
},
});
}
fetchAudio(audioSrc) {
this.isLoading.set(true);
const src = typeof audioSrc === 'object'
? this.domSanitizer.sanitize(SecurityContext.URL, audioSrc)
: audioSrc;
if (!src) {
console.error('Invalid SafeUrl: could not sanitize');
this.hasError.set(true);
return;
}
this.httpClient
.get(src, { 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.hasError.set(true);
}
},
error: error => {
console.error(error);
this.hasError.set(true);
},
});
}
pauseChange(event) {
if (!(event.target instanceof HTMLAudioElement))
return;
this.isPaused.set(event.target.paused);
}
// event handlers
onKeyDown(event) {
if (!this.isPlatformBrowser)
return;
const audio = this.audioRef().nativeElement;
const duration = this.exactDuration();
switch (event.key) {
case ' ':
case 'Enter':
event.preventDefault();
if (this.isPaused()) {
this.play();
}
else {
this.pause();
}
break;
case 'ArrowLeft':
event.preventDefault();
const leftTime = Math.max(0, audio.currentTime - this.skip());
this.play(leftTime);
break;
case 'ArrowRight':
event.preventDefault();
const rightTime = Math.min(duration, audio.currentTime + this.skip());
this.play(rightTime);
break;
case 'Home':
event.preventDefault();
this.play(0);
break;
case 'End':
event.preventDefault();
this.play(duration);
break;
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: NgxAudioWave, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: NgxAudioWave, isStandalone: true, selector: "ngx-audio-wave", inputs: { audioSrc: { classPropertyName: "audioSrc", publicName: "audioSrc", isSignal: true, isRequired: true, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, 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 }, skip: { classPropertyName: "skip", publicName: "skip", isSignal: true, isRequired: false, transformFunction: null }, volume: { classPropertyName: "volume", publicName: "volume", isSignal: true, isRequired: false, transformFunction: null }, playbackRate: { classPropertyName: "playbackRate", publicName: "playbackRate", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, playButtonLabel: { classPropertyName: "playButtonLabel", publicName: "playButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, pauseButtonLabel: { classPropertyName: "pauseButtonLabel", publicName: "pauseButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, progressBarLabel: { classPropertyName: "progressBarLabel", publicName: "progressBarLabel", isSignal: true, isRequired: false, transformFunction: null } }, providers: [NgxAudioWaveService], viewQueries: [{ propertyName: "audioRef", first: true, predicate: ["audioRef"], descendants: true, isSignal: true }], ngImport: i0, template: "<audio\n #audioRef\n [src]=\"audioSrc()\"\n (pause)=\"pauseChange($event)\"\n (play)=\"pauseChange($event)\"\n hidden=\"hidden\"\n></audio>\n\n<div\n class=\"ngx-audio-wave-wrapper\"\n [attr.aria-label]=\"ariaLabel() || 'Audio player'\"\n [style.--ngx-audio-wave-color]=\"color()\"\n role=\"region\"\n>\n @if (!hasError()) {\n @if (!hideBtn()) {\n @if (isPaused()) {\n <button\n class=\"ngx-audio-wave-btn\"\n [attr.aria-label]=\"playButtonLabel()\"\n [attr.aria-pressed]=\"false\"\n (click)=\"play()\"\n type=\"button\"\n >\n <svg\n aria-hidden=\"true\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n width=\"16\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\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 />\n </svg>\n </button>\n } @else {\n <button\n class=\"ngx-audio-wave-btn\"\n [attr.aria-label]=\"pauseButtonLabel()\"\n [attr.aria-pressed]=\"true\"\n (click)=\"pause()\"\n type=\"button\"\n >\n <svg\n aria-hidden=\"true\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n width=\"16\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\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 />\n </svg>\n </button>\n }\n }\n\n <div\n class=\"ngx-audio-wave\"\n [attr.aria-label]=\"progressBarLabel()\"\n [attr.aria-valuemax]=\"100\"\n [attr.aria-valuemin]=\"0\"\n [attr.aria-valuenow]=\"exactPlayedPercent()\"\n [attr.aria-valuetext]=\"progressText()\"\n [style.height.px]=\"height()\"\n [style.width.px]=\"width()\"\n (click)=\"setTime($event)\"\n (keydown)=\"onKeyDown($event)\"\n role=\"progressbar\"\n tabindex=\"0\"\n >\n @if (!isLoading()) {\n <svg\n class=\"real\"\n [attr.viewBox]=\"`0 0 ${width()} ${height()}`\"\n aria-hidden=\"true\"\n >\n @for (rect of normalizedData(); track rect; let index = $index) {\n <rect\n [attr.height]=\"rect * height()\"\n [attr.rx]=\"rounded() ? 1 : 0\"\n [attr.ry]=\"rounded() ? 1 : 0\"\n [attr.width]=\"2\"\n [attr.x]=\"index * gap()\"\n [attr.y]=\"height() - rect * height()\"\n ></rect>\n }\n </svg>\n\n <div class=\"fake\" [style.clip-path]=\"clipPath()\">\n <svg [attr.viewBox]=\"`0 0 ${width()} ${height()}`\" aria-hidden=\"true\">\n @for (rect of normalizedData(); track rect; let index = $index) {\n <rect\n [attr.height]=\"rect * height()\"\n [attr.rx]=\"rounded() ? 1 : 0\"\n [attr.ry]=\"rounded() ? 1 : 0\"\n [attr.width]=\"2\"\n [attr.x]=\"index * gap()\"\n [attr.y]=\"height() - rect * height()\"\n ></rect>\n }\n </svg>\n </div>\n } @else {\n <div\n class=\"ngx-audio-wave-loading\"\n aria-label=\"Loading audio\"\n role=\"status\"\n >\n <span></span>\n <span></span>\n <span></span>\n </div>\n }\n </div>\n\n <!-- Live region for screen readers -->\n <div class=\"ngx-audio-wave-sr-only\" aria-atomic=\"true\" aria-live=\"polite\">\n {{ statusText() }}\n </div>\n } @else {\n <div aria-label=\"Audio loading error\" role=\"alert\">\n Some errors occurred\n </div>\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%)}}.ngx-audio-wave-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.ngx-audio-wave:focus{outline:2px solid var(--ngx-audio-wave-color);outline-offset:2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: NgxAudioWave, decorators: [{
type: Component,
args: [{ selector: 'ngx-audio-wave', changeDetection: ChangeDetectionStrategy.OnPush, providers: [NgxAudioWaveService], template: "<audio\n #audioRef\n [src]=\"audioSrc()\"\n (pause)=\"pauseChange($event)\"\n (play)=\"pauseChange($event)\"\n hidden=\"hidden\"\n></audio>\n\n<div\n class=\"ngx-audio-wave-wrapper\"\n [attr.aria-label]=\"ariaLabel() || 'Audio player'\"\n [style.--ngx-audio-wave-color]=\"color()\"\n role=\"region\"\n>\n @if (!hasError()) {\n @if (!hideBtn()) {\n @if (isPaused()) {\n <button\n class=\"ngx-audio-wave-btn\"\n [attr.aria-label]=\"playButtonLabel()\"\n [attr.aria-pressed]=\"false\"\n (click)=\"play()\"\n type=\"button\"\n >\n <svg\n aria-hidden=\"true\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n width=\"16\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\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 />\n </svg>\n </button>\n } @else {\n <button\n class=\"ngx-audio-wave-btn\"\n [attr.aria-label]=\"pauseButtonLabel()\"\n [attr.aria-pressed]=\"true\"\n (click)=\"pause()\"\n type=\"button\"\n >\n <svg\n aria-hidden=\"true\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n width=\"16\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\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 />\n </svg>\n </button>\n }\n }\n\n <div\n class=\"ngx-audio-wave\"\n [attr.aria-label]=\"progressBarLabel()\"\n [attr.aria-valuemax]=\"100\"\n [attr.aria-valuemin]=\"0\"\n [attr.aria-valuenow]=\"exactPlayedPercent()\"\n [attr.aria-valuetext]=\"progressText()\"\n [style.height.px]=\"height()\"\n [style.width.px]=\"width()\"\n (click)=\"setTime($event)\"\n (keydown)=\"onKeyDown($event)\"\n role=\"progressbar\"\n tabindex=\"0\"\n >\n @if (!isLoading()) {\n <svg\n class=\"real\"\n [attr.viewBox]=\"`0 0 ${width()} ${height()}`\"\n aria-hidden=\"true\"\n >\n @for (rect of normalizedData(); track rect; let index = $index) {\n <rect\n [attr.height]=\"rect * height()\"\n [attr.rx]=\"rounded() ? 1 : 0\"\n [attr.ry]=\"rounded() ? 1 : 0\"\n [attr.width]=\"2\"\n [attr.x]=\"index * gap()\"\n [attr.y]=\"height() - rect * height()\"\n ></rect>\n }\n </svg>\n\n <div class=\"fake\" [style.clip-path]=\"clipPath()\">\n <svg [attr.viewBox]=\"`0 0 ${width()} ${height()}`\" aria-hidden=\"true\">\n @for (rect of normalizedData(); track rect; let index = $index) {\n <rect\n [attr.height]=\"rect * height()\"\n [attr.rx]=\"rounded() ? 1 : 0\"\n [attr.ry]=\"rounded() ? 1 : 0\"\n [attr.width]=\"2\"\n [attr.x]=\"index * gap()\"\n [attr.y]=\"height() - rect * height()\"\n ></rect>\n }\n </svg>\n </div>\n } @else {\n <div\n class=\"ngx-audio-wave-loading\"\n aria-label=\"Loading audio\"\n role=\"status\"\n >\n <span></span>\n <span></span>\n <span></span>\n </div>\n }\n </div>\n\n <!-- Live region for screen readers -->\n <div class=\"ngx-audio-wave-sr-only\" aria-atomic=\"true\" aria-live=\"polite\">\n {{ statusText() }}\n </div>\n } @else {\n <div aria-label=\"Audio loading error\" role=\"alert\">\n Some errors occurred\n </div>\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%)}}.ngx-audio-wave-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.ngx-audio-wave:focus{outline:2px solid var(--ngx-audio-wave-color);outline-offset:2px}\n"] }]
}] });
/*
* Public API Surface of ngx-audio-wave
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxAudioWave };
//# sourceMappingURL=ngx-audio-wave.mjs.map