import { Component, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { lucideMic, lucideHeadphones, lucideAudioLines, lucideActivity } from '@ng-icons/lucide'; import { WebRTCService } from '../../../../core/services/webrtc.service'; import { VoiceLevelingService } from '../../../../core/services/voice-leveling.service'; import { NotificationAudioService, AppSound, } from '../../../../core/services/notification-audio.service'; import { STORAGE_KEY_VOICE_SETTINGS } from '../../../../core/constants'; interface AudioDevice { deviceId: string; label: string; } @Component({ selector: 'app-voice-settings', standalone: true, imports: [CommonModule, FormsModule, NgIcon], viewProviders: [ provideIcons({ lucideMic, lucideHeadphones, lucideAudioLines, lucideActivity, }), ], templateUrl: './voice-settings.component.html', }) export class VoiceSettingsComponent { private webrtcService = inject(WebRTCService); readonly voiceLeveling = inject(VoiceLevelingService); readonly audioService = inject(NotificationAudioService); inputDevices = signal([]); outputDevices = signal([]); selectedInputDevice = signal(''); selectedOutputDevice = signal(''); inputVolume = signal(100); outputVolume = signal(100); audioBitrate = signal(96); latencyProfile = signal<'low' | 'balanced' | 'high'>('balanced'); includeSystemAudio = signal(false); noiseReduction = signal(false); constructor() { this.loadVoiceSettings(); this.loadAudioDevices(); } async loadAudioDevices(): Promise { try { if (!navigator.mediaDevices?.enumerateDevices) return; const devices = await navigator.mediaDevices.enumerateDevices(); this.inputDevices.set( devices .filter((d) => d.kind === 'audioinput') .map((d) => ({ deviceId: d.deviceId, label: d.label })), ); this.outputDevices.set( devices .filter((d) => d.kind === 'audiooutput') .map((d) => ({ deviceId: d.deviceId, label: d.label })), ); } catch {} } loadVoiceSettings(): void { try { const raw = localStorage.getItem(STORAGE_KEY_VOICE_SETTINGS); if (!raw) return; const s = JSON.parse(raw); if (s.inputDevice) this.selectedInputDevice.set(s.inputDevice); if (s.outputDevice) this.selectedOutputDevice.set(s.outputDevice); if (typeof s.inputVolume === 'number') this.inputVolume.set(s.inputVolume); if (typeof s.outputVolume === 'number') this.outputVolume.set(s.outputVolume); if (typeof s.audioBitrate === 'number') this.audioBitrate.set(s.audioBitrate); if (s.latencyProfile) this.latencyProfile.set(s.latencyProfile); if (typeof s.includeSystemAudio === 'boolean') this.includeSystemAudio.set(s.includeSystemAudio); if (typeof s.noiseReduction === 'boolean') this.noiseReduction.set(s.noiseReduction); } catch {} if (this.noiseReduction() !== this.webrtcService.isNoiseReductionEnabled()) { this.webrtcService.toggleNoiseReduction(this.noiseReduction()); } } saveVoiceSettings(): void { try { localStorage.setItem( STORAGE_KEY_VOICE_SETTINGS, JSON.stringify({ inputDevice: this.selectedInputDevice(), outputDevice: this.selectedOutputDevice(), inputVolume: this.inputVolume(), outputVolume: this.outputVolume(), audioBitrate: this.audioBitrate(), latencyProfile: this.latencyProfile(), includeSystemAudio: this.includeSystemAudio(), noiseReduction: this.noiseReduction(), }), ); } catch {} } onInputDeviceChange(event: Event): void { const select = event.target as HTMLSelectElement; this.selectedInputDevice.set(select.value); this.saveVoiceSettings(); } onOutputDeviceChange(event: Event): void { const select = event.target as HTMLSelectElement; this.selectedOutputDevice.set(select.value); this.webrtcService.setOutputVolume(this.outputVolume() / 100); this.saveVoiceSettings(); } onInputVolumeChange(event: Event): void { const input = event.target as HTMLInputElement; this.inputVolume.set(parseInt(input.value, 10)); this.saveVoiceSettings(); } onOutputVolumeChange(event: Event): void { const input = event.target as HTMLInputElement; this.outputVolume.set(parseInt(input.value, 10)); this.webrtcService.setOutputVolume(this.outputVolume() / 100); this.saveVoiceSettings(); } onLatencyProfileChange(event: Event): void { const select = event.target as HTMLSelectElement; const profile = select.value as 'low' | 'balanced' | 'high'; this.latencyProfile.set(profile); this.webrtcService.setLatencyProfile(profile); this.saveVoiceSettings(); } onAudioBitrateChange(event: Event): void { const input = event.target as HTMLInputElement; this.audioBitrate.set(parseInt(input.value, 10)); this.webrtcService.setAudioBitrate(this.audioBitrate()); this.saveVoiceSettings(); } onIncludeSystemAudioChange(event: Event): void { const input = event.target as HTMLInputElement; this.includeSystemAudio.set(!!input.checked); this.saveVoiceSettings(); } async onNoiseReductionChange(): Promise { this.noiseReduction.update((v) => !v); await this.webrtcService.toggleNoiseReduction(this.noiseReduction()); this.saveVoiceSettings(); } /* ── Voice Leveling handlers ───────────────────────────────── */ onVoiceLevelingToggle(): void { this.voiceLeveling.setEnabled(!this.voiceLeveling.enabled()); } onTargetDbfsChange(event: Event): void { const input = event.target as HTMLInputElement; this.voiceLeveling.setTargetDbfs(parseInt(input.value, 10)); } onStrengthChange(event: Event): void { const select = event.target as HTMLSelectElement; this.voiceLeveling.setStrength(select.value as 'low' | 'medium' | 'high'); } onMaxGainDbChange(event: Event): void { const input = event.target as HTMLInputElement; this.voiceLeveling.setMaxGainDb(parseInt(input.value, 10)); } onSpeedChange(event: Event): void { const select = event.target as HTMLSelectElement; this.voiceLeveling.setSpeed(select.value as 'slow' | 'medium' | 'fast'); } onNoiseGateToggle(): void { this.voiceLeveling.setNoiseGate(!this.voiceLeveling.noiseGate()); } onNotificationVolumeChange(event: Event): void { const input = event.target as HTMLInputElement; this.audioService.setNotificationVolume(parseFloat(input.value)); } previewNotificationSound(): void { this.audioService.play(AppSound.Notification); } }