UNPKG

@ngx-telly/plugin-vu-meter

Version:

Telly - Vu meter plugin

334 lines (326 loc) 26.7 kB
import * as i0 from '@angular/core'; import { Injectable, inject, signal, Input, HostBinding, Component, input, HostListener, makeEnvironmentProviders } from '@angular/core'; import { BehaviorSubject, Subject, Subscription, tap, map } from 'rxjs'; import { PlayerComponent } from '@ngx-telly/player'; import { NgIf, NgFor, AsyncPipe } from '@angular/common'; class VuMeterService { show$ = new BehaviorSubject(false); reconfigure$ = new Subject(); toggle() { this.show$.next(!this.show$.value); } reconfigure() { this.reconfigure$.next(); } isVisible() { return this.show$.value; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: VuMeterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: VuMeterService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: VuMeterService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); class TellyVuMeterComponent { display = 'none'; fftSize = 1024; channelCount = 2; channelNames = []; peakHoldTime = 300; peakDecayRate = 0.1; peakAccelerationFactor = 4; peakSmoothingFactor = 1; updateRateMs = 40; service = inject(VuMeterService); player = inject(PlayerComponent); subscription = new Subscription(); audioNodes = { context: undefined, splitter: undefined, source: undefined, analyzers: [], dataArrays: [], }; analysisState = { isAnalyzing: false, peakHoldValues: [], peakHoldTimers: [], }; levels = signal([]); reconfiguring = signal(false); ngOnInit() { this.setupVisibilitySubscription(); } ngOnChanges(changes) { if (changes['channelCount'] && !changes['channelCount'].isFirstChange()) { this.updateChannelCount(changes['channelCount'].currentValue); } } ngOnDestroy() { this.stopAnalysis(); this.disconnectAudioGraph(); this.cleanupAudioContext(); this.subscription.unsubscribe(); } trackByChannel(index, level) { return level.channel; } setupVisibilitySubscription() { this.subscription.add(this.service.show$ .pipe(tap((show) => this.player.media.indicate(`VU Meter ${show ? 'ON' : 'OFF'}`)), tap((show) => { if (show) { this.initializeAudioAndStartAnalysis(); } else { this.stopAnalysis(); this.disconnectAudioGraph(); } }), map((show) => (show ? 'flex' : 'none'))) .subscribe((display) => (this.display = display))); this.subscription.add(this.service.reconfigure$.subscribe(() => this.reconfigure())); } reconfigure() { this.reconfiguring.set(true); this.stopAnalysis(); this.disconnectAudioGraph(); setTimeout(() => { this.reconfiguring.set(false); this.initializeAudioAndStartAnalysis(); }, 1000); } updateChannelCount(newCount) { this.channelCount = newCount; if (this.audioNodes.source && this.audioNodes.context) { const wasAnalyzing = this.analysisState.isAnalyzing; this.stopAnalysis(); this.disconnectAudioGraph(); if (wasAnalyzing) { this.connectAudioGraphAndStartAnalysis(); } } } initializeAudioAndStartAnalysis() { if (!this.audioNodes.context) { this.initializeAudioContext() .then(() => { this.connectAudioGraphAndStartAnalysis(); }) .catch((error) => { console.error('Failed to initialize audio for VU meter:', error); }); } else { this.connectAudioGraphAndStartAnalysis(); } } async initializeAudioContext() { if (this.audioNodes.context) { return; } this.audioNodes.context = new AudioContext(); await this.audioNodes.context.resume(); if (!this.audioNodes.source && this.audioNodes.context) { this.audioNodes.source = this.audioNodes.context.createMediaElementSource(this.player.video.nativeElement); this.audioNodes.source.connect(this.audioNodes.context.destination); } } connectAudioGraphAndStartAnalysis() { if (!this.audioNodes.context || !this.audioNodes.source) { console.warn('Audio context or source not initialized'); return; } this.buildAudioGraph(); this.startAnalysis(); } buildAudioGraph() { if (!this.audioNodes.context || !this.audioNodes.source) return; this.audioNodes.splitter = this.audioNodes.context.createChannelSplitter(this.channelCount); this.audioNodes.analyzers = Array(this.channelCount) .fill(0) .map(() => { const analyzer = this.audioNodes.context.createAnalyser(); analyzer.fftSize = this.fftSize; analyzer.smoothingTimeConstant = 0.8; return analyzer; }); this.audioNodes.source.connect(this.audioNodes.splitter); this.audioNodes.analyzers.forEach((analyzer, i) => { this.audioNodes.splitter.connect(analyzer, i); }); this.audioNodes.dataArrays = this.audioNodes.analyzers.map((analyzer) => new Float32Array(analyzer.fftSize)); } startAnalysis() { if (this.analysisState.isAnalyzing) { return; } this.analysisState.isAnalyzing = true; this.analysisState.peakHoldValues = new Array(this.channelCount).fill(0); this.analysisState.peakHoldTimers = new Array(this.channelCount).fill(0); const channelNames = this.getChannelNames(this.channelCount); this.runAnalysisLoop(channelNames); } runAnalysisLoop(channelNames) { if (!this.analysisState.isAnalyzing) { return; } this.audioNodes.analyzers.forEach((analyzer, i) => { analyzer.getFloatTimeDomainData(this.audioNodes.dataArrays[i]); }); const levels = this.audioNodes.dataArrays.map((dataArray, i) => this.calculateChannelLevel(dataArray, channelNames[i], i)); this.levels.set(levels); this.analysisState.animationFrame = requestAnimationFrame(() => { setTimeout(() => this.runAnalysisLoop(channelNames), this.updateRateMs); }); } calculateChannelLevel(dataArray, channelName, channelIndex) { let sumSquares = 0; for (let i = 0; i < dataArray.length; i++) { sumSquares += dataArray[i] * dataArray[i]; } const rms = Math.sqrt(sumSquares / dataArray.length); const db = Math.round(20 * Math.log10(Math.max(rms, 0.0000001))); const normalized = Math.max(0, Math.min(1, (db + 60) / 60)); const peak = this.updatePeakHold(normalized, channelIndex); return { channel: channelName, rms, db, normalized, peak, }; } updatePeakHold(currentLevel, channelIndex) { if (currentLevel > this.analysisState.peakHoldValues[channelIndex]) { this.analysisState.peakHoldValues[channelIndex] = currentLevel; this.analysisState.peakHoldTimers[channelIndex] = Date.now(); } else { const timeSinceLastPeak = Date.now() - this.analysisState.peakHoldTimers[channelIndex]; if (timeSinceLastPeak > this.peakHoldTime) { const decayTime = timeSinceLastPeak - this.peakHoldTime; const decaySeconds = decayTime / 1000; const smoothingFactor = Math.min(decaySeconds, 1); const currentDecayRate = this.peakDecayRate * Math.pow(this.peakAccelerationFactor, decaySeconds) * smoothingFactor; this.analysisState.peakHoldValues[channelIndex] = Math.max(0, this.analysisState.peakHoldValues[channelIndex] - currentDecayRate); } } return this.analysisState.peakHoldValues[channelIndex]; } stopAnalysis() { this.analysisState.isAnalyzing = false; if (this.analysisState.animationFrame) { cancelAnimationFrame(this.analysisState.animationFrame); this.analysisState.animationFrame = undefined; } } getChannelNames(count) { if (this.channelNames.length) { return this.channelNames; } switch (count) { case 1: return ['M']; case 2: return ['L', 'R']; case 3: return ['L', 'R', 'LFE']; case 4: return ['FL', 'FR', 'SL', 'SR']; case 5: return ['FL', 'FR', 'C', 'SL', 'SR']; case 6: return ['FL', 'FR', 'C', 'LFE', 'SL', 'SR']; case 7: return ['FL', 'FR', 'C', 'LFE', 'SL', 'SR', 'BC']; case 8: return ['FL', 'FR', 'C', 'LFE', 'SL', 'SR', 'BL', 'BR']; default: return Array(count) .fill(0) .map((_, i) => `C${i + 1}`); } } disconnectAudioGraph() { if (this.audioNodes.splitter) { this.audioNodes.analyzers.forEach((analyzer) => { analyzer.disconnect(); }); this.audioNodes.splitter.disconnect(); } this.audioNodes.splitter = undefined; this.audioNodes.analyzers = []; this.audioNodes.dataArrays = []; } cleanupAudioContext() { if (this.audioNodes.source && this.audioNodes.context) { this.audioNodes.source.disconnect(); } if (this.audioNodes.context) { this.audioNodes.context.suspend().catch((err) => { console.error('Error suspending audio context:', err); }); } this.audioNodes.source = undefined; this.audioNodes.context = undefined; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: TellyVuMeterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.9", type: TellyVuMeterComponent, isStandalone: true, selector: "telly-vu-meter", inputs: { fftSize: "fftSize", channelCount: "channelCount", channelNames: "channelNames", peakHoldTime: "peakHoldTime", peakDecayRate: "peakDecayRate", peakAccelerationFactor: "peakAccelerationFactor", peakSmoothingFactor: "peakSmoothingFactor", updateRateMs: "updateRateMs" }, host: { properties: { "style.display": "this.display" } }, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"(service.show$ | async) && !reconfiguring()\">\n <div class=\"vu-meter-container\">\n <div class=\"vu-meter-panel\">\n <div class=\"vu-meter-channels\">\n <div class=\"channel-meter\" *ngFor=\"let level of levels(); trackBy: trackByChannel\">\n <div class=\"channel-label\">{{ level.channel }}</div>\n <div class=\"meter-container\">\n <div class=\"meter-background\">\n <div class=\"meter-segment green\"></div>\n <div class=\"meter-segment green\"></div>\n <div class=\"meter-segment green\"></div>\n <div class=\"meter-segment green\"></div>\n <div class=\"meter-segment yellow\"></div>\n <div class=\"meter-segment yellow\"></div>\n <div class=\"meter-segment red\"></div>\n <div class=\"meter-segment red\"></div>\n </div>\n <div class=\"meter-background-full\"\n [style.clip-path]=\"'polygon(0 100%, 100% 100%, 100% ' + (100 - level.normalized * 100) + '%, 0 ' + (100 - level.normalized * 100) + '%)'\">\n <div class=\"meter-segment-full green\"></div>\n <div class=\"meter-segment-full green\"></div>\n <div class=\"meter-segment-full green\"></div>\n <div class=\"meter-segment-full green\"></div>\n <div class=\"meter-segment-full yellow\"></div>\n <div class=\"meter-segment-full yellow\"></div>\n <div class=\"meter-segment-full red\"></div>\n <div class=\"meter-segment-full red\"></div>\n </div>\n <div class=\"peak-indicator\"\n [style.bottom.%]=\"level.peak * 100\"\n [class.visible]=\"level.peak > 0.01\"></div>\n </div>\n </div>\n </div>\n </div>\n </div>\n</ng-container>\n\n<ng-container *ngIf=\"reconfiguring()\">\n <div class=\"vu-meter-container\">\n <div class=\"vu-meter-panel\">\n <div class=\"vu-meter-channels\">\n <div class=\"channel-meter\">\n <div class=\"loading-info\">\n <div>Audio</div>\n <div>Loading</div>\n <div class=\"loading-animation\">\n <div class=\"spinner\"></div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n</ng-container>\n", styles: [":host{color:var(--telly-text)!important;position:absolute;right:1rem;bottom:6rem;z-index:1000}.vu-meter-container{padding:16px;font-family:monospace;border-radius:12px;border:1px solid rgba(54,54,54,.05);box-shadow:0 5px 15px #0000007f;box-sizing:border-box;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);background-blend-mode:overlay;background-color:#50505080;color:#fffc}.vu-meter-panel{display:flex;flex-direction:column;gap:12px}.db-mark{position:relative}.vu-meter-channels{display:flex;flex-direction:row;gap:12px;justify-content:center}.channel-meter{display:flex;flex-direction:column;align-items:center;gap:8px;min-width:32px}.channel-label{color:#2d2;font-size:11px;font-weight:700;min-width:32px;text-align:center;background:#ffffff1a;border-radius:4px;padding:2px 4px;border:1px solid rgba(255,255,255,.1)}.meter-container{position:relative;width:28px;height:140px;background:linear-gradient(180deg,#1414141a,#0a0a0a33,#05050566);border-radius:8px;border:1px solid rgba(0,0,0,.2);box-shadow:inset 0 2px 8px #0000001a,inset 0 -1px 2px #ffffff0d,0 2px 4px #0000001a;overflow:hidden}.meter-container:before{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,rgba(255,255,255,.03) 0%,transparent 30%,transparent 70%,rgba(255,255,255,.02) 100%);border-radius:6px;pointer-events:none}.meter-background{position:absolute;inset:0;display:flex;flex-direction:column-reverse}.meter-segment{flex:1;border-top:1px solid rgba(0,0,0,.2);position:relative}.meter-segment:last-child{border-top:none}.meter-segment.green{background:linear-gradient(180deg,#44ff4440,#22c82226);box-shadow:inset 0 1px #44ff441a}.meter-segment.yellow{background:linear-gradient(180deg,#ffff4440,#c8c82226);box-shadow:inset 0 1px #ffff441a}.meter-segment.red{background:linear-gradient(180deg,#ff444440,#c8222226);box-shadow:inset 0 1px #ff44441a}.meter-segment-full.green{background:linear-gradient(180deg,#6f6,#4f4,#2d2);box-shadow:0 0 8px #4f4c,inset 0 1px #ffffff4d,inset 0 -1px #0003}.meter-segment-full.yellow{background:linear-gradient(180deg,#ff6,#ff4,#dd2);box-shadow:0 0 8px #ff4c,inset 0 1px #ffffff4d,inset 0 -1px #0003}.meter-segment-full.red{background:linear-gradient(180deg,#f66,#f44,#d22);box-shadow:0 0 8px #f44c,inset 0 1px #ffffff4d,inset 0 -1px #0003}.meter-background-full{position:absolute;inset:0;display:flex;flex-direction:column-reverse;transition:clip-path .08s ease-out;border-radius:6px;overflow:hidden}.meter-segment-full{flex:1;border-top:1px solid rgba(0,0,0,.2);position:relative}.meter-segment-full:last-child{border-top:none}.meter-segment-full:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,rgba(255,255,255,.1) 0%,transparent 50%,rgba(255,255,255,.05) 100%);pointer-events:none}.peak-indicator{position:absolute;left:0;right:0;height:2px;background:linear-gradient(90deg,#fffc,#fff,#fffc);box-shadow:0 0 8px #ffffffe6,0 0 4px #fff9,inset 0 1px #fff6;opacity:0;transition:opacity .15s ease-out;border-radius:2px;z-index:10}.peak-indicator.visible{opacity:1}.loading-info>div{color:#2d2;text-align:center;min-height:12px}.loading-animation{margin-top:8px;display:flex;align-items:center;justify-content:center;gap:8px}.spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-radius:50%;border-top-color:#2d2;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: TellyVuMeterComponent, decorators: [{ type: Component, args: [{ selector: 'telly-vu-meter', imports: [NgIf, NgFor, AsyncPipe], template: "<ng-container *ngIf=\"(service.show$ | async) && !reconfiguring()\">\n <div class=\"vu-meter-container\">\n <div class=\"vu-meter-panel\">\n <div class=\"vu-meter-channels\">\n <div class=\"channel-meter\" *ngFor=\"let level of levels(); trackBy: trackByChannel\">\n <div class=\"channel-label\">{{ level.channel }}</div>\n <div class=\"meter-container\">\n <div class=\"meter-background\">\n <div class=\"meter-segment green\"></div>\n <div class=\"meter-segment green\"></div>\n <div class=\"meter-segment green\"></div>\n <div class=\"meter-segment green\"></div>\n <div class=\"meter-segment yellow\"></div>\n <div class=\"meter-segment yellow\"></div>\n <div class=\"meter-segment red\"></div>\n <div class=\"meter-segment red\"></div>\n </div>\n <div class=\"meter-background-full\"\n [style.clip-path]=\"'polygon(0 100%, 100% 100%, 100% ' + (100 - level.normalized * 100) + '%, 0 ' + (100 - level.normalized * 100) + '%)'\">\n <div class=\"meter-segment-full green\"></div>\n <div class=\"meter-segment-full green\"></div>\n <div class=\"meter-segment-full green\"></div>\n <div class=\"meter-segment-full green\"></div>\n <div class=\"meter-segment-full yellow\"></div>\n <div class=\"meter-segment-full yellow\"></div>\n <div class=\"meter-segment-full red\"></div>\n <div class=\"meter-segment-full red\"></div>\n </div>\n <div class=\"peak-indicator\"\n [style.bottom.%]=\"level.peak * 100\"\n [class.visible]=\"level.peak > 0.01\"></div>\n </div>\n </div>\n </div>\n </div>\n </div>\n</ng-container>\n\n<ng-container *ngIf=\"reconfiguring()\">\n <div class=\"vu-meter-container\">\n <div class=\"vu-meter-panel\">\n <div class=\"vu-meter-channels\">\n <div class=\"channel-meter\">\n <div class=\"loading-info\">\n <div>Audio</div>\n <div>Loading</div>\n <div class=\"loading-animation\">\n <div class=\"spinner\"></div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n</ng-container>\n", styles: [":host{color:var(--telly-text)!important;position:absolute;right:1rem;bottom:6rem;z-index:1000}.vu-meter-container{padding:16px;font-family:monospace;border-radius:12px;border:1px solid rgba(54,54,54,.05);box-shadow:0 5px 15px #0000007f;box-sizing:border-box;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);background-blend-mode:overlay;background-color:#50505080;color:#fffc}.vu-meter-panel{display:flex;flex-direction:column;gap:12px}.db-mark{position:relative}.vu-meter-channels{display:flex;flex-direction:row;gap:12px;justify-content:center}.channel-meter{display:flex;flex-direction:column;align-items:center;gap:8px;min-width:32px}.channel-label{color:#2d2;font-size:11px;font-weight:700;min-width:32px;text-align:center;background:#ffffff1a;border-radius:4px;padding:2px 4px;border:1px solid rgba(255,255,255,.1)}.meter-container{position:relative;width:28px;height:140px;background:linear-gradient(180deg,#1414141a,#0a0a0a33,#05050566);border-radius:8px;border:1px solid rgba(0,0,0,.2);box-shadow:inset 0 2px 8px #0000001a,inset 0 -1px 2px #ffffff0d,0 2px 4px #0000001a;overflow:hidden}.meter-container:before{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,rgba(255,255,255,.03) 0%,transparent 30%,transparent 70%,rgba(255,255,255,.02) 100%);border-radius:6px;pointer-events:none}.meter-background{position:absolute;inset:0;display:flex;flex-direction:column-reverse}.meter-segment{flex:1;border-top:1px solid rgba(0,0,0,.2);position:relative}.meter-segment:last-child{border-top:none}.meter-segment.green{background:linear-gradient(180deg,#44ff4440,#22c82226);box-shadow:inset 0 1px #44ff441a}.meter-segment.yellow{background:linear-gradient(180deg,#ffff4440,#c8c82226);box-shadow:inset 0 1px #ffff441a}.meter-segment.red{background:linear-gradient(180deg,#ff444440,#c8222226);box-shadow:inset 0 1px #ff44441a}.meter-segment-full.green{background:linear-gradient(180deg,#6f6,#4f4,#2d2);box-shadow:0 0 8px #4f4c,inset 0 1px #ffffff4d,inset 0 -1px #0003}.meter-segment-full.yellow{background:linear-gradient(180deg,#ff6,#ff4,#dd2);box-shadow:0 0 8px #ff4c,inset 0 1px #ffffff4d,inset 0 -1px #0003}.meter-segment-full.red{background:linear-gradient(180deg,#f66,#f44,#d22);box-shadow:0 0 8px #f44c,inset 0 1px #ffffff4d,inset 0 -1px #0003}.meter-background-full{position:absolute;inset:0;display:flex;flex-direction:column-reverse;transition:clip-path .08s ease-out;border-radius:6px;overflow:hidden}.meter-segment-full{flex:1;border-top:1px solid rgba(0,0,0,.2);position:relative}.meter-segment-full:last-child{border-top:none}.meter-segment-full:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,rgba(255,255,255,.1) 0%,transparent 50%,rgba(255,255,255,.05) 100%);pointer-events:none}.peak-indicator{position:absolute;left:0;right:0;height:2px;background:linear-gradient(90deg,#fffc,#fff,#fffc);box-shadow:0 0 8px #ffffffe6,0 0 4px #fff9,inset 0 1px #fff6;opacity:0;transition:opacity .15s ease-out;border-radius:2px;z-index:10}.peak-indicator.visible{opacity:1}.loading-info>div{color:#2d2;text-align:center;min-height:12px}.loading-animation{margin-top:8px;display:flex;align-items:center;justify-content:center;gap:8px}.spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-radius:50%;border-top-color:#2d2;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n"] }] }], propDecorators: { display: [{ type: HostBinding, args: ['style.display'] }], fftSize: [{ type: Input }], channelCount: [{ type: Input }], channelNames: [{ type: Input }], peakHoldTime: [{ type: Input }], peakDecayRate: [{ type: Input }], peakAccelerationFactor: [{ type: Input }], peakSmoothingFactor: [{ type: Input }], updateRateMs: [{ type: Input }] } }); const equalizer = 'M10,20H14V4H10V20M4,20H8V12H4V20M16,9V20H20V9H16Z'; const gauge = 'M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12C20,14.4 19,16.5 17.3,18C15.9,16.7 14,16 12,16C10,16 8.2,16.7 6.7,18C5,16.5 4,14.4 4,12A8,8 0 0,1 12,4M14,5.89C13.62,5.9 13.26,6.15 13.1,6.54L11.81,9.77L11.71,10C11,10.13 10.41,10.6 10.14,11.26C9.73,12.29 10.23,13.45 11.26,13.86C12.29,14.27 13.45,13.77 13.86,12.74C14.12,12.08 14,11.32 13.57,10.76L13.67,10.5L14.96,7.29L14.97,7.26C15.17,6.75 14.92,6.17 14.41,5.96C14.28,5.91 14.15,5.89 14,5.89M10,6A1,1 0 0,0 9,7A1,1 0 0,0 10,8A1,1 0 0,0 11,7A1,1 0 0,0 10,6M7,9A1,1 0 0,0 6,10A1,1 0 0,0 7,11A1,1 0 0,0 8,10A1,1 0 0,0 7,9M17,9A1,1 0 0,0 16,10A1,1 0 0,0 17,11A1,1 0 0,0 18,10A1,1 0 0,0 17,9'; class TellyVuMeterToggleComponent { icon = input(gauge); size = input('1.4rem'); service = inject(VuMeterService); title = 'VU Meter'; onClick() { this.service.toggle(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: TellyVuMeterToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.9", type: TellyVuMeterToggleComponent, isStandalone: true, selector: "telly-vu-meter-toggle", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerdown": "onClick()" }, properties: { "attr.tip": "this.title" } }, ngImport: i0, template: "<svg\n viewBox=\"0 0 24 24\"\n [style.width]=\"size()\"\n>\n <path\n fill=\"white\"\n [attr.d]=\"icon()\"\n />\n</svg>\n\n", styles: [""] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: TellyVuMeterToggleComponent, decorators: [{ type: Component, args: [{ selector: 'telly-vu-meter-toggle', imports: [], template: "<svg\n viewBox=\"0 0 24 24\"\n [style.width]=\"size()\"\n>\n <path\n fill=\"white\"\n [attr.d]=\"icon()\"\n />\n</svg>\n\n" }] }], propDecorators: { title: [{ type: HostBinding, args: ['attr.tip'] }], onClick: [{ type: HostListener, args: ['pointerdown'] }] } }); function provideTellyVuMeter() { return makeEnvironmentProviders([VuMeterService]); } /** * Generated bundle index. Do not edit. */ export { TellyVuMeterComponent, TellyVuMeterToggleComponent, VuMeterService, provideTellyVuMeter }; //# sourceMappingURL=ngx-telly-plugin-vu-meter.mjs.map