UNPKG

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
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