ngx-audio-wave
Version:
A modern, accessible audio wave visualization component for Angular 22+ with comprehensive keyboard navigation and screen reader support.
523 lines (517 loc) • 37.4 kB
JavaScript
import * as i0 from '@angular/core';
import { input, numberAttribute, booleanAttribute, signal, computed, inject, PLATFORM_ID, viewChild, effect, SecurityContext, ChangeDetectionStrategy, Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { isPlatformBrowser } from '@angular/common';
import { DomSanitizer } from '@angular/platform-browser';
/**
* Filters the AudioBuffer retrieved from an external source.
*/
function filterAudioBufferData(audioBuffer, samples) {
const rawData = audioBuffer.getChannelData(0);
const sampleCount = Math.max(1, Math.floor(samples));
const blockSize = Math.max(1, Math.floor(rawData.length / sampleCount));
const filteredData = [];
for (let i = 0; i < sampleCount; i++) {
const blockStart = blockSize * i;
let sum = 0;
let valuesInBlock = 0;
for (let j = 0; j < blockSize && blockStart + j < rawData.length; j++) {
sum += Math.abs(rawData[blockStart + j]);
valuesInBlock++;
}
filteredData.push(valuesInBlock > 0 ? sum / valuesInBlock : 0);
}
return filteredData;
}
/**
* Normalizes audio data to the 0..1 range used by the SVG bars.
*/
function normalizeAudioData(filteredData) {
const maxValue = Math.max(...filteredData);
if (!Number.isFinite(maxValue) || maxValue <= 0) {
return filteredData.map(() => 0);
}
const multiplier = Math.pow(maxValue, -1);
return filteredData.map(n => n * multiplier);
}
let nextGradientId = 0;
class NgxAudioWave {
// required inputs
audioSrc = input.required(/* @ts-ignore */
...(ngDevMode ? [{ debugName: "audioSrc" }] : /* istanbul ignore next */ []));
// optional inputs
color = input('#1e90ff', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
height = input(25, { ...(ngDevMode ? { debugName: "height" } : /* istanbul ignore next */ {}), transform: numberAttribute });
gap = input(5, { ...(ngDevMode ? { debugName: "gap" } : /* istanbul ignore next */ {}), transform: numberAttribute });
rounded = input(true, { ...(ngDevMode ? { debugName: "rounded" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
hideBtn = input(false, { ...(ngDevMode ? { debugName: "hideBtn" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
skip = input(5, { ...(ngDevMode ? { debugName: "skip" } : /* istanbul ignore next */ {}), transform: numberAttribute });
volume = input(1, { ...(ngDevMode ? { debugName: "volume" } : /* istanbul ignore next */ {}), transform: numberAttribute });
playbackRate = input(1, { ...(ngDevMode ? { debugName: "playbackRate" } : /* istanbul ignore next */ {}), transform: numberAttribute });
loop = input(false, { ...(ngDevMode ? { debugName: "loop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
samples = input(50, { ...(ngDevMode ? { debugName: "samples" } : /* istanbul ignore next */ {}), transform: numberAttribute });
// accessibility inputs
ariaLabel = input('', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
playButtonLabel = input('Play audio', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "playButtonLabel" }] : /* istanbul ignore next */ []));
pauseButtonLabel = input('Pause audio', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "pauseButtonLabel" }] : /* istanbul ignore next */ []));
progressBarLabel = input('Audio progress bar', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "progressBarLabel" }] : /* istanbul ignore next */ []));
// public state signals
isPaused = signal(true, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "isPaused" }] : /* istanbul ignore next */ []));
isLoading = signal(true, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
hasError = signal(false, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
currentVolume = signal(1, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "currentVolume" }] : /* istanbul ignore next */ []));
currentPlaybackRate = signal(1, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "currentPlaybackRate" }] : /* istanbul ignore next */ []));
isLooping = signal(false, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "isLooping" }] : /* istanbul ignore next */ []));
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)`;
}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "progressText" }] : /* istanbul ignore next */ []));
statusText = computed(() => {
if (this.isLoading()) {
return 'Loading audio';
}
if (this.hasError()) {
return 'Error loading audio';
}
if (this.isPaused()) {
return 'Audio paused';
}
return 'Audio playing';
}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "statusText" }] : /* istanbul ignore next */ []));
// public-exact
exactPlayedPercent = computed(() => {
const percent = this.calculatePercent(this.exactDuration(), this.exactCurrentTime());
return percent < 100 ? percent : 100;
}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "exactPlayedPercent" }] : /* istanbul ignore next */ []));
exactCurrentTime = signal(0, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "exactCurrentTime" }] : /* istanbul ignore next */ []));
exactDuration = signal(0, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "exactDuration" }] : /* istanbul ignore next */ []));
// injecting
platformId = inject(PLATFORM_ID);
isPlatformBrowser = isPlatformBrowser(this.platformId);
domSanitizer = inject(DomSanitizer);
httpClient = inject(HttpClient);
// component internal signals
gradientId = `ngx-audio-wave-gradient-${nextGradientId++}`;
audioElementSrc = computed(() => this.sanitizeAudioSrc(this.audioSrc()) ?? '', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "audioElementSrc" }] : /* istanbul ignore next */ []));
normalizedData = signal([], /* @ts-ignore */
...(ngDevMode ? [{ debugName: "normalizedData" }] : /* istanbul ignore next */ []));
isHovering = signal(false, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "isHovering" }] : /* istanbul ignore next */ []));
hoverOffset = signal(0, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "hoverOffset" }] : /* istanbul ignore next */ []));
progressOffset = computed(() => `${this.exactPlayedPercent()}%`, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "progressOffset" }] : /* istanbul ignore next */ []));
visualHeight = computed(() => Math.max(1, this.height()), /* @ts-ignore */
...(ngDevMode ? [{ debugName: "visualHeight" }] : /* istanbul ignore next */ []));
visualGap = computed(() => Math.max(1, this.gap()), /* @ts-ignore */
...(ngDevMode ? [{ debugName: "visualGap" }] : /* istanbul ignore next */ []));
visualSamples = computed(() => Math.max(1, Math.floor(this.samples())), /* @ts-ignore */
...(ngDevMode ? [{ debugName: "visualSamples" }] : /* istanbul ignore next */ []));
width = computed(() => this.visualSamples() * this.visualGap(), /* @ts-ignore */
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
// view
audioRef = viewChild('audioRef', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "audioRef" }] : /* istanbul ignore next */ []));
audioFetchSubscription;
audioLoadId = 0;
animationFrameId = null;
lastNonZeroVolume = 1;
constructor() {
effect(() => {
if (!this.isPlatformBrowser || !this.audioRef()) {
return;
}
this.fetchAudio(this.audioSrc(), this.visualSamples());
});
effect(() => {
const volume = this.clampVolume(this.volume());
const playbackRate = this.clampPlaybackRate(this.playbackRate());
const loop = this.loop();
this.currentVolume.set(volume);
this.currentPlaybackRate.set(playbackRate);
this.isLooping.set(loop);
if (volume > 0) {
this.lastNonZeroVolume = volume;
}
const audio = this.getAudioElement();
if (!this.isPlatformBrowser || !audio) {
return;
}
audio.volume = volume;
audio.playbackRate = playbackRate;
audio.loop = loop;
});
}
ngOnDestroy() {
this.audioFetchSubscription?.unsubscribe();
this.stopCurrentTimeSync();
this.stop();
}
// playback control
play(time) {
if (!this.isPlatformBrowser)
return;
const audio = this.getAudioElement();
if (!audio)
return;
if (!this.audioElementSrc()) {
this.hasError.set(true);
return;
}
if (time !== undefined) {
this.seekTo(time);
}
void audio.play().catch(error => {
if (error instanceof DOMException && error.name === 'NotSupportedError') {
this.hasError.set(true);
return;
}
console.error(error);
});
}
pause() {
if (!this.isPlatformBrowser)
return;
const audio = this.getAudioElement();
if (!audio)
return;
audio.pause();
}
stop() {
if (!this.isPlatformBrowser)
return;
const audio = this.getAudioElement();
if (!audio)
return;
audio.currentTime = 0;
this.pause();
}
setVolume(volume) {
if (!this.isPlatformBrowser)
return;
const audio = this.getAudioElement();
if (!audio)
return;
const clampedVolume = this.clampVolume(volume);
audio.volume = clampedVolume;
this.currentVolume.set(clampedVolume);
if (clampedVolume > 0) {
this.lastNonZeroVolume = clampedVolume;
}
}
/** @deprecated Use setVolume(0) instead. */
mute() {
this.setVolume(0);
}
/** @deprecated Use setVolume(previousNonZeroVolume) with your own stored value instead. */
unmute() {
this.setVolume(this.lastNonZeroVolume);
}
/** @deprecated Use setVolume(currentVolume() === 0 ? value : 0) instead. */
toggleMute() {
if (this.currentVolume() === 0) {
this.unmute();
}
else {
this.mute();
}
}
setPlaybackRate(rate) {
if (!this.isPlatformBrowser)
return;
const audio = this.getAudioElement();
if (!audio)
return;
const clampedRate = this.clampPlaybackRate(rate);
audio.playbackRate = clampedRate;
this.currentPlaybackRate.set(clampedRate);
}
/** @deprecated Use setPlaybackRate(1) instead. */
resetPlaybackRate() {
this.setPlaybackRate(1);
}
/** @deprecated Use setPlaybackRate(currentPlaybackRate() + 0.25) instead. */
increasePlaybackRate() {
const currentRate = this.currentPlaybackRate();
const newRate = Math.min(4, currentRate + 0.25);
this.setPlaybackRate(newRate);
}
/** @deprecated Use setPlaybackRate(currentPlaybackRate() - 0.25) instead. */
decreasePlaybackRate() {
const currentRate = this.currentPlaybackRate();
const newRate = Math.max(0.25, currentRate - 0.25);
this.setPlaybackRate(newRate);
}
setLoop(loop) {
if (!this.isPlatformBrowser)
return;
const audio = this.getAudioElement();
if (!audio)
return;
audio.loop = loop;
this.isLooping.set(loop);
}
/** @deprecated Use setLoop(true) instead. */
enableLoop() {
this.setLoop(true);
}
/** @deprecated Use setLoop(false) instead. */
disableLoop() {
this.setLoop(false);
}
/** @deprecated Use setLoop(!isLooping()) instead. */
toggleLoop() {
this.setLoop(!this.isLooping());
}
// user interaction
setTime(mouseEvent) {
const pointer = this.getPointerPosition(mouseEvent);
const clickPercent = this.calculatePercent(pointer.width, pointer.offset);
const time = (clickPercent * this.exactDuration()) / 100;
this.seekTo(time);
}
setHoverPosition(mouseEvent) {
this.isHovering.set(true);
this.hoverOffset.set(this.getPointerPosition(mouseEvent).offset);
}
clearHoverPosition() {
this.isHovering.set(false);
}
// private helpers
calculatePercent(total, value) {
if (total <= 0) {
return 0;
}
const percent = (value / total) * 100;
return Number.isFinite(percent) ? percent : 0;
}
clampVolume(volume) {
return Math.max(0, Math.min(1, volume));
}
clampPlaybackRate(rate) {
return Math.max(0.25, Math.min(4, rate));
}
getPointerPosition(event) {
const target = event.currentTarget;
if (!(target instanceof HTMLElement)) {
return { offset: 0, width: this.width() };
}
const rect = target.getBoundingClientRect();
return {
offset: Math.max(0, Math.min(rect.width, event.clientX - rect.left)),
width: rect.width,
};
}
clampTime(time) {
const duration = this.exactDuration();
if (!Number.isFinite(time)) {
return 0;
}
return Math.max(0, duration > 0 ? Math.min(duration, time) : time);
}
seekTo(time) {
const audio = this.getAudioElement();
if (!audio)
return;
const clampedTime = this.clampTime(time);
audio.currentTime = clampedTime;
this.exactCurrentTime.set(clampedTime);
}
updateCurrentTime() {
const audio = this.getAudioElement();
if (!audio)
return;
this.exactCurrentTime.set(audio.currentTime);
}
getAudioElement() {
return this.audioRef()?.nativeElement ?? null;
}
startCurrentTimeSync() {
if (this.animationFrameId !== null) {
return;
}
const sync = () => {
this.updateCurrentTime();
if (!this.isPaused()) {
this.animationFrameId = requestAnimationFrame(sync);
}
else {
this.animationFrameId = null;
}
};
this.animationFrameId = requestAnimationFrame(sync);
}
stopCurrentTimeSync() {
if (this.animationFrameId === null) {
return;
}
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
fetchAudio(audioSrc, samples) {
this.audioFetchSubscription?.unsubscribe();
const loadId = ++this.audioLoadId;
this.isLoading.set(true);
this.hasError.set(false);
this.exactDuration.set(0);
this.exactCurrentTime.set(0);
this.normalizedData.set([]);
const src = this.sanitizeAudioSrc(audioSrc);
if (!src) {
console.error('Invalid SafeUrl: could not sanitize');
this.hasError.set(true);
this.isLoading.set(false);
return;
}
this.audioFetchSubscription = this.httpClient
.get(src, { responseType: 'arraybuffer' })
.subscribe({
next: arrayBuffer => {
void this.decodeAudio(arrayBuffer, samples, loadId);
},
error: error => {
if (loadId === this.audioLoadId) {
console.error(error);
this.hasError.set(true);
this.isLoading.set(false);
}
},
});
}
async decodeAudio(arrayBuffer, samples, loadId) {
let audioContext = null;
try {
audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
if (loadId !== this.audioLoadId) {
return;
}
this.exactDuration.set(audioBuffer.duration);
const filteredData = filterAudioBufferData(audioBuffer, samples);
this.normalizedData.set(normalizeAudioData(filteredData));
}
catch (error) {
if (loadId === this.audioLoadId) {
console.error(error);
this.hasError.set(true);
}
}
finally {
if (audioContext) {
await audioContext.close();
}
if (loadId === this.audioLoadId) {
this.isLoading.set(false);
}
}
}
sanitizeAudioSrc(audioSrc) {
return typeof audioSrc === 'object'
? this.domSanitizer.sanitize(SecurityContext.URL, audioSrc)
: audioSrc;
}
pauseChange(event) {
if (!(event.target instanceof HTMLAudioElement))
return;
this.isPaused.set(event.target.paused);
if (event.target.paused) {
this.stopCurrentTimeSync();
this.updateCurrentTime();
}
else {
this.startCurrentTimeSync();
}
}
durationChange(event) {
if (!(event.target instanceof HTMLAudioElement))
return;
const duration = event.target.duration;
if (Number.isFinite(duration) && duration > 0) {
this.exactDuration.set(duration);
}
}
timeChange(event) {
if (!(event.target instanceof HTMLAudioElement))
return;
this.exactCurrentTime.set(event.target.currentTime);
}
// event handlers
onKeyDown(event) {
if (!this.isPlatformBrowser)
return;
const audio = this.getAudioElement();
if (!audio)
return;
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.seekTo(leftTime);
break;
case 'ArrowRight':
event.preventDefault();
const rightTime = Math.min(duration, audio.currentTime + this.skip());
this.seekTo(rightTime);
break;
case 'Home':
event.preventDefault();
this.seekTo(0);
break;
case 'End':
event.preventDefault();
this.seekTo(duration);
break;
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NgxAudioWave, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", 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 }, samples: { classPropertyName: "samples", publicName: "samples", 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 } }, viewQueries: [{ propertyName: "audioRef", first: true, predicate: ["audioRef"], descendants: true, isSignal: true }], ngImport: i0, template: "<audio\n #audioRef\n [src]=\"audioElementSrc()\"\n (durationchange)=\"durationChange($event)\"\n (loadedmetadata)=\"durationChange($event)\"\n (pause)=\"pauseChange($event)\"\n (play)=\"pauseChange($event)\"\n (timeupdate)=\"timeChange($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-orientation]=\"'horizontal'\"\n [attr.aria-valuemax]=\"100\"\n [attr.aria-valuemin]=\"0\"\n [attr.aria-valuenow]=\"exactPlayedPercent()\"\n [attr.aria-valuetext]=\"progressText()\"\n [style.height.px]=\"visualHeight()\"\n [style.width.px]=\"width()\"\n (click)=\"setTime($event)\"\n (keydown)=\"onKeyDown($event)\"\n (mouseleave)=\"clearHoverPosition()\"\n (mousemove)=\"setHoverPosition($event)\"\n role=\"slider\"\n tabindex=\"0\"\n >\n @if (!isLoading()) {\n <svg\n class=\"ngx-audio-wave-svg\"\n [attr.viewBox]=\"`0 0 ${width()} ${visualHeight()}`\"\n aria-hidden=\"true\"\n >\n <defs>\n <linearGradient\n [attr.id]=\"gradientId\"\n [attr.x2]=\"width()\"\n gradientUnits=\"userSpaceOnUse\"\n x1=\"0\"\n y1=\"0\"\n y2=\"0\"\n >\n <stop offset=\"0%\" stop-color=\"var(--ngx-audio-wave-color)\" />\n <stop\n [attr.offset]=\"progressOffset()\"\n stop-color=\"var(--ngx-audio-wave-color)\"\n />\n <stop\n [attr.offset]=\"progressOffset()\"\n stop-color=\"var(--ngx-audio-wave-color)\"\n stop-opacity=\"0.3\"\n />\n <stop\n offset=\"100%\"\n stop-color=\"var(--ngx-audio-wave-color)\"\n stop-opacity=\"0.3\"\n />\n </linearGradient>\n </defs>\n\n @for (rect of normalizedData(); track index; let index = $index) {\n <rect\n [attr.fill]=\"`url(#${gradientId})`\"\n [attr.height]=\"rect * visualHeight()\"\n [attr.rx]=\"rounded() ? 1 : 0\"\n [attr.ry]=\"rounded() ? 1 : 0\"\n [attr.width]=\"2\"\n [attr.x]=\"index * visualGap()\"\n [attr.y]=\"visualHeight() - rect * visualHeight()\"\n ></rect>\n }\n </svg>\n\n @if (isHovering()) {\n <span\n class=\"ngx-audio-wave-hover-guide\"\n [style.left.px]=\"hoverOffset()\"\n aria-hidden=\"true\"\n ></span>\n }\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;max-width:100%;cursor:pointer}.ngx-audio-wave svg{position:absolute;left:0;bottom:0;width:100%;height:100%}.ngx-audio-wave-hover-guide{position:absolute;top:-3px;bottom:-3px;width:2px;background-color:var(--ngx-audio-wave-color);border-radius:999px;box-shadow:0 0 0 2px #ffffffe6;opacity:.85;pointer-events:none;transform:translate(-50%)}.ngx-audio-wave-hover-guide:before{content:\"\";position:absolute;top:-4px;left:50%;width:6px;height:6px;background-color:var(--ngx-audio-wave-color);border-radius:50%;transform:translate(-50%)}.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-visible,.ngx-audio-wave-btn:focus-visible{outline:2px solid var(--ngx-audio-wave-color);outline-offset:2px}@media(prefers-reduced-motion:reduce){.ngx-audio-wave-loading span{animation:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NgxAudioWave, decorators: [{
type: Component,
args: [{ selector: 'ngx-audio-wave', changeDetection: ChangeDetectionStrategy.OnPush, template: "<audio\n #audioRef\n [src]=\"audioElementSrc()\"\n (durationchange)=\"durationChange($event)\"\n (loadedmetadata)=\"durationChange($event)\"\n (pause)=\"pauseChange($event)\"\n (play)=\"pauseChange($event)\"\n (timeupdate)=\"timeChange($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-orientation]=\"'horizontal'\"\n [attr.aria-valuemax]=\"100\"\n [attr.aria-valuemin]=\"0\"\n [attr.aria-valuenow]=\"exactPlayedPercent()\"\n [attr.aria-valuetext]=\"progressText()\"\n [style.height.px]=\"visualHeight()\"\n [style.width.px]=\"width()\"\n (click)=\"setTime($event)\"\n (keydown)=\"onKeyDown($event)\"\n (mouseleave)=\"clearHoverPosition()\"\n (mousemove)=\"setHoverPosition($event)\"\n role=\"slider\"\n tabindex=\"0\"\n >\n @if (!isLoading()) {\n <svg\n class=\"ngx-audio-wave-svg\"\n [attr.viewBox]=\"`0 0 ${width()} ${visualHeight()}`\"\n aria-hidden=\"true\"\n >\n <defs>\n <linearGradient\n [attr.id]=\"gradientId\"\n [attr.x2]=\"width()\"\n gradientUnits=\"userSpaceOnUse\"\n x1=\"0\"\n y1=\"0\"\n y2=\"0\"\n >\n <stop offset=\"0%\" stop-color=\"var(--ngx-audio-wave-color)\" />\n <stop\n [attr.offset]=\"progressOffset()\"\n stop-color=\"var(--ngx-audio-wave-color)\"\n />\n <stop\n [attr.offset]=\"progressOffset()\"\n stop-color=\"var(--ngx-audio-wave-color)\"\n stop-opacity=\"0.3\"\n />\n <stop\n offset=\"100%\"\n stop-color=\"var(--ngx-audio-wave-color)\"\n stop-opacity=\"0.3\"\n />\n </linearGradient>\n </defs>\n\n @for (rect of normalizedData(); track index; let index = $index) {\n <rect\n [attr.fill]=\"`url(#${gradientId})`\"\n [attr.height]=\"rect * visualHeight()\"\n [attr.rx]=\"rounded() ? 1 : 0\"\n [attr.ry]=\"rounded() ? 1 : 0\"\n [attr.width]=\"2\"\n [attr.x]=\"index * visualGap()\"\n [attr.y]=\"visualHeight() - rect * visualHeight()\"\n ></rect>\n }\n </svg>\n\n @if (isHovering()) {\n <span\n class=\"ngx-audio-wave-hover-guide\"\n [style.left.px]=\"hoverOffset()\"\n aria-hidden=\"true\"\n ></span>\n }\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;max-width:100%;cursor:pointer}.ngx-audio-wave svg{position:absolute;left:0;bottom:0;width:100%;height:100%}.ngx-audio-wave-hover-guide{position:absolute;top:-3px;bottom:-3px;width:2px;background-color:var(--ngx-audio-wave-color);border-radius:999px;box-shadow:0 0 0 2px #ffffffe6;opacity:.85;pointer-events:none;transform:translate(-50%)}.ngx-audio-wave-hover-guide:before{content:\"\";position:absolute;top:-4px;left:50%;width:6px;height:6px;background-color:var(--ngx-audio-wave-color);border-radius:50%;transform:translate(-50%)}.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-visible,.ngx-audio-wave-btn:focus-visible{outline:2px solid var(--ngx-audio-wave-color);outline-offset:2px}@media(prefers-reduced-motion:reduce){.ngx-audio-wave-loading span{animation:none}}\n"] }]
}], ctorParameters: () => [], propDecorators: { audioSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "audioSrc", required: true }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], hideBtn: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideBtn", required: false }] }], skip: [{ type: i0.Input, args: [{ isSignal: true, alias: "skip", required: false }] }], volume: [{ type: i0.Input, args: [{ isSignal: true, alias: "volume", required: false }] }], playbackRate: [{ type: i0.Input, args: [{ isSignal: true, alias: "playbackRate", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], samples: [{ type: i0.Input, args: [{ isSignal: true, alias: "samples", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], playButtonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "playButtonLabel", required: false }] }], pauseButtonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pauseButtonLabel", required: false }] }], progressBarLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "progressBarLabel", required: false }] }], audioRef: [{ type: i0.ViewChild, args: ['audioRef', { isSignal: true }] }] } });
/*
* Public API Surface of ngx-audio-wave
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxAudioWave, filterAudioBufferData, normalizeAudioData };
//# sourceMappingURL=ngx-audio-wave.mjs.map