diff --git a/src/room/CallEventAudioRenderer.tsx b/src/room/CallEventAudioRenderer.tsx index a12b669c..21e667e3 100644 --- a/src/room/CallEventAudioRenderer.tsx +++ b/src/room/CallEventAudioRenderer.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { ReactNode, useEffect } from "react"; -import { debounce, filter, interval, skip, throttle } from "rxjs"; +import { debounce, filter, interval, throttle } from "rxjs"; import { CallViewModel } from "../state/CallViewModel"; import joinCallSoundMp3 from "../sound/join_call.mp3"; import joinCallSoundOgg from "../sound/join_call.ogg"; diff --git a/src/room/ReactionAudioRenderer.tsx b/src/room/ReactionAudioRenderer.tsx index cc0b4a57..7f1ca6f7 100644 --- a/src/room/ReactionAudioRenderer.tsx +++ b/src/room/ReactionAudioRenderer.tsx @@ -5,70 +5,56 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ReactNode, useEffect, useRef } from "react"; +import { ReactNode, useDeferredValue, useEffect } from "react"; import { useReactions } from "../useReactions"; -import { - playReactionsSound, - soundEffectVolumeSetting as effectSoundVolumeSetting, - useSetting, -} from "../settings/settings"; -import { GenericReaction, ReactionSet } from "../reactions"; +import { playReactionsSound, useSetting } from "../settings/settings"; +import { ReactionSet } from "../reactions"; +import { prefetchSounds, useAudioContext } from "../useAudioContext"; + +const SoundMap = Object.fromEntries( + ReactionSet.filter((v) => v.sound !== undefined).map((v) => [ + v.name, + v.sound!, + ]), +); + +const Sounds = prefetchSounds(SoundMap); export function ReactionsAudioRenderer(): ReactNode { const { reactions } = useReactions(); const [shouldPlay] = useSetting(playReactionsSound); - const [effectSoundVolume] = useSetting(effectSoundVolumeSetting); - const audioElements = useRef>({}); + const audioEngineCtx = useAudioContext({ + sounds: Sounds, + latencyHint: "interactive", + }); + const oldReactions = useDeferredValue(reactions); useEffect(() => { - if (!audioElements.current) { + if (!audioEngineCtx) { return; } if (!shouldPlay) { return; } + const oldReactionSet = new Set( + Object.values(oldReactions).map((r) => r.name), + ); for (const reactionName of new Set( Object.values(reactions).map((r) => r.name), )) { - const audioElement = - audioElements.current[reactionName] ?? audioElements.current.generic; - if (audioElement?.paused) { - audioElement.volume = effectSoundVolume; - void audioElement.play(); + if (oldReactionSet.has(reactionName)) { + // Don't replay old reactions + return; + } + if (SoundMap[reactionName]) { + audioEngineCtx.playSound(reactionName); + } else { + // Fallback sounds. + audioEngineCtx.playSound("generic"); } } - }, [audioElements, shouldPlay, reactions, effectSoundVolume]); - - // Do not render any audio elements if playback is disabled. Will save - // audio file fetches. - if (!shouldPlay) { - return null; - } - - // NOTE: We load all audio elements ahead of time to allow the cache - // to be populated, rather than risk a cache miss and have the audio - // be delayed. - return ( - <> - {[GenericReaction, ...ReactionSet].map( - (r) => - r.sound && ( - - ), - )} - - ); + }, [shouldPlay, oldReactions, reactions]); + return <>; } diff --git a/src/useAudioContext.tsx b/src/useAudioContext.tsx index d9a26e0e..c07b6736 100644 --- a/src/useAudioContext.tsx +++ b/src/useAudioContext.tsx @@ -6,7 +6,7 @@ import { } from "./settings/settings"; import { useMediaDevices } from "./livekit/MediaDevicesContext"; -type SoundDefinition = { mp3: string; ogg: string }; +type SoundDefinition = { mp3?: string; ogg: string }; async function fetchBuffer(filename: string) { // Load an audio file @@ -64,7 +64,10 @@ export async function prefetchSounds( await Promise.all( Object.entries(sounds).map(async ([name, file]) => { const { mp3, ogg } = file as SoundDefinition; - buffers[name] = await fetchBuffer(PreferredFormat === "ogg" ? ogg : mp3); + // Use preferred format, fallback to ogg if no mp3 is provided. + buffers[name] = await fetchBuffer( + PreferredFormat === "ogg" ? ogg : (mp3 ?? ogg), + ); }), ); return buffers as Record;