Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sound effect volume slider #2732

Merged
merged 6 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions public/locales/en-GB/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@
"room_auth_view_eula_caption": "By clicking \"Continue\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
"screenshare_button_label": "Share screen",
"settings": {
"audio_tab": {
"effect_volume_description": "Adjust the volume at which reactions and hand raised effects play",
"effect_volume_label": "Sound effect volume"
},
"developer_settings_label": "Developer Settings",
"developer_settings_label_description": "Expose developer settings in the settings window.",
"developer_tab_title": "Developer",
Expand Down
15 changes: 13 additions & 2 deletions src/room/InCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ import handSoundOgg from "../sound/raise_hand.ogg?url";
import handSoundMp3 from "../sound/raise_hand.mp3?url";
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
import { useSwitchCamera } from "./useSwitchCamera";
import { showReactions, useSetting } from "../settings/settings";
import {
soundEffectVolumeSetting,
showReactions,
useSetting,
} from "../settings/settings";

const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});

Expand Down Expand Up @@ -182,6 +186,7 @@ export const InCallView: FC<InCallViewProps> = ({
onShareClick,
}) => {
const [shouldShowReactions] = useSetting(showReactions);
const [soundEffectVolume] = useSetting(soundEffectVolumeSetting);
const { supportsReactions, raisedHands, reactions } = useReactions();
const raisedHandCount = useMemo(
() => Object.keys(raisedHands).length,
Expand Down Expand Up @@ -344,11 +349,17 @@ export const InCallView: FC<InCallViewProps> = ({
return;
}
if (previousRaisedHandCount < raisedHandCount) {
handRaisePlayer.current.volume = soundEffectVolume;
handRaisePlayer.current.play().catch((ex) => {
logger.warn("Failed to play raise hand sound", ex);
});
}
}, [raisedHandCount, handRaisePlayer, previousRaisedHandCount]);
}, [
raisedHandCount,
handRaisePlayer,
previousRaisedHandCount,
soundEffectVolume,
]);

useEffect(() => {
widget?.api.transport
Expand Down
28 changes: 27 additions & 1 deletion src/room/ReactionAudioRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
} from "../utils/testReactions";
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
import { GenericReaction, ReactionSet } from "../reactions";
import { playReactionsSound } from "../settings/settings";
import {
playReactionsSound,
soundEffectVolumeSetting,
} from "../settings/settings";

const memberUserIdAlice = "@alice:example.org";
const memberUserIdBob = "@bob:example.org";
Expand Down Expand Up @@ -49,6 +52,7 @@ function TestComponent({
const originalPlayFn = window.HTMLMediaElement.prototype.play;
afterAll(() => {
playReactionsSound.setValue(playReactionsSound.defaultValue);
soundEffectVolumeSetting.setValue(soundEffectVolumeSetting.defaultValue);
window.HTMLMediaElement.prototype.play = originalPlayFn;
});

Expand Down Expand Up @@ -125,6 +129,28 @@ test("will play the generic audio sound when there is soundless reaction", () =>
expect(audioIsPlaying[0]).toContain(GenericReaction.sound?.ogg);
});

test("will play an audio sound with the correct volume", () => {
playReactionsSound.setValue(true);
soundEffectVolumeSetting.setValue(0.5);
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByTestId } = render(<TestComponent rtcSession={rtcSession} />);

// Find the first reaction with a sound effect
const chosenReaction = ReactionSet.find((r) => !!r.sound);
if (!chosenReaction) {
throw Error(
"No reactions have sounds configured, this test cannot succeed",
);
}
act(() => {
room.testSendReaction(memberEventAlice, chosenReaction, membership);
});
expect((getByTestId(chosenReaction.name) as HTMLAudioElement).volume).toEqual(
0.5,
);
});

test("will play multiple audio sounds when there are multiple different reactions", () => {
const audioIsPlaying: string[] = [];
window.HTMLMediaElement.prototype.play = async function (): Promise<void> {
Expand Down
10 changes: 8 additions & 2 deletions src/room/ReactionAudioRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ Please see LICENSE in the repository root for full details.
import { ReactNode, useEffect, useRef } from "react";

import { useReactions } from "../useReactions";
import { playReactionsSound, useSetting } from "../settings/settings";
import {
playReactionsSound,
soundEffectVolumeSetting as effectSoundVolumeSetting,
useSetting,
} from "../settings/settings";
import { GenericReaction, ReactionSet } from "../reactions";

export function ReactionsAudioRenderer(): ReactNode {
const { reactions } = useReactions();
const [shouldPlay] = useSetting(playReactionsSound);
const [effectSoundVolume] = useSetting(effectSoundVolumeSetting);
const audioElements = useRef<Record<string, HTMLAudioElement | null>>({});

useEffect(() => {
Expand All @@ -30,10 +35,11 @@ export function ReactionsAudioRenderer(): ReactNode {
const audioElement =
audioElements.current[reactionName] ?? audioElements.current.generic;
if (audioElement?.paused) {
audioElement.volume = effectSoundVolume;
void audioElement.play();
}
}
}, [audioElements, shouldPlay, reactions]);
}, [audioElements, shouldPlay, reactions, effectSoundVolume]);

// Do not render any audio elements if playback is disabled. Will save
// audio file fetches.
Expand Down
17 changes: 17 additions & 0 deletions src/settings/SettingsModal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,20 @@ Please see LICENSE in the repository root for full details.
.fieldRowText {
margin-bottom: 0;
}

.volumeSlider {
margin-top: var(--cpd-space-2x);
}

.volumeSlider > label {
margin-bottom: var(--cpd-space-1x);
display: block;
}

.volumeSlider > span {
max-width: 20em;
}

.volumeSlider > p {
color: var(--cpd-color-text-secondary);
}
19 changes: 18 additions & 1 deletion src/settings/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
import { ChangeEvent, FC, ReactNode, useCallback } from "react";
import { Trans, useTranslation } from "react-i18next";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { Dropdown, Text } from "@vector-im/compound-web";
import { Dropdown, Separator, Text } from "@vector-im/compound-web";

import { Modal } from "../Modal";
import styles from "./SettingsModal.module.css";
Expand All @@ -28,9 +28,11 @@ import {
developerSettingsTab as developerSettingsTabSetting,
duplicateTiles as duplicateTilesSetting,
useOptInAnalytics,
soundEffectVolumeSetting,
} from "./settings";
import { isFirefox } from "../Platform";
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
import { Slider } from "../Slider";

type SettingsTab =
| "audio"
Expand Down Expand Up @@ -116,6 +118,8 @@ export const SettingsModal: FC<Props> = ({
const devices = useMediaDevices();
useMediaDeviceNames(devices, open);

const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting);

const audioTab: Tab<SettingsTab> = {
key: "audio",
name: t("common.audio"),
Expand All @@ -127,6 +131,19 @@ export const SettingsModal: FC<Props> = ({
devices.audioOutput,
t("settings.speaker_device_selection_label"),
)}
<Separator />
<div className={styles.volumeSlider}>
<label>{t("settings.audio_tab.effect_volume_label")}</label>
<p>{t("settings.audio_tab.effect_volume_description")}</p>
<Slider
label={t("video_tile.volume")}
value={soundVolume}
onValueChange={setSoundVolume}
min={0}
max={1}
step={0.01}
/>
</div>
</>
),
};
Expand Down
5 changes: 5 additions & 0 deletions src/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,9 @@ export const playReactionsSound = new Setting<boolean>(
true,
);

export const soundEffectVolumeSetting = new Setting<number>(
"sound-effect-volume",
1,
);

export const alwaysShowSelf = new Setting<boolean>("always-show-self", true);
4 changes: 2 additions & 2 deletions src/useReactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export const ReactionsProvider = ({

// This effect handles any *live* reaction/redactions in the room.
useEffect(() => {
const reactionTimeouts = new Set<NodeJS.Timeout>();
const reactionTimeouts = new Set<number>();
const handleReactionEvent = (event: MatrixEvent): void => {
if (event.isSending()) {
// Skip any events that are still sending.
Expand Down Expand Up @@ -245,7 +245,7 @@ export const ReactionsProvider = ({
// We've still got a reaction from this user, ignore it to prevent spamming
return reactions;
}
const timeout = setTimeout(() => {
const timeout = window.setTimeout(() => {
// Clear the reaction after some time.
setReactions(({ [sender]: _unused, ...remaining }) => remaining);
reactionTimeouts.delete(timeout);
Expand Down