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$);
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.
useEffect((): void | (() => void) => {
if (callPickupState === "timeout") {
@@ -384,13 +394,16 @@ export const InCallView: FC<InCallViewProps> = ({
// When waiting for pickup, loop a waiting sound
useEffect((): void | (() => void) => {
if (callPickupState !== "ringing" || !pickupPhaseAudio) return;
const endSound = pickupPhaseAudio.playSoundLooping("waiting");
const endSound = pickupPhaseAudio.playSoundLooping(
"waiting",
ringDelay / 2,
);
return () => {
void endSound().catch((e) => {
logger.error("Failed to stop ringing sound", e);
});
};
}, [callPickupState, pickupPhaseAudio]);
}, [callPickupState, pickupPhaseAudio, ringDelay]);
// Waiting UI overlay
const waitingOverlay: JSX.Element | null = useMemo(() => {

View File

@@ -25,10 +25,12 @@
inset: -12px;
border-radius: 9999px;
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);
}
@@ -36,13 +38,24 @@
0% {
transform: scale(0.95);
opacity: 0.7;
transform: scale(0);
opacity: 1;
}
70% {
35% {
transform: scale(1.15);
opacity: 0.15;
}
100% {
50% {
transform: scale(1.2);
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,
volume: number,
stereoPan: number,
delayS = 0,
abort?: AbortController,
): Promise<void> {
const gain = ctx.createGain();
gain.gain.setValueAtTime(volume, 0);
@@ -39,10 +41,13 @@ async function playSound(
pan.pan.setValueAtTime(stereoPan, 0);
const src = ctx.createBufferSource();
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()));
src.connect(gain).connect(pan).connect(ctx.destination);
controls.setPlaybackStarted();
src.start();
src.start(ctx.currentTime + delayS);
return p;
}
@@ -60,23 +65,35 @@ function playSoundLooping(
buffer: AudioBuffer,
volume: number,
stereoPan: number,
delayS?: number,
): () => Promise<void> {
const gain = ctx.createGain();
gain.gain.setValueAtTime(volume, 0);
const pan = ctx.createStereoPanner();
pan.pan.setValueAtTime(stereoPan, 0);
const src = ctx.createBufferSource();
src.buffer = buffer;
src.connect(gain).connect(pan).connect(ctx.destination);
controls.setPlaybackStarted();
src.loop = true;
src.start();
if (delayS === 0) {
throw Error("Looping sounds must have a delay");
}
// Our audio loop
let lastSoundPromise: Promise<void>;
let nextSoundPromise: Promise<void>;
let ac: AbortController | undefined;
void (async () => {
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 () => {
const p = new Promise<void>((r) =>
src.addEventListener("ended", () => r()),
);
src.stop();
return p;
ac?.abort();
// Wait for sounds to finish.
await lastSoundPromise;
await nextSoundPromise;
};
}
@@ -91,9 +108,13 @@ interface Props<S extends string> {
muted?: boolean;
}
interface UseAudioContext<S> {
interface UseAudioContext<S extends string> {
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,
);
},
playSoundLooping: (name): (() => Promise<void>) => {
playSoundLooping: (name, delayS: number): (() => Promise<void>) => {
if (!audioBuffers[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],
soundEffectVolume * earpieceVolume,
earpiecePan,
delayS,
);
},
soundDuration: Object.fromEntries(
Object.entries(audioBuffers).map(([k, v]) => [
k,
(v as AudioBuffer).duration,
]),
),
};
}