Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Advanced audio processing settings (#8759)
Browse files Browse the repository at this point in the history
Co-authored-by: Šimon Brandner <[email protected]>
Fixes element-hq/element-web#6278
Fixes undefined
  • Loading branch information
MrAnno authored Nov 9, 2022
1 parent da77953 commit afdf289
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 22 deletions.
37 changes: 37 additions & 0 deletions src/MediaDeviceHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ export default class MediaDeviceHandler extends EventEmitter {

await MatrixClientPeg.get().getMediaHandler().setAudioInput(audioDeviceId);
await MatrixClientPeg.get().getMediaHandler().setVideoInput(videoDeviceId);

await MediaDeviceHandler.updateAudioSettings();
}

private static async updateAudioSettings(): Promise<void> {
await MatrixClientPeg.get().getMediaHandler().setAudioSettings({
autoGainControl: MediaDeviceHandler.getAudioAutoGainControl(),
echoCancellation: MediaDeviceHandler.getAudioEchoCancellation(),
noiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression(),
});
}

public setAudioOutput(deviceId: string): void {
Expand Down Expand Up @@ -123,6 +133,21 @@ export default class MediaDeviceHandler extends EventEmitter {
}
}

public static async setAudioAutoGainControl(value: boolean): Promise<void> {
await SettingsStore.setValue("webrtc_audio_autoGainControl", null, SettingLevel.DEVICE, value);
await MediaDeviceHandler.updateAudioSettings();
}

public static async setAudioEchoCancellation(value: boolean): Promise<void> {
await SettingsStore.setValue("webrtc_audio_echoCancellation", null, SettingLevel.DEVICE, value);
await MediaDeviceHandler.updateAudioSettings();
}

public static async setAudioNoiseSuppression(value: boolean): Promise<void> {
await SettingsStore.setValue("webrtc_audio_noiseSuppression", null, SettingLevel.DEVICE, value);
await MediaDeviceHandler.updateAudioSettings();
}

public static getAudioOutput(): string {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
}
Expand All @@ -135,6 +160,18 @@ export default class MediaDeviceHandler extends EventEmitter {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
}

public static getAudioAutoGainControl(): boolean {
return SettingsStore.getValue("webrtc_audio_autoGainControl");
}

public static getAudioEchoCancellation(): boolean {
return SettingsStore.getValue("webrtc_audio_echoCancellation");
}

public static getAudioNoiseSuppression(): boolean {
return SettingsStore.getValue("webrtc_audio_noiseSuppression");
}

/**
* Returns the current set deviceId for a device kind
* @param {MediaDeviceKindEnum} kind of the device that will be returned
Expand Down
75 changes: 63 additions & 12 deletions src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import Modal from "../../../../../Modal";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsFlag from '../../../elements/SettingsFlag';
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import ErrorDialog from '../../../dialogs/ErrorDialog';

const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => {
Expand All @@ -41,8 +42,14 @@ const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => {
}
};

interface IState extends Record<MediaDeviceKindEnum, string> {
interface IState {
mediaDevices: IMediaDevices;
[MediaDeviceKindEnum.AudioOutput]: string;
[MediaDeviceKindEnum.AudioInput]: string;
[MediaDeviceKindEnum.VideoInput]: string;
audioAutoGainControl: boolean;
audioEchoCancellation: boolean;
audioNoiseSuppression: boolean;
}

export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
Expand All @@ -54,6 +61,9 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
[MediaDeviceKindEnum.AudioOutput]: null,
[MediaDeviceKindEnum.AudioInput]: null,
[MediaDeviceKindEnum.VideoInput]: null,
audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl(),
audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation(),
audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression(),
};
}

Expand Down Expand Up @@ -183,22 +193,63 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
return (
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Voice & Video") }</div>
{ requestButton }
<div className="mx_SettingsTab_section">
{ requestButton }
<span className="mx_SettingsTab_subheading">{ _t("Voice settings") }</span>
{ speakerDropdown }
{ microphoneDropdown }
<LabelledToggleSwitch
value={this.state.audioAutoGainControl}
onChange={async (v) => {
await MediaDeviceHandler.setAudioAutoGainControl(v);
this.setState({ audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl() });
}}
label={_t("Automatically adjust the microphone volume")}
data-testid='voice-auto-gain'
/>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Video settings") }</span>
{ webcamDropdown }
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
<SettingsFlag
name='webRtcAllowPeerToPeer'
level={SettingLevel.DEVICE}
onChange={this.changeWebRtcMethod}
/>
<SettingsFlag
name='fallbackICEServerAllowed'
level={SettingLevel.DEVICE}
onChange={this.changeFallbackICEServerAllowed}
/>
</div>

<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Voice processing") }</span>
<div className="mx_SettingsTab_section">
<LabelledToggleSwitch
value={this.state.audioNoiseSuppression}
onChange={async (v) => {
await MediaDeviceHandler.setAudioNoiseSuppression(v);
this.setState({ audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression() });
}}
label={_t("Noise suppression")}
data-testid='voice-noise-suppression'
/>
<LabelledToggleSwitch
value={this.state.audioEchoCancellation}
onChange={async (v) => {
await MediaDeviceHandler.setAudioEchoCancellation(v);
this.setState({ audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation() });
}}
label={_t("Echo cancellation")}
data-testid='voice-echo-cancellation'
/>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Connection") }</span>
<SettingsFlag
name='webRtcAllowPeerToPeer'
level={SettingLevel.DEVICE}
onChange={this.changeWebRtcMethod}
/>
<SettingsFlag
name='fallbackICEServerAllowed'
level={SettingLevel.DEVICE}
onChange={this.changeFallbackICEServerAllowed}
/>
</div>
</div>
</div>
);
Expand Down
14 changes: 12 additions & 2 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,11 @@
"Match system theme": "Match system theme",
"Use a system font": "Use a system font",
"System font name": "System font name",
"Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)",
"Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls",
"When enabled, the other party might be able to see your IP address": "When enabled, the other party might be able to see your IP address",
"Automatic gain control": "Automatic gain control",
"Echo cancellation": "Echo cancellation",
"Noise suppression": "Noise suppression",
"Send analytics data": "Send analytics data",
"Record the client name, version, and url to recognise sessions more easily in session manager": "Record the client name, version, and url to recognise sessions more easily in session manager",
"Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session",
Expand All @@ -992,7 +996,8 @@
"Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list",
"Show hidden events in timeline": "Show hidden events in timeline",
"Low bandwidth mode (requires compatible homeserver)": "Low bandwidth mode (requires compatible homeserver)",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)",
"Allow fallback call assist server (turn.matrix.org)": "Allow fallback call assist server (turn.matrix.org)",
"Only applies if your homeserver does not offer one. Your IP address would be shared during a call.": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.",
"Show previews/thumbnails for images": "Show previews/thumbnails for images",
"Enable message search in encrypted rooms": "Enable message search in encrypted rooms",
"How fast should messages be downloaded.": "How fast should messages be downloaded.",
Expand Down Expand Up @@ -1619,6 +1624,11 @@
"No Microphones detected": "No Microphones detected",
"Camera": "Camera",
"No Webcams detected": "No Webcams detected",
"Voice settings": "Voice settings",
"Automatically adjust the microphone volume": "Automatically adjust the microphone volume",
"Video settings": "Video settings",
"Voice processing": "Voice processing",
"Connection": "Connection",
"This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.",
"Upgrade this space to the recommended room version": "Upgrade this space to the recommended room version",
Expand Down
31 changes: 23 additions & 8 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ export type SettingValueType = boolean |
string |
number[] |
string[] |
Record<string, unknown>;
Record<string, unknown> |
null;

export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
isFeature?: false | undefined;
Expand Down Expand Up @@ -712,10 +713,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
},
"webRtcAllowPeerToPeer": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td(
"Allow Peer-to-Peer for 1:1 calls " +
"(if you enable this, the other party might be able to see your IP address)",
),
displayName: _td("Allow Peer-to-Peer for 1:1 calls"),
description: _td("When enabled, the other party might be able to see your IP address"),
default: true,
invertedSettingName: 'webRtcForceTURN',
},
Expand All @@ -731,6 +730,21 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: "default",
},
"webrtc_audio_autoGainControl": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("Automatic gain control"),
default: true,
},
"webrtc_audio_echoCancellation": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("Echo cancellation"),
default: true,
},
"webrtc_audio_noiseSuppression": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("Noise suppression"),
default: true,
},
"language": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: "en",
Expand Down Expand Up @@ -902,9 +916,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
},
"fallbackICEServerAllowed": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td(
"Allow fallback call assist server turn.matrix.org when your homeserver " +
"does not offer one (your IP address would be shared during a call)",
displayName: _td("Allow fallback call assist server (turn.matrix.org)"),
description: _td(
"Only applies if your homeserver does not offer one. " +
"Your IP address would be shared during a call.",
),
// This is a tri-state value, where `null` means "prompt the user".
default: null,
Expand Down
65 changes: 65 additions & 0 deletions test/MediaDeviceHandler-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { mocked } from 'jest-mock';

import { SettingLevel } from "../src/settings/SettingLevel";
import { MatrixClientPeg } from '../src/MatrixClientPeg';
import { stubClient } from "./test-utils";
import MediaDeviceHandler from "../src/MediaDeviceHandler";
import SettingsStore from '../src/settings/SettingsStore';

jest.mock("../src/settings/SettingsStore");

const SettingsStoreMock = mocked(SettingsStore);

describe("MediaDeviceHandler", () => {
beforeEach(() => {
stubClient();
});

afterEach(() => {
jest.clearAllMocks();
});

it("sets audio settings", async () => {
const expectedAudioSettings = new Map<string, boolean>([
["webrtc_audio_autoGainControl", false],
["webrtc_audio_echoCancellation", true],
["webrtc_audio_noiseSuppression", false],
]);

SettingsStoreMock.getValue.mockImplementation((settingName): any => {
return expectedAudioSettings.get(settingName);
});

await MediaDeviceHandler.setAudioAutoGainControl(false);
await MediaDeviceHandler.setAudioEchoCancellation(true);
await MediaDeviceHandler.setAudioNoiseSuppression(false);

expectedAudioSettings.forEach((value, key) => {
expect(SettingsStoreMock.setValue).toHaveBeenCalledWith(
key, null, SettingLevel.DEVICE, value,
);
});

expect(MatrixClientPeg.get().getMediaHandler().setAudioSettings).toHaveBeenCalledWith({
autoGainControl: false,
echoCancellation: true,
noiseSuppression: false,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import { mocked } from 'jest-mock';
import { render } from '@testing-library/react';

import VoiceUserSettingsTab from '../../../../../../src/components/views/settings/tabs/user/VoiceUserSettingsTab';
import MediaDeviceHandler from "../../../../../../src/MediaDeviceHandler";

jest.mock("../../../../../../src/MediaDeviceHandler");
const MediaDeviceHandlerMock = mocked(MediaDeviceHandler);

describe('<VoiceUserSettingsTab />', () => {
const getComponent = (): React.ReactElement => (<VoiceUserSettingsTab />);

beforeEach(() => {
jest.clearAllMocks();
});

it('renders audio processing settings', () => {
const { getByTestId } = render(getComponent());
expect(getByTestId('voice-auto-gain')).toBeTruthy();
expect(getByTestId('voice-noise-suppression')).toBeTruthy();
expect(getByTestId('voice-echo-cancellation')).toBeTruthy();
});

it('sets and displays audio processing settings', () => {
MediaDeviceHandlerMock.getAudioAutoGainControl.mockReturnValue(false);
MediaDeviceHandlerMock.getAudioEchoCancellation.mockReturnValue(true);
MediaDeviceHandlerMock.getAudioNoiseSuppression.mockReturnValue(false);

const { getByRole } = render(getComponent());

getByRole("switch", { name: "Automatically adjust the microphone volume" }).click();
getByRole("switch", { name: "Noise suppression" }).click();
getByRole("switch", { name: "Echo cancellation" }).click();

expect(MediaDeviceHandler.setAudioAutoGainControl).toHaveBeenCalledWith(true);
expect(MediaDeviceHandler.setAudioEchoCancellation).toHaveBeenCalledWith(false);
expect(MediaDeviceHandler.setAudioNoiseSuppression).toHaveBeenCalledWith(true);
});
});
1 change: 1 addition & 0 deletions test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export function createTestClient(): MatrixClient {
getMediaHandler: jest.fn().mockReturnValue({
setVideoInput: jest.fn(),
setAudioInput: jest.fn(),
setAudioSettings: jest.fn(),
} as unknown as MediaHandler),
uploadContent: jest.fn(),
getEventMapper: () => (opts) => new MatrixEvent(opts),
Expand Down

0 comments on commit afdf289

Please sign in to comment.