diff --git a/apps/web/src/lib/SoundSettings.svelte b/apps/web/src/lib/SoundSettings.svelte new file mode 100644 index 0000000..f226eef --- /dev/null +++ b/apps/web/src/lib/SoundSettings.svelte @@ -0,0 +1,223 @@ + + + + + + +{#if open} +
+{/if} + + diff --git a/apps/web/src/lib/sounds.ts b/apps/web/src/lib/sounds.ts new file mode 100644 index 0000000..f4ffc61 --- /dev/null +++ b/apps/web/src/lib/sounds.ts @@ -0,0 +1,133 @@ +/** + * Notification sounds for peer join/leave events. + * + * Supports custom sounds stored as base64 data URLs in localStorage, + * with configurable volume. Falls back to Web Audio API generated tones. + */ + +const STORAGE_KEYS = { + joinSound: 'voca:sound:join', + leaveSound: 'voca:sound:leave', + volume: 'voca:sound:volume', +} as const; + +let audioCtx: AudioContext | null = null; + +function getAudioContext(): AudioContext { + if (!audioCtx) { + audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)(); + } + return audioCtx; +} + +/** Get the notification volume (0-1). Defaults to 0.5. */ +export function getVolume(): number { + try { + const stored = localStorage.getItem(STORAGE_KEYS.volume); + if (stored !== null) return Math.max(0, Math.min(1, parseFloat(stored))); + } catch { } + return 0.5; +} + +/** Set the notification volume (0-1). */ +export function setVolume(v: number) { + try { + localStorage.setItem(STORAGE_KEYS.volume, String(Math.max(0, Math.min(1, v)))); + } catch { } +} + +/** Get a custom sound data URL from localStorage. */ +export function getCustomSound(type: 'join' | 'leave'): string | null { + try { + return localStorage.getItem(type === 'join' ? STORAGE_KEYS.joinSound : STORAGE_KEYS.leaveSound); + } catch { + return null; + } +} + +/** Store a custom sound as a base64 data URL. */ +export function setCustomSound(type: 'join' | 'leave', dataUrl: string) { + try { + localStorage.setItem( + type === 'join' ? STORAGE_KEYS.joinSound : STORAGE_KEYS.leaveSound, + dataUrl, + ); + } catch { } +} + +/** Remove a custom sound, reverting to the default tone. */ +export function removeCustomSound(type: 'join' | 'leave') { + try { + localStorage.removeItem(type === 'join' ? STORAGE_KEYS.joinSound : STORAGE_KEYS.leaveSound); + } catch { } +} + +/** + * Read a File as a base64 data URL. + * Returns null if the file is too large (> 512KB). + */ +export function fileToDataUrl(file: File): Promise