@ngx-telly/plugin-vu-meter
Version:
Telly - Vu meter plugin
334 lines (326 loc) • 26.7 kB
JavaScript
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