// Looping ringtones. Two patterns: // - outgoing: long calling tone, 3s cycle (oscillator only) // - incoming: classic "ring ring" double beep (oscillator), optionally // upgraded to a user-supplied audio file stored in ringtoneStorage // // The custom file plays immediately if we can load it; otherwise we fall // back to the generated oscillator pattern so ringing never misses an // incoming call due to an IO failure. import { getAudioSettings, subscribeAudioSettings } from './audioSettings'; import { getIncomingRingtone } from './ringtoneStorage'; type Pattern = 'outgoing' | 'incoming'; class Ringtone { private ctx: AudioContext | null = null; private interval: number | null = null; private pattern: Pattern | null = null; // Custom-file playback path (incoming only). private audioEl: HTMLAudioElement | null = null; private customUrl: string | null = null; // Sequence token to ignore slow IO completing after user changed state. private startSeq = 0; // Live-subscribe so settings-slider changes reflect while the ringtone // is playing (user can hear the effect of their slider immediately). private unsubVolume: (() => void) | null = null; private get volume(): number { const v = getAudioSettings().ringtoneVolume; if (!Number.isFinite(v)) return 0.9; return Math.min(1, Math.max(0, v)); } start(pattern: Pattern): void { if (this.pattern === pattern) return; // already playing this pattern this.stop(); this.pattern = pattern; const seq = ++this.startSeq; // Track live slider moves so the user can dial in the volume while a // call is ringing and hear the change immediately. this.unsubVolume = subscribeAudioSettings(() => { if (this.audioEl) this.audioEl.volume = this.volume; }); if (pattern === 'incoming') { // Kick off oscillator immediately so we never miss ringing feedback // while the custom file (if any) loads asynchronously. Once the blob // is ready we hand playback over to the