Update to delay ringtone logic.

This commit is contained in:
Half-Shot
2025-09-15 10:29:00 +01:00
parent 055c4c5ae7
commit f3131c945d
3 changed files with 80 additions and 26 deletions

View File

@@ -356,6 +356,16 @@ export const InCallView: FC<InCallViewProps> = ({
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$); const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
useSubscription(vm.autoLeave$, onLeave); useSubscription(vm.autoLeave$, onLeave);
const ringDelay = (pickupPhaseAudio?.soundDuration["waiting"] ?? 1) * 2;
useEffect(() => {
window.document.body.style.setProperty(
"--call-ring-duration-s",
`${ringDelay}s`,
);
window.document.body.style.setProperty("--call-ring-delay-s", `1s`);
}, [pickupPhaseAudio?.soundDuration, ringDelay]);
// When we enter timeout or decline we will leave the call. // When we enter timeout or decline we will leave the call.
useEffect((): void | (() => void) => { useEffect((): void | (() => void) => {
if (callPickupState === "timeout") { if (callPickupState === "timeout") {
@@ -384,13 +394,16 @@ export const InCallView: FC<InCallViewProps> = ({
// When waiting for pickup, loop a waiting sound // When waiting for pickup, loop a waiting sound
useEffect((): void | (() => void) => { useEffect((): void | (() => void) => {
if (callPickupState !== "ringing" || !pickupPhaseAudio) return; if (callPickupState !== "ringing" || !pickupPhaseAudio) return;
const endSound = pickupPhaseAudio.playSoundLooping("waiting"); const endSound = pickupPhaseAudio.playSoundLooping(
"waiting",
ringDelay / 2,
);
return () => { return () => {
void endSound().catch((e) => { void endSound().catch((e) => {
logger.error("Failed to stop ringing sound", e); logger.error("Failed to stop ringing sound", e);
}); });
}; };
}, [callPickupState, pickupPhaseAudio]); }, [callPickupState, pickupPhaseAudio, ringDelay]);
// Waiting UI overlay // Waiting UI overlay
const waitingOverlay: JSX.Element | null = useMemo(() => { const waitingOverlay: JSX.Element | null = useMemo(() => {

View File

@@ -25,10 +25,12 @@
inset: -12px; inset: -12px;
border-radius: 9999px; border-radius: 9999px;
border: 12px solid rgba(255, 255, 255, 0.6); border: 12px solid rgba(255, 255, 255, 0.6);
animation: pulse 1.6s ease-out infinite; animation: pulse var(--call-ring-duration-s) ease-out infinite;
animation-delay: 1s;
opacity: 0;
} }
.text { .pulse::before .text {
color: var(--cpd-color-text-on-solid-primary); color: var(--cpd-color-text-on-solid-primary);
} }
@@ -36,13 +38,24 @@
0% { 0% {
transform: scale(0.95); transform: scale(0.95);
opacity: 0.7; opacity: 0.7;
transform: scale(0);
opacity: 1;
} }
70% { 35% {
transform: scale(1.15); transform: scale(1.15);
opacity: 0.15; opacity: 0.15;
} }
100% { 50% {
transform: scale(1.2); transform: scale(1.2);
opacity: 0; opacity: 0;
} }
50.01% {
transform: scale(0);
}
85% {
transform: scale(0);
}
100% {
transform: scale(0);
}
} }

View File

@@ -32,6 +32,8 @@ async function playSound(
buffer: AudioBuffer, buffer: AudioBuffer,
volume: number, volume: number,
stereoPan: number, stereoPan: number,
delayS = 0,
abort?: AbortController,
): Promise<void> { ): Promise<void> {
const gain = ctx.createGain(); const gain = ctx.createGain();
gain.gain.setValueAtTime(volume, 0); gain.gain.setValueAtTime(volume, 0);
@@ -39,10 +41,13 @@ async function playSound(
pan.pan.setValueAtTime(stereoPan, 0); pan.pan.setValueAtTime(stereoPan, 0);
const src = ctx.createBufferSource(); const src = ctx.createBufferSource();
src.buffer = buffer; src.buffer = buffer;
src.connect(gain).connect(pan).connect(ctx.destination); abort?.signal.addEventListener("abort", () => {
src.disconnect();
});
const p = new Promise<void>((r) => src.addEventListener("ended", () => r())); const p = new Promise<void>((r) => src.addEventListener("ended", () => r()));
src.connect(gain).connect(pan).connect(ctx.destination);
controls.setPlaybackStarted(); controls.setPlaybackStarted();
src.start(); src.start(ctx.currentTime + delayS);
return p; return p;
} }
@@ -60,23 +65,35 @@ function playSoundLooping(
buffer: AudioBuffer, buffer: AudioBuffer,
volume: number, volume: number,
stereoPan: number, stereoPan: number,
delayS?: number,
): () => Promise<void> { ): () => Promise<void> {
const gain = ctx.createGain(); if (delayS === 0) {
gain.gain.setValueAtTime(volume, 0); throw Error("Looping sounds must have a delay");
const pan = ctx.createStereoPanner(); }
pan.pan.setValueAtTime(stereoPan, 0);
const src = ctx.createBufferSource(); // Our audio loop
src.buffer = buffer; let lastSoundPromise: Promise<void>;
src.connect(gain).connect(pan).connect(ctx.destination); let nextSoundPromise: Promise<void>;
controls.setPlaybackStarted(); let ac: AbortController | undefined;
src.loop = true; void (async () => {
src.start(); ac = new AbortController();
// Play a sound immediately
lastSoundPromise = Promise.resolve();
do {
// Queue up the next sound.
nextSoundPromise = playSound(ctx, buffer, volume, stereoPan, delayS, ac);
// Await the previous sound.
await lastSoundPromise;
// Swap the promises over, and loop round to play the next sound.
lastSoundPromise = nextSoundPromise;
} while (!ac.signal.aborted);
})();
return async () => { return async () => {
const p = new Promise<void>((r) => ac?.abort();
src.addEventListener("ended", () => r()), // Wait for sounds to finish.
); await lastSoundPromise;
src.stop(); await nextSoundPromise;
return p;
}; };
} }
@@ -91,9 +108,13 @@ interface Props<S extends string> {
muted?: boolean; muted?: boolean;
} }
interface UseAudioContext<S> { interface UseAudioContext<S extends string> {
playSound(soundName: S): Promise<void>; playSound(soundName: S): Promise<void>;
playSoundLooping(soundName: S): () => Promise<void>; playSoundLooping(soundName: S, delayS?: number): () => Promise<void>;
/**
* Map of sound name to duration in seconds.
*/
soundDuration: Record<string, number>;
} }
/** /**
@@ -181,7 +202,7 @@ export function useAudioContext<S extends string>(
earpiecePan, earpiecePan,
); );
}, },
playSoundLooping: (name): (() => Promise<void>) => { playSoundLooping: (name, delayS: number): (() => Promise<void>) => {
if (!audioBuffers[name]) { if (!audioBuffers[name]) {
throw Error(`Tried to play a sound that wasn't buffered (${name})`); throw Error(`Tried to play a sound that wasn't buffered (${name})`);
} }
@@ -190,7 +211,14 @@ export function useAudioContext<S extends string>(
audioBuffers[name], audioBuffers[name],
soundEffectVolume * earpieceVolume, soundEffectVolume * earpieceVolume,
earpiecePan, earpiecePan,
delayS,
); );
}, },
soundDuration: Object.fromEntries(
Object.entries(audioBuffers).map(([k, v]) => [
k,
(v as AudioBuffer).duration,
]),
),
}; };
} }