Files
cinny/src/app/features/call/CallSounds.ts

162 lines
4.1 KiB
TypeScript

/* 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, string> = {
[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<string> {
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<CallSoundType, HTMLAudioElement> = 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;
}
}