202 lines
6.8 KiB
TypeScript
202 lines
6.8 KiB
TypeScript
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<AudioDevice[]>([]);
|
|
outputDevices = signal<AudioDevice[]>([]);
|
|
selectedInputDevice = signal<string>('');
|
|
selectedOutputDevice = signal<string>('');
|
|
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<void> {
|
|
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<void> {
|
|
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);
|
|
}
|
|
}
|