/* eslint-disable no-console */ /** * Sound types that can be played during a call */ export enum CallSoundType { MicMute = 'mic_mute', MicUnmute = 'mic_unmute', Deafen = 'deafen', Undeafen = 'undeafen', CallJoined = 'call_joined', CallDepart = 'call_depart', CallDialing = 'call_dialing', } const SOUND_FILES: Record = { [CallSoundType.MicMute]: 'Mic_Mute.ogg', [CallSoundType.MicUnmute]: 'Mic_Unmuted.ogg', [CallSoundType.Deafen]: 'Speakers_Deafen.ogg', [CallSoundType.Undeafen]: 'Speakers_Undeafen.ogg', [CallSoundType.CallJoined]: 'Call_Joined.ogg', [CallSoundType.CallDepart]: 'Call_Depart.ogg', [CallSoundType.CallDialing]: 'Call_Dialing.ogg', }; /** * Resolves the base URL for sounds. * In Electron production, sounds are in resources/sound/ as plain files. * In dev or browser, sounds are served from public/sound/. */ async function resolveSoundBaseUrl(): Promise { const electronAudio = (window as any).electron?.audio; if (electronAudio?.getSoundBaseUrl) { const base: string | null = await electronAudio.getSoundBaseUrl(); if (base) return base; } return './sound'; } /** * Manages call-related sound effects. * Handles playing local sounds and coordinating with remote participants. */ export class CallSounds { private audioElements: Map = new Map(); private dialingLoopElement: HTMLAudioElement | null = null; private isDeafened = false; constructor() { resolveSoundBaseUrl().then((base) => this.preloadSounds(base)); } /** * Preloads all call sounds for instant playback */ private preloadSounds(base: string): void { Object.entries(SOUND_FILES).forEach(([type, file]) => { const audio = new Audio(`${base}/${file}`); audio.preload = 'auto'; this.audioElements.set(type as CallSoundType, audio); }); } /** * Plays a call sound locally * @param soundType - The type of sound to play * @param loop - Whether to loop the sound (e.g., for dialing) */ playSound(soundType: CallSoundType, loop = false): void { // Don't play sounds if deafened (except deafen/undeafen sounds for feedback) if (this.isDeafened && soundType !== CallSoundType.Deafen && soundType !== CallSoundType.Undeafen) { return; } const audio = this.audioElements.get(soundType); if (!audio) { console.warn(`Sound not found: ${soundType}`); return; } // Stop any currently playing instance and reset audio.currentTime = 0; audio.loop = loop; // Track looping dialing sound separately if (soundType === CallSoundType.CallDialing && loop) { this.dialingLoopElement = audio; } audio.play().catch((err) => { console.warn(`Failed to play sound ${soundType}:`, err); }); } /** * Stops the dialing loop sound if playing */ stopDialingLoop(): void { if (this.dialingLoopElement) { this.dialingLoopElement.pause(); this.dialingLoopElement.currentTime = 0; this.dialingLoopElement.loop = false; this.dialingLoopElement = null; } } /** * Sets the deafened state - affects whether sounds are played * @param deafened - Whether the user is deafened */ setDeafened(deafened: boolean): void { this.isDeafened = deafened; } /** * Gets whether sounds are currently deafened */ getIsDeafened(): boolean { return this.isDeafened; } /** * Cleans up audio resources */ dispose(): void { this.stopDialingLoop(); this.audioElements.forEach((audio) => { audio.pause(); }); this.audioElements.clear(); } } /** * Singleton instance of CallSounds for app-wide use */ let callSoundsInstance: CallSounds | null = null; /** * Gets or creates the CallSounds singleton instance */ export function getCallSounds(): CallSounds { if (!callSoundsInstance) { callSoundsInstance = new CallSounds(); } return callSoundsInstance; } /** * Disposes the CallSounds singleton */ export function disposeCallSounds(): void { if (callSoundsInstance) { callSoundsInstance.dispose(); callSoundsInstance = null; } }