Adding an UI for the new client audio processing unit
This commit is contained in:
parent
1878c92a57
commit
1dfa10b09b
29 changed files with 1631 additions and 354 deletions
|
@ -1,4 +1,9 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **29.03.21**
|
||||||
|
- Accquiering the default input recorder when opening the settings
|
||||||
|
- Adding new modal Input Processing Properties for the native client
|
||||||
|
- Fixed that you can't finish off the name editing by pressing enter
|
||||||
|
|
||||||
* **25.03.21**
|
* **25.03.21**
|
||||||
- Allowing to directly select the speaker output device
|
- Allowing to directly select the speaker output device
|
||||||
- Saving the speaker output device
|
- Saving the speaker output device
|
||||||
|
|
|
@ -25,6 +25,7 @@ import "./static/modal-serverinfobandwidth.scss"
|
||||||
import "./static/modal-serverinfo.scss"
|
import "./static/modal-serverinfo.scss"
|
||||||
import "./static/modal-settings.scss"
|
import "./static/modal-settings.scss"
|
||||||
import "./static/overlay-image-preview.scss"
|
import "./static/overlay-image-preview.scss"
|
||||||
|
import "./static/color-variables.scss"
|
||||||
|
|
||||||
import "./static/ts/tab.scss"
|
import "./static/ts/tab.scss"
|
||||||
import "./static/ts/country.scss"
|
import "./static/ts/country.scss"
|
45
shared/css/static/color-variables.scss
Normal file
45
shared/css/static/color-variables.scss
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
html:root {
|
||||||
|
/* Permission editor modal */
|
||||||
|
--modal-permissions-header-text: #e1e1e1;
|
||||||
|
--modal-permissions-header-background: #19191b;
|
||||||
|
--modal-permissions-header-hover: #4e4e4e;
|
||||||
|
--modal-permissions-header-selected: #0073d4;
|
||||||
|
|
||||||
|
--modal-permission-right: #303036;
|
||||||
|
--modal-permission-left: #222226;
|
||||||
|
|
||||||
|
--modal-permissions-entry-hover: #28282c;
|
||||||
|
--modal-permissions-entry-selected: #111111;
|
||||||
|
--modal-permissions-current-group: #101012;
|
||||||
|
|
||||||
|
--modal-permissions-buttons-background: #0f0f0f;
|
||||||
|
--modal-permissions-buttons-hover: #262626;
|
||||||
|
--modal-permissions-buttons-disabled: #1b1b1b;
|
||||||
|
|
||||||
|
--modal-permissions-seperator: #1e1e1e; /* the seperator for the "enter a unique id" and "client info" part */
|
||||||
|
--modal-permissions-container-seperator: #222224; /* the seperator between left and right */
|
||||||
|
|
||||||
|
--modal-permissions-icon-select: #121213;
|
||||||
|
--modal-permissions-icon-select-border: #0d0d0d;
|
||||||
|
--modal-permissions-icon-select-hover: #17171a;
|
||||||
|
--modal-permissions-icon-select-hover-border: #333333;
|
||||||
|
|
||||||
|
--modal-permission-no-permnissions: #18171c;
|
||||||
|
--modal-permissions-table-border: #1e2025;
|
||||||
|
|
||||||
|
--modal-permissions-table-header: #303036;
|
||||||
|
--modal-permissions-table-row-odd: #303036;
|
||||||
|
--modal-permissions-table-row-even: #25252a;
|
||||||
|
--modal-permissions-table-row-hover: #343a47;
|
||||||
|
|
||||||
|
--modal-permissions-table-header-text: #e1e1e1;
|
||||||
|
--modal-permissions-table-row-text: #535455;
|
||||||
|
--modal-permissions-table-entry-active-text: #e1e1e1;
|
||||||
|
--modal-permissions-table-entry-group-text: #e1e1e1;
|
||||||
|
|
||||||
|
--modal-permissions-table-input: #e1e1e1;
|
||||||
|
--modal-permissions-table-input-focus: #3f7dbf;
|
||||||
|
|
||||||
|
/* The host banner */
|
||||||
|
--hostbanner-background: #2e2e2e;
|
||||||
|
}
|
|
@ -5,21 +5,17 @@ import {ServerSettings, Settings, settings, StaticSettings} from "./settings";
|
||||||
import {Sound, SoundManager} from "./audio/Sounds";
|
import {Sound, SoundManager} from "./audio/Sounds";
|
||||||
import {ConnectionProfile} from "./profiles/ConnectionProfile";
|
import {ConnectionProfile} from "./profiles/ConnectionProfile";
|
||||||
import {LogCategory, logError, logInfo, logTrace, logWarn} from "./log";
|
import {LogCategory, logError, logInfo, logTrace, logWarn} from "./log";
|
||||||
import {createErrorModal, createInfoModal, createInputModal, Modal} from "./ui/elements/Modal";
|
import {createErrorModal, createInputModal, Modal} from "./ui/elements/Modal";
|
||||||
import {hashPassword} from "./utils/helpers";
|
import {hashPassword} from "./utils/helpers";
|
||||||
import {HandshakeHandler} from "./connection/HandshakeHandler";
|
import {HandshakeHandler} from "./connection/HandshakeHandler";
|
||||||
import * as htmltags from "./ui/htmltags";
|
import * as htmltags from "./ui/htmltags";
|
||||||
import {FilterMode, InputStartError, InputState} from "./voice/RecorderBase";
|
import {FilterMode, InputStartError, InputState} from "./voice/RecorderBase";
|
||||||
import {CommandResult} from "./connection/ServerConnectionDeclaration";
|
|
||||||
import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile";
|
import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile";
|
||||||
import {Regex} from "./ui/modal/ModalConnect";
|
import {Regex} from "./ui/modal/ModalConnect";
|
||||||
import {formatMessage} from "./ui/frames/chat";
|
import {formatMessage} from "./ui/frames/chat";
|
||||||
import {spawnAvatarUpload} from "./ui/modal/ModalAvatar";
|
|
||||||
import {EventHandler, Registry} from "./events";
|
import {EventHandler, Registry} from "./events";
|
||||||
import {FileManager} from "./file/FileManager";
|
import {FileManager} from "./file/FileManager";
|
||||||
import {FileTransferState, TransferProvider} from "./file/Transfer";
|
import {tr} from "./i18n/localize";
|
||||||
import {tr, traj} from "./i18n/localize";
|
|
||||||
import {md5} from "./crypto/md5";
|
|
||||||
import {guid} from "./crypto/uid";
|
import {guid} from "./crypto/uid";
|
||||||
import {PluginCmdRegistry} from "./connection/PluginCmdHandler";
|
import {PluginCmdRegistry} from "./connection/PluginCmdHandler";
|
||||||
import {VoiceConnectionStatus, WhisperSessionInitializeData} from "./connection/VoiceConnection";
|
import {VoiceConnectionStatus, WhisperSessionInitializeData} from "./connection/VoiceConnection";
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
import * as loader from "tc-loader";
|
|
||||||
import {Stage} from "tc-loader";
|
|
||||||
import {AbstractInput, LevelMeter} from "../voice/RecorderBase";
|
import {AbstractInput, LevelMeter} from "../voice/RecorderBase";
|
||||||
import {Registry} from "../events";
|
import {Registry} from "../events";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
|
||||||
|
|
||||||
export interface AudioRecorderBacked {
|
export interface AudioRecorderBacked {
|
||||||
createInput() : AbstractInput;
|
createInput() : AbstractInput;
|
||||||
createLevelMeter(device: InputDevice) : Promise<LevelMeter>;
|
createLevelMeter(device: InputDevice) : Promise<LevelMeter>;
|
||||||
|
|
||||||
getDeviceList() : DeviceList;
|
getDeviceList() : DeviceList;
|
||||||
|
|
||||||
isRnNoiseSupported() : boolean;
|
|
||||||
toggleRnNoise(target: boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceListEvents {
|
export interface DeviceListEvents {
|
||||||
|
@ -166,15 +160,3 @@ export function setRecorderBackend(instance: AudioRecorderBacked) {
|
||||||
|
|
||||||
recorderBackend = instance;
|
recorderBackend = instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|
||||||
name: "audio filter init",
|
|
||||||
priority: 10,
|
|
||||||
function: async () => {
|
|
||||||
const backend = getRecorderBackend();
|
|
||||||
if(backend.isRnNoiseSupported()) {
|
|
||||||
getRecorderBackend().toggleRnNoise(settings.getValue(Settings.KEY_RNNOISE_FILTER));
|
|
||||||
settings.globalChangeListener(Settings.KEY_RNNOISE_FILTER, value => getRecorderBackend().toggleRnNoise(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
|
@ -1,10 +1,6 @@
|
||||||
@import "../../../css/static/properties";
|
@import "../../../css/static/properties";
|
||||||
@import "../../../css/static/mixin";
|
@import "../../../css/static/mixin";
|
||||||
|
|
||||||
html:root {
|
|
||||||
--hostbanner-background: #2e2e2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -14,6 +10,7 @@ html:root {
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
.withBackground {
|
.withBackground {
|
||||||
background-color: var(--hostbanner-background);
|
background-color: var(--hostbanner-background);
|
||||||
|
|
|
@ -3,10 +3,11 @@ import {tra} from "tc-shared/i18n/localize";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {modal_settings, SettingProfileEvents} from "tc-shared/ui/modal/ModalSettings";
|
import {modal_settings, SettingProfileEvents} from "tc-shared/ui/modal/ModalSettings";
|
||||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||||
import {initialize_audio_microphone_controller, MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/Microphone";
|
import {initialize_audio_microphone_controller} from "tc-shared/ui/modal/settings/Microphone";
|
||||||
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
|
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
|
import {MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/MicrophoneDefinitions";
|
||||||
|
|
||||||
export interface EventModalNewcomer {
|
export interface EventModalNewcomer {
|
||||||
"show_step": {
|
"show_step": {
|
||||||
|
|
|
@ -23,9 +23,10 @@ import {KeyMapSettings} from "tc-shared/ui/modal/settings/Keymap";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import {NotificationSettings} from "tc-shared/ui/modal/settings/Notifications";
|
import {NotificationSettings} from "tc-shared/ui/modal/settings/Notifications";
|
||||||
import {initialize_audio_microphone_controller, MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/Microphone";
|
import {initialize_audio_microphone_controller} from "tc-shared/ui/modal/settings/Microphone";
|
||||||
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
|
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
|
||||||
import {getBackend} from "tc-shared/backend";
|
import {getBackend} from "tc-shared/backend";
|
||||||
|
import {MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/MicrophoneDefinitions";
|
||||||
|
|
||||||
type ProfileInfoEvent = {
|
type ProfileInfoEvent = {
|
||||||
id: string,
|
id: string,
|
||||||
|
|
140
shared/js/ui/modal/input-processor/Controller.ts
Normal file
140
shared/js/ui/modal/input-processor/Controller.ts
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import {
|
||||||
|
InputProcessor,
|
||||||
|
kInputProcessorConfigRNNoiseKeys,
|
||||||
|
kInputProcessorConfigWebRTCKeys
|
||||||
|
} from "tc-shared/voice/RecorderBase";
|
||||||
|
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||||
|
import {Registry} from "tc-events";
|
||||||
|
import {ModalInputProcessorEvents, ModalInputProcessorVariables} from "tc-shared/ui/modal/input-processor/Definitios";
|
||||||
|
import {createIpcUiVariableProvider, IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||||
|
import _ from "lodash";
|
||||||
|
import {LogCategory, logError, logTrace} from "tc-shared/log";
|
||||||
|
|
||||||
|
class Controller {
|
||||||
|
readonly events: Registry<ModalInputProcessorEvents>;
|
||||||
|
readonly variables: IpcUiVariableProvider<ModalInputProcessorVariables>;
|
||||||
|
|
||||||
|
private readonly processor: InputProcessor;
|
||||||
|
private statisticsTask: number;
|
||||||
|
|
||||||
|
private currentConfigRNNoise;
|
||||||
|
private currentConfigWebRTC;
|
||||||
|
private currentStatistics;
|
||||||
|
|
||||||
|
private filter: string;
|
||||||
|
|
||||||
|
constructor(processor: InputProcessor) {
|
||||||
|
this.processor = processor;
|
||||||
|
|
||||||
|
this.events = new Registry<ModalInputProcessorEvents>();
|
||||||
|
this.variables = createIpcUiVariableProvider();
|
||||||
|
|
||||||
|
this.filter = "";
|
||||||
|
|
||||||
|
for(const key of kInputProcessorConfigRNNoiseKeys) {
|
||||||
|
this.variables.setVariableProvider(key, () => this.currentConfigRNNoise[key]);
|
||||||
|
this.variables.setVariableEditor(key, newValue => {
|
||||||
|
if(this.currentConfigRNNoise[key] === newValue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const update = {};
|
||||||
|
update[key] = newValue;
|
||||||
|
this.processor.applyProcessorConfig("rnnoise", update as any);
|
||||||
|
} catch (error) {
|
||||||
|
logTrace(LogCategory.AUDIO, tr("Tried to apply rnnoise: %o"), this.currentConfigRNNoise);
|
||||||
|
this.sendApplyError(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentConfigRNNoise[key] = newValue;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const key of kInputProcessorConfigWebRTCKeys) {
|
||||||
|
this.variables.setVariableProvider(key, () => this.currentConfigWebRTC[key]);
|
||||||
|
this.variables.setVariableEditor(key, newValue => {
|
||||||
|
if(this.currentConfigWebRTC[key] === newValue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const update = {};
|
||||||
|
update[key] = newValue;
|
||||||
|
this.processor.applyProcessorConfig("webrtc-processing", update as any);
|
||||||
|
} catch (error) {
|
||||||
|
logTrace(LogCategory.AUDIO, tr("Tried to apply webrtc-processing: %o"), this.currentConfigWebRTC);
|
||||||
|
this.sendApplyError(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentConfigWebRTC[key] = newValue;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("propertyFilter", () => this.filter);
|
||||||
|
this.variables.setVariableEditor("propertyFilter", newValue => {
|
||||||
|
this.filter = newValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentConfigRNNoise = this.processor.getProcessorConfig("rnnoise");
|
||||||
|
this.currentConfigWebRTC = this.processor.getProcessorConfig("webrtc-processing");
|
||||||
|
|
||||||
|
this.events.on("query_statistics", () => this.sendStatistics(true));
|
||||||
|
|
||||||
|
this.statisticsTask = setInterval(() => {
|
||||||
|
this.sendStatistics(false);
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
clearInterval(this.statisticsTask);
|
||||||
|
this.events.destroy();
|
||||||
|
this.variables.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStatistics(force: boolean) {
|
||||||
|
const statistics = this.processor.getStatistics();
|
||||||
|
if(!force && _.isEqual(this.currentStatistics, statistics)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentStatistics = statistics;
|
||||||
|
this.events.fire_react("notify_statistics", { statistics: statistics });
|
||||||
|
}
|
||||||
|
|
||||||
|
sendApplyError(error: any) {
|
||||||
|
if(error instanceof Error) {
|
||||||
|
error = error.message;
|
||||||
|
} else if(typeof error !== "string") {
|
||||||
|
logError(LogCategory.AUDIO, tr("Failed to apply new processor config: %o"), error);
|
||||||
|
error = tr("lookup the console");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.events.fire("notify_apply_error", { message: error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawnInputProcessorModal(processor: InputProcessor) {
|
||||||
|
if(__build.target !== "client") {
|
||||||
|
throw tr("only the native client supports such modal");
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new Controller(processor);
|
||||||
|
|
||||||
|
const modal = spawnModal("modal-input-processor", [
|
||||||
|
controller.events.generateIpcDescription(),
|
||||||
|
controller.variables.generateConsumerDescription()
|
||||||
|
], {
|
||||||
|
popoutable: true,
|
||||||
|
popedOut: true
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.getEvents().on("destroy", () => {
|
||||||
|
controller.destroy();
|
||||||
|
});
|
||||||
|
modal.show().then(undefined);
|
||||||
|
}
|
18
shared/js/ui/modal/input-processor/Definitios.ts
Normal file
18
shared/js/ui/modal/input-processor/Definitios.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {
|
||||||
|
InputProcessorConfigRNNoise,
|
||||||
|
InputProcessorConfigWebRTC,
|
||||||
|
InputProcessorStatistics
|
||||||
|
} from "tc-shared/voice/RecorderBase";
|
||||||
|
|
||||||
|
export type ModalInputProcessorVariables = {
|
||||||
|
propertyFilter: string
|
||||||
|
} & InputProcessorConfigRNNoise & InputProcessorConfigWebRTC;
|
||||||
|
export interface ModalInputProcessorEvents {
|
||||||
|
query_statistics: {},
|
||||||
|
notify_statistics: {
|
||||||
|
statistics: InputProcessorStatistics,
|
||||||
|
},
|
||||||
|
notify_apply_error: {
|
||||||
|
message: string,
|
||||||
|
}
|
||||||
|
}
|
286
shared/js/ui/modal/input-processor/Renderer.scss
Normal file
286
shared/js/ui/modal/input-processor/Renderer.scss
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
@import "../../../../css/static/mixin.scss";
|
||||||
|
@import "../../../../css/static/properties.scss";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: .5em;
|
||||||
|
|
||||||
|
min-height: 25em;
|
||||||
|
min-width: 35em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #557edc;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.windowed {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerStatistics {
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
.statistics {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.statistic {
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.key {
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 4em;
|
||||||
|
|
||||||
|
@include text-dotdotdot();
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 2em;
|
||||||
|
|
||||||
|
@include text-dotdotdot();
|
||||||
|
|
||||||
|
.unset {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2n + 1) {
|
||||||
|
padding-right: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2n) {
|
||||||
|
padding-left: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerProperties {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
min-height: 5em;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
min-height: 4em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerFilter {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableBody {
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
min-height: 2em;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
background-color: var(--modal-permission-right);
|
||||||
|
|
||||||
|
padding-top: 2em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.6em;
|
||||||
|
|
||||||
|
color: var(--modal-permission-loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
a {
|
||||||
|
color: var(--modal-permission-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableEntry {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--modal-permissions-table-row-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
margin-right: .5em; /* scroll bar width */
|
||||||
|
|
||||||
|
.header {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
color: var(--modal-permissions-table-header-text);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
display: flex;
|
||||||
|
margin-left: .5em;
|
||||||
|
|
||||||
|
width: 1.1em;
|
||||||
|
height: 1.1em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$border-color: #070708;
|
||||||
|
.tableEntry {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
line-height: 1.8em;
|
||||||
|
height: 2em;
|
||||||
|
|
||||||
|
color: var(--modal-permissions-table-entry-active-text);
|
||||||
|
|
||||||
|
.column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
padding-left: 1em;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid $border-color;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-width: 5em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
align-self: flex-start;
|
||||||
|
|
||||||
|
@include text-dotdotdot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.value {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 15em;
|
||||||
|
|
||||||
|
padding: .25em;
|
||||||
|
|
||||||
|
.containerInput {
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
min-width: 5em;
|
||||||
|
|
||||||
|
height: 1.5em;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
font-size: .9em;
|
||||||
|
|
||||||
|
input {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(2n) {
|
||||||
|
background-color: var(--modal-permissions-table-row-even);
|
||||||
|
}
|
||||||
|
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
415
shared/js/ui/modal/input-processor/Renderer.tsx
Normal file
415
shared/js/ui/modal/input-processor/Renderer.tsx
Normal file
|
@ -0,0 +1,415 @@
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
import React, {useContext, useState} from "react";
|
||||||
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {IpcRegistryDescription, Registry} from "tc-events";
|
||||||
|
import {ModalInputProcessorEvents, ModalInputProcessorVariables} from "tc-shared/ui/modal/input-processor/Definitios";
|
||||||
|
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||||
|
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||||
|
import {InputProcessorStatistics} from "tc-shared/voice/RecorderBase";
|
||||||
|
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||||
|
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||||
|
import {ControlledBoxedInputField, ControlledSelect} from "tc-shared/ui/react-elements/InputField";
|
||||||
|
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||||
|
import {traj} from "tc-shared/i18n/localize";
|
||||||
|
|
||||||
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
const EventContext = React.createContext<Registry<ModalInputProcessorEvents>>(undefined);
|
||||||
|
const VariableContext = React.createContext<UiVariableConsumer<ModalInputProcessorVariables>>(undefined);
|
||||||
|
const StatisticsContext = React.createContext<InputProcessorStatistics>(undefined);
|
||||||
|
|
||||||
|
const TablePropertiesHead = React.memo(() => (
|
||||||
|
<div className={cssStyle.tableEntry + " " + cssStyle.tableHeader}>
|
||||||
|
<div className={joinClassList(cssStyle.column, cssStyle.name)}>
|
||||||
|
<div className={cssStyle.header}>
|
||||||
|
<Translatable>Property</Translatable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={joinClassList(cssStyle.column, cssStyle.value)}>
|
||||||
|
<div className={cssStyle.header}>
|
||||||
|
<Translatable>Value</Translatable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
type PropertyType = {
|
||||||
|
type: "integer" | "float" | "boolean"
|
||||||
|
} | {
|
||||||
|
type: "select",
|
||||||
|
values: string[]
|
||||||
|
};
|
||||||
|
|
||||||
|
const PropertyValueBooleanRenderer = React.memo((props: { property: keyof ModalInputProcessorVariables }) => {
|
||||||
|
const variables = useContext(VariableContext);
|
||||||
|
const value = variables.useVariable(props.property);
|
||||||
|
|
||||||
|
if(value.status === "loading") {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
value={value.localValue as any}
|
||||||
|
disabled={value.status !== "loaded"}
|
||||||
|
onChange={newValue => value.setValue(newValue)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const PropertyValueNumberRenderer = React.memo((props: { property: keyof ModalInputProcessorVariables }) => {
|
||||||
|
const variables = useContext(VariableContext);
|
||||||
|
const value = variables.useVariable(props.property);
|
||||||
|
|
||||||
|
if(value.status === "loading") {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<ControlledBoxedInputField
|
||||||
|
className={cssStyle.containerInput}
|
||||||
|
type={"number"}
|
||||||
|
value={value.localValue as any}
|
||||||
|
|
||||||
|
onChange={newValue => value.setValue(newValue as any, true)}
|
||||||
|
onBlur={() => {
|
||||||
|
const targetValue = parseFloat(value.localValue as any);
|
||||||
|
if(isNaN(targetValue)) {
|
||||||
|
value.setValue(value.remoteValue, true);
|
||||||
|
} else {
|
||||||
|
value.setValue(targetValue, false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
finishOnEnter={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const PropertyValueSelectRenderer = React.memo((props: { property: keyof ModalInputProcessorVariables, options: string[] }) => {
|
||||||
|
const variables = useContext(VariableContext);
|
||||||
|
const value = variables.useVariable(props.property);
|
||||||
|
|
||||||
|
if(value.status === "loading") {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
const index = props.options.indexOf(value.localValue as any);
|
||||||
|
return (
|
||||||
|
<ControlledSelect
|
||||||
|
className={cssStyle.containerInput}
|
||||||
|
type={"boxed"}
|
||||||
|
|
||||||
|
value={index === -1 ? "local-value" : index.toString()}
|
||||||
|
disabled={value.status !== "loaded"}
|
||||||
|
onChange={event => {
|
||||||
|
const targetIndex = parseInt(event.target.value);
|
||||||
|
if(isNaN(targetIndex)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.setValue(props.options[targetIndex]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option key={"empty"} style={{ display: "none" }}/>
|
||||||
|
<option key={"local-value"} style={{ display: "none" }}>{value.localValue}</option>
|
||||||
|
{props.options.map((option, index) => (
|
||||||
|
<option key={"option_" + index} value={index}>{option}</option>
|
||||||
|
)) as any}
|
||||||
|
</ControlledSelect>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const TablePropertyValue = React.memo((props: { property: keyof ModalInputProcessorVariables, type: PropertyType }) => {
|
||||||
|
const variables = useContext(VariableContext);
|
||||||
|
const filter = variables.useReadOnly("propertyFilter", undefined, undefined);
|
||||||
|
if(typeof filter === "string" && filter.length > 0) {
|
||||||
|
const key = props.property as string;
|
||||||
|
if(key.toLowerCase().indexOf(filter) === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let value;
|
||||||
|
switch (props.type.type) {
|
||||||
|
case "integer":
|
||||||
|
case "float":
|
||||||
|
value = <PropertyValueNumberRenderer property={props.property} key={"number"} />;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "boolean":
|
||||||
|
value = <PropertyValueBooleanRenderer property={props.property} key={"boolean"} />;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "select":
|
||||||
|
value = <PropertyValueSelectRenderer options={props.type.values} property={props.property} key={"select"} />;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.tableEntry}>
|
||||||
|
<div className={joinClassList(cssStyle.column, cssStyle.name)}>
|
||||||
|
<div className={cssStyle.text}>{props.property}</div>
|
||||||
|
</div>
|
||||||
|
<div className={joinClassList(cssStyle.column, cssStyle.value)}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PropertyFilter = React.memo(() => {
|
||||||
|
const variables = useContext(VariableContext);
|
||||||
|
const filter = variables.useVariable("propertyFilter");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.containerFilter}>
|
||||||
|
<ControlledBoxedInputField
|
||||||
|
disabled={filter.status === "loading"}
|
||||||
|
onChange={newValue => filter.setValue(newValue)}
|
||||||
|
value={filter.localValue}
|
||||||
|
placeholder={useTr("Filter")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const Properties = React.memo(() => {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.containerProperties}>
|
||||||
|
<div className={cssStyle.title}>
|
||||||
|
<Translatable>Properties</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.note}>
|
||||||
|
<Translatable>Note: All changes are temporary and will be reset on the next restart.</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.table}>
|
||||||
|
<TablePropertiesHead />
|
||||||
|
<div className={cssStyle.tableBody}>
|
||||||
|
<TablePropertyValue property={"pipeline.maximum_internal_processing_rate"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"pipeline.multi_channel_render"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"pipeline.multi_channel_capture"} type={{ type: "boolean" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"pre_amplifier.enabled"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"pre_amplifier.fixed_gain_factor"} type={{ type: "float" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"high_pass_filter.enabled"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"high_pass_filter.apply_in_full_band"} type={{ type: "boolean" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"echo_canceller.enabled"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"echo_canceller.mobile_mode"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"echo_canceller.export_linear_aec_output"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"echo_canceller.enforce_high_pass_filtering"} type={{ type: "boolean" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"noise_suppression.enabled"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"noise_suppression.level"} type={{
|
||||||
|
type: "select",
|
||||||
|
values: ["low", "moderate", "high", "very-high"]
|
||||||
|
}} />
|
||||||
|
<TablePropertyValue property={"noise_suppression.analyze_linear_aec_output_when_available"} type={{ type: "boolean" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"transient_suppression.enabled"} type={{ type: "boolean" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"voice_detection.enabled"} type={{ type: "boolean" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"gain_controller1.enabled"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller1.mode"} type={{
|
||||||
|
type: "select",
|
||||||
|
values: ["adaptive-analog", "adaptive-digital", "fixed-digital"]
|
||||||
|
}} />
|
||||||
|
<TablePropertyValue property={"gain_controller1.target_level_dbfs"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller1.compression_gain_db"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller1.enable_limiter"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller1.analog_level_minimum"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller1.analog_level_maximum"} type={{ type: "float" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"gain_controller1.analog_gain_controller.enabled"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller1.analog_gain_controller.startup_min_volume"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller1.analog_gain_controller.clipped_level_min"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller1.analog_gain_controller.enable_agc2_level_estimator"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller1.analog_gain_controller.enable_digital_adaptive"} type={{ type: "boolean" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"gain_controller2.enabled"} type={{ type: "boolean" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"gain_controller2.fixed_digital.gain_db"} type={{ type: "float" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"gain_controller2.adaptive_digital.enabled"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller2.adaptive_digital.vad_probability_attack"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller2.adaptive_digital.level_estimator"} type={{
|
||||||
|
type: "select",
|
||||||
|
values: ["rms", "peak"]
|
||||||
|
}} />
|
||||||
|
<TablePropertyValue property={"gain_controller2.adaptive_digital.level_estimator_adjacent_speech_frames_threshold"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller2.adaptive_digital.use_saturation_protector"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller2.adaptive_digital.initial_saturation_margin_db"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller2.adaptive_digital.extra_saturation_margin_db"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller2.adaptive_digital.gain_applier_adjacent_speech_frames_threshold"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller2.adaptive_digital.max_gain_change_db_per_second"} type={{ type: "float" }} />
|
||||||
|
<TablePropertyValue property={"gain_controller2.adaptive_digital.max_output_noise_level_dbfs"} type={{ type: "float" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"residual_echo_detector.enabled"} type={{ type: "boolean" }} />
|
||||||
|
<TablePropertyValue property={"level_estimation.enabled"} type={{ type: "boolean" }} />
|
||||||
|
|
||||||
|
<TablePropertyValue property={"rnnoise.enabled"} type={{ type: "boolean" }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PropertyFilter />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const StatisticValue = React.memo((props: { statisticKey: keyof InputProcessorStatistics }) => {
|
||||||
|
const statistics = useContext(StatisticsContext);
|
||||||
|
|
||||||
|
let value;
|
||||||
|
switch(statistics ? typeof statistics[props.statisticKey] : "undefined") {
|
||||||
|
case "number":
|
||||||
|
value = statistics[props.statisticKey].toPrecision(4);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "boolean":
|
||||||
|
value = statistics[props.statisticKey] ? (
|
||||||
|
<Translatable key={"true"}>true</Translatable>
|
||||||
|
) : (
|
||||||
|
<Translatable key={"false"}>false</Translatable>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "string":
|
||||||
|
value = statistics[props.statisticKey];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
value = (
|
||||||
|
<div className={cssStyle.unset}>
|
||||||
|
<Translatable>unset</Translatable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const Statistic = React.memo((props: { statisticKey: keyof InputProcessorStatistics, children }) => (
|
||||||
|
<div className={cssStyle.statistic}>
|
||||||
|
<div className={cssStyle.key}>
|
||||||
|
{props.children}:
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.value}>
|
||||||
|
<StatisticValue statisticKey={props.statisticKey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
const StatisticsProvider = React.memo((props: { children }) => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
const [ statistics, setStatistics ] = useState<InputProcessorStatistics>(() => {
|
||||||
|
events.fire("query_statistics");
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
events.reactUse("notify_statistics", event => setStatistics(event.statistics), undefined, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatisticsContext.Provider value={statistics}>
|
||||||
|
{props.children}
|
||||||
|
</StatisticsContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Statistics = React.memo(() => (
|
||||||
|
<StatisticsProvider>
|
||||||
|
<div className={cssStyle.containerStatistics}>
|
||||||
|
<div className={cssStyle.title}>
|
||||||
|
<Translatable>Statistics</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.statistics}>
|
||||||
|
<Statistic statisticKey={"output_rms_dbfs"}>
|
||||||
|
<Translatable>Output RMS (dbfs)</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<Statistic statisticKey={"voice_detected"}>
|
||||||
|
<Translatable>Voice detected</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<Statistic statisticKey={"echo_return_loss"}>
|
||||||
|
<Translatable>Echo return loss</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<Statistic statisticKey={"echo_return_loss_enhancement"}>
|
||||||
|
<Translatable>Echo return loss enchancement</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<Statistic statisticKey={"delay_median_ms"}>
|
||||||
|
<Translatable>Delay median (ms)</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<Statistic statisticKey={"delay_ms"}>
|
||||||
|
<Translatable>Delay (ms)</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<Statistic statisticKey={"delay_standard_deviation_ms"}>
|
||||||
|
<Translatable>Delay standard deviation (ms)</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<Statistic statisticKey={"divergent_filter_fraction"}>
|
||||||
|
<Translatable>Divergent filter fraction</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<Statistic statisticKey={"residual_echo_likelihood"}>
|
||||||
|
<Translatable>Residual echo likelihood</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<Statistic statisticKey={"residual_echo_likelihood_recent_max"}>
|
||||||
|
<Translatable>Residual echo likelihood (max)</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<Statistic statisticKey={"rnnoise_volume"}>
|
||||||
|
<Translatable>RNNoise volume</Translatable>
|
||||||
|
</Statistic>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</StatisticsProvider>
|
||||||
|
));
|
||||||
|
|
||||||
|
class Modal extends AbstractModal {
|
||||||
|
private readonly events: Registry<ModalInputProcessorEvents>;
|
||||||
|
private readonly variables: UiVariableConsumer<ModalInputProcessorVariables>;
|
||||||
|
|
||||||
|
constructor(events: IpcRegistryDescription<ModalInputProcessorEvents>, variables: IpcVariableDescriptor<ModalInputProcessorVariables>) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.events = Registry.fromIpcDescription(events);
|
||||||
|
this.variables = createIpcUiVariableConsumer(variables);
|
||||||
|
|
||||||
|
this.events.on("notify_apply_error", event => {
|
||||||
|
createErrorModal(tr("Failed to apply changes"), traj("Failed to apply changes:{:br:}{}", event.message)).open();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
this.events.destroy();
|
||||||
|
this.variables.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<EventContext.Provider value={this.events}>
|
||||||
|
<VariableContext.Provider value={this.variables}>
|
||||||
|
<div className={joinClassList(cssStyle.container, this.properties.windowed && cssStyle.windowed)}>
|
||||||
|
<Properties />
|
||||||
|
<Statistics />
|
||||||
|
</div>
|
||||||
|
</VariableContext.Provider>
|
||||||
|
</EventContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTitle(): string | React.ReactElement {
|
||||||
|
return <Translatable>Input processor properties</Translatable>;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
|
@ -1,49 +1,6 @@
|
||||||
@import "../../../../css/static/mixin";
|
@import "../../../../css/static/mixin";
|
||||||
@import "../../../../css/static/properties";
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
html:root {
|
|
||||||
--modal-permissions-header-text: #e1e1e1;
|
|
||||||
--modal-permissions-header-background: #19191b;
|
|
||||||
--modal-permissions-header-hover: #4e4e4e;
|
|
||||||
--modal-permissions-header-selected: #0073d4;
|
|
||||||
|
|
||||||
--modal-permission-right: #303036;
|
|
||||||
--modal-permission-left: #222226;
|
|
||||||
|
|
||||||
--modal-permissions-entry-hover: #28282c;
|
|
||||||
--modal-permissions-entry-selected: #111111;
|
|
||||||
--modal-permissions-current-group: #101012;
|
|
||||||
|
|
||||||
--modal-permissions-buttons-background: #0f0f0f;
|
|
||||||
--modal-permissions-buttons-hover: #262626;
|
|
||||||
--modal-permissions-buttons-disabled: #1b1b1b;
|
|
||||||
|
|
||||||
--modal-permissions-seperator: #1e1e1e; /* the seperator for the "enter a unique id" and "client info" part */
|
|
||||||
--modal-permissions-container-seperator: #222224; /* the seperator between left and right */
|
|
||||||
|
|
||||||
--modal-permissions-icon-select: #121213;
|
|
||||||
--modal-permissions-icon-select-border: #0d0d0d;
|
|
||||||
--modal-permissions-icon-select-hover: #17171a;
|
|
||||||
--modal-permissions-icon-select-hover-border: #333333;
|
|
||||||
|
|
||||||
--modal-permission-no-permnissions: #18171c;
|
|
||||||
--modal-permissions-table-border: #1e2025;
|
|
||||||
|
|
||||||
--modal-permissions-table-header: #303036;
|
|
||||||
--modal-permissions-table-row-odd: #303036;
|
|
||||||
--modal-permissions-table-row-even: #25252a;
|
|
||||||
--modal-permissions-table-row-hover: #343a47;
|
|
||||||
|
|
||||||
--modal-permissions-table-header-text: #e1e1e1;
|
|
||||||
--modal-permissions-table-row-text: #535455;
|
|
||||||
--modal-permissions-table-entry-active-text: #e1e1e1;
|
|
||||||
--modal-permissions-table-entry-group-text: #e1e1e1;
|
|
||||||
|
|
||||||
--modal-permissions-table-input: #e1e1e1;
|
|
||||||
--modal-permissions-table-input-focus: #3f7dbf;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@include user-select(none);
|
@include user-select(none);
|
||||||
|
|
||||||
|
|
|
@ -260,7 +260,7 @@
|
||||||
|
|
||||||
padding-bottom: .5em;
|
padding-bottom: .5em;
|
||||||
|
|
||||||
a {
|
.text {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
@ -284,6 +284,20 @@
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
min-width: 8em;
|
min-width: 8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
align-self: flex-end;
|
||||||
|
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipContainer {
|
||||||
|
min-width: 14em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.containerDevices {
|
.containerDevices {
|
||||||
|
|
|
@ -1,92 +1,21 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {LevelMeter} from "tc-shared/voice/RecorderBase";
|
import {AbstractInput, FilterMode, LevelMeter} from "tc-shared/voice/RecorderBase";
|
||||||
import {LogCategory, logTrace, logWarn} from "tc-shared/log";
|
import {LogCategory, logError, logTrace, logWarn} from "tc-shared/log";
|
||||||
import {defaultRecorder} from "tc-shared/voice/RecorderProfile";
|
import {defaultRecorder} from "tc-shared/voice/RecorderProfile";
|
||||||
import {DeviceListState, getRecorderBackend, InputDevice} from "tc-shared/audio/Recorder";
|
import {getRecorderBackend, InputDevice} from "tc-shared/audio/Recorder";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
import {getBackend} from "tc-shared/backend";
|
import {getBackend} from "tc-shared/backend";
|
||||||
import * as _ from "lodash";
|
import * as _ from "lodash";
|
||||||
import {getAudioBackend} from "tc-shared/audio/Player";
|
import {getAudioBackend} from "tc-shared/audio/Player";
|
||||||
|
import {
|
||||||
export type MicrophoneSetting =
|
InputDeviceLevel,
|
||||||
"volume"
|
MicrophoneSettingsEvents,
|
||||||
| "vad-type"
|
SelectedMicrophone
|
||||||
| "ppt-key"
|
} from "tc-shared/ui/modal/settings/MicrophoneDefinitions";
|
||||||
| "ppt-release-delay"
|
import {spawnInputProcessorModal} from "tc-shared/ui/modal/input-processor/Controller";
|
||||||
| "ppt-release-delay-active"
|
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||||
| "threshold-threshold"
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
| "rnnoise";
|
|
||||||
|
|
||||||
export type MicrophoneDevice = {
|
|
||||||
id: string,
|
|
||||||
name: string,
|
|
||||||
driver: string,
|
|
||||||
default: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SelectedMicrophone = { type: "default" } | { type: "none" } | { type: "device", deviceId: string };
|
|
||||||
export type MicrophoneDevices = {
|
|
||||||
status: "error",
|
|
||||||
error: string
|
|
||||||
} | {
|
|
||||||
status: "audio-not-initialized"
|
|
||||||
} | {
|
|
||||||
status: "no-permissions",
|
|
||||||
shouldAsk: boolean
|
|
||||||
} | {
|
|
||||||
status: "success",
|
|
||||||
devices: MicrophoneDevice[]
|
|
||||||
selectedDevice: SelectedMicrophone;
|
|
||||||
};
|
|
||||||
export interface MicrophoneSettingsEvents {
|
|
||||||
"query_devices": { refresh_list: boolean },
|
|
||||||
"query_help": {},
|
|
||||||
"query_setting": {
|
|
||||||
setting: MicrophoneSetting
|
|
||||||
},
|
|
||||||
|
|
||||||
"action_help_click": {},
|
|
||||||
"action_request_permissions": {},
|
|
||||||
"action_set_selected_device": { target: SelectedMicrophone },
|
|
||||||
"action_set_selected_device_result": {
|
|
||||||
status: "error",
|
|
||||||
reason: string
|
|
||||||
},
|
|
||||||
|
|
||||||
"action_set_setting": {
|
|
||||||
setting: MicrophoneSetting;
|
|
||||||
value: any;
|
|
||||||
},
|
|
||||||
|
|
||||||
notify_setting: {
|
|
||||||
setting: MicrophoneSetting;
|
|
||||||
value: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
notify_devices: MicrophoneDevices,
|
|
||||||
notify_device_selected: { device: SelectedMicrophone },
|
|
||||||
|
|
||||||
notify_device_level: {
|
|
||||||
level: {
|
|
||||||
[key: string]: {
|
|
||||||
deviceId: string,
|
|
||||||
status: "success" | "error",
|
|
||||||
|
|
||||||
level?: number,
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
status: Exclude<DeviceListState, "error">
|
|
||||||
},
|
|
||||||
|
|
||||||
notify_highlight: {
|
|
||||||
field: "hs-0" | "hs-1" | "hs-2" | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
notify_destroy: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initialize_audio_microphone_controller(events: Registry<MicrophoneSettingsEvents>) {
|
export function initialize_audio_microphone_controller(events: Registry<MicrophoneSettingsEvents>) {
|
||||||
const recorderBackend = getRecorderBackend();
|
const recorderBackend = getRecorderBackend();
|
||||||
|
@ -219,6 +148,47 @@ export function initialize_audio_microphone_controller(events: Registry<Micropho
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let currentLevel: InputDeviceLevel = { status: "uninitialized" };
|
||||||
|
let levelMeter: LevelMeter;
|
||||||
|
|
||||||
|
/* input device level meter */
|
||||||
|
const initializeInput = (input: AbstractInput) => {
|
||||||
|
try {
|
||||||
|
levelMeter?.destroy();
|
||||||
|
|
||||||
|
levelMeter = input.createLevelMeter();
|
||||||
|
levelMeter.setObserver(value => {
|
||||||
|
currentLevel = { status: "success", level: value };
|
||||||
|
events.fire_react("notify_input_level", { level: currentLevel });
|
||||||
|
});
|
||||||
|
|
||||||
|
currentLevel = { status: "success", level: 0 };
|
||||||
|
} catch (error) {
|
||||||
|
if(typeof error !== "string") {
|
||||||
|
logError(LogCategory.GENERAL, tr("Failed to create input device level meter: %o"), error);
|
||||||
|
error = tr("lookup the console");
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevel = { status: "error", message: error };
|
||||||
|
}
|
||||||
|
events.fire_react("notify_input_level", { level: currentLevel });
|
||||||
|
}
|
||||||
|
|
||||||
|
events.on("notify_destroy", () => {
|
||||||
|
levelMeter?.setObserver(undefined);
|
||||||
|
levelMeter?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("query_input_level", () => events.fire_react("notify_input_level", { level: currentLevel }));
|
||||||
|
|
||||||
|
if(defaultRecorder.input) {
|
||||||
|
initializeInput(defaultRecorder.input);
|
||||||
|
} else {
|
||||||
|
events.on("notify_destroy", defaultRecorder.events.one("notify_input_initialized", () => initializeInput(defaultRecorder.input)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* device list */
|
/* device list */
|
||||||
{
|
{
|
||||||
const currentSelectedDevice = (): SelectedMicrophone => {
|
const currentSelectedDevice = (): SelectedMicrophone => {
|
||||||
|
@ -444,6 +414,16 @@ export function initialize_audio_microphone_controller(events: Registry<Micropho
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
events.on("action_open_processor_properties", () => {
|
||||||
|
const processor = defaultRecorder.input?.getInputProcessor();
|
||||||
|
if(!processor) {
|
||||||
|
createErrorModal(tr("Missing input processor"), tr("Missing default recorders input processor.")).open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnInputProcessorModal(processor);
|
||||||
|
});
|
||||||
|
|
||||||
events.on("notify_destroy", recorderBackend.getDeviceList().getEvents().on("notify_list_updated", () => {
|
events.on("notify_destroy", recorderBackend.getDeviceList().getEvents().on("notify_list_updated", () => {
|
||||||
events.fire("query_devices");
|
events.fire("query_devices");
|
||||||
}));
|
}));
|
||||||
|
@ -455,45 +435,28 @@ export function initialize_audio_microphone_controller(events: Registry<Micropho
|
||||||
if(!getAudioBackend().isInitialized()) {
|
if(!getAudioBackend().isInitialized()) {
|
||||||
getAudioBackend().executeWhenInitialized(() => events.fire_react("query_devices"));
|
getAudioBackend().executeWhenInitialized(() => events.fire_react("query_devices"));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/* TODO: Only do this on user request? */
|
||||||
import * as loader from "tc-loader";
|
{
|
||||||
import {Stage} from "tc-loader";
|
const ownDefaultRecorder = () => {
|
||||||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
const originalHandlerId = defaultRecorder.current_handler?.handlerId;
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
defaultRecorder.unmount().then(() => {
|
||||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
defaultRecorder.input.start().catch(error => {
|
||||||
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
|
logError(LogCategory.AUDIO, tr("Failed to start default input: %o"), error);
|
||||||
|
});
|
||||||
loader.register_task(Stage.LOADED, {
|
|
||||||
name: "test",
|
|
||||||
function: async () => {
|
|
||||||
aplayer.on_ready(() => {
|
|
||||||
const modal = spawnReactModal(class extends InternalModal {
|
|
||||||
settings = new Registry<MicrophoneSettingsEvents>();
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
initialize_audio_microphone_controller(this.settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBody(): React.ReactElement {
|
|
||||||
return <div style={{
|
|
||||||
padding: "1em",
|
|
||||||
backgroundColor: "#2f2f35"
|
|
||||||
}}>
|
|
||||||
<MicrophoneSettings events={this.settings} />
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
title(): string | React.ReactElement<Translatable> {
|
|
||||||
return "test";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modal.show();
|
events.on("notify_destroy", () => {
|
||||||
|
server_connections.findConnection(originalHandlerId)?.acquireInputHardware().catch(error => {
|
||||||
|
logError(LogCategory.GENERAL, tr("Failed to acquire microphone after settings detach: %o"), error);
|
||||||
});
|
});
|
||||||
},
|
|
||||||
priority: -2
|
|
||||||
});
|
});
|
||||||
*/
|
};
|
||||||
|
|
||||||
|
if(defaultRecorder.input) {
|
||||||
|
ownDefaultRecorder();
|
||||||
|
} else {
|
||||||
|
events.on("notify_destroy", defaultRecorder.events.one("notify_input_initialized", () => ownDefaultRecorder()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
shared/js/ui/modal/settings/MicrophoneDefinitions.ts
Normal file
96
shared/js/ui/modal/settings/MicrophoneDefinitions.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import {DeviceListState} from "tc-shared/audio/Recorder";
|
||||||
|
|
||||||
|
export type MicrophoneSetting =
|
||||||
|
"volume"
|
||||||
|
| "vad-type"
|
||||||
|
| "ppt-key"
|
||||||
|
| "ppt-release-delay"
|
||||||
|
| "ppt-release-delay-active"
|
||||||
|
| "threshold-threshold"
|
||||||
|
| "rnnoise";
|
||||||
|
|
||||||
|
export type MicrophoneDevice = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
driver: string,
|
||||||
|
default: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SelectedMicrophone = { type: "default" } | { type: "none" } | { type: "device", deviceId: string };
|
||||||
|
export type MicrophoneDevices = {
|
||||||
|
status: "error",
|
||||||
|
error: string
|
||||||
|
} | {
|
||||||
|
status: "audio-not-initialized"
|
||||||
|
} | {
|
||||||
|
status: "no-permissions",
|
||||||
|
shouldAsk: boolean
|
||||||
|
} | {
|
||||||
|
status: "success",
|
||||||
|
devices: MicrophoneDevice[]
|
||||||
|
selectedDevice: SelectedMicrophone;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InputDeviceLevel = {
|
||||||
|
status: "success",
|
||||||
|
level: number
|
||||||
|
} | {
|
||||||
|
status: "uninitialized"
|
||||||
|
} | {
|
||||||
|
status: "error",
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MicrophoneSettingsEvents {
|
||||||
|
"query_devices": { refresh_list: boolean },
|
||||||
|
"query_help": {},
|
||||||
|
"query_setting": {
|
||||||
|
setting: MicrophoneSetting
|
||||||
|
},
|
||||||
|
"query_input_level": {}
|
||||||
|
|
||||||
|
"action_help_click": {},
|
||||||
|
"action_request_permissions": {},
|
||||||
|
"action_set_selected_device": { target: SelectedMicrophone },
|
||||||
|
"action_set_selected_device_result": {
|
||||||
|
status: "error",
|
||||||
|
reason: string
|
||||||
|
},
|
||||||
|
"action_open_processor_properties": {},
|
||||||
|
|
||||||
|
"action_set_setting": {
|
||||||
|
setting: MicrophoneSetting;
|
||||||
|
value: any;
|
||||||
|
},
|
||||||
|
|
||||||
|
notify_setting: {
|
||||||
|
setting: MicrophoneSetting;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_devices: MicrophoneDevices,
|
||||||
|
notify_device_selected: { device: SelectedMicrophone },
|
||||||
|
|
||||||
|
notify_device_level: {
|
||||||
|
level: {
|
||||||
|
[key: string]: {
|
||||||
|
deviceId: string,
|
||||||
|
status: "success" | "error",
|
||||||
|
|
||||||
|
level?: number,
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
status: Exclude<DeviceListState, "error">
|
||||||
|
},
|
||||||
|
notify_input_level: {
|
||||||
|
level: InputDeviceLevel
|
||||||
|
},
|
||||||
|
|
||||||
|
notify_highlight: {
|
||||||
|
field: "hs-0" | "hs-1" | "hs-2" | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_destroy: {}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useEffect, useRef, useState} from "react";
|
import {useContext, useEffect, useRef, useState} from "react";
|
||||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {MicrophoneDevice, MicrophoneSettingsEvents, SelectedMicrophone} from "tc-shared/ui/modal/settings/Microphone";
|
import {MicrophoneDevice, MicrophoneSettingsEvents, SelectedMicrophone} from "tc-shared/ui/modal/settings/MicrophoneDefinitions";
|
||||||
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
|
@ -17,8 +17,12 @@ import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||||
import {BoxedInputField} from "tc-shared/ui/react-elements/InputField";
|
import {BoxedInputField} from "tc-shared/ui/react-elements/InputField";
|
||||||
import {HighlightContainer, HighlightRegion, HighlightText} from "./Heighlight";
|
import {HighlightContainer, HighlightRegion, HighlightText} from "./Heighlight";
|
||||||
import {InputDevice} from "tc-shared/audio/Recorder";
|
import {InputDevice} from "tc-shared/audio/Recorder";
|
||||||
|
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
|
||||||
|
import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
const cssStyle = require("./Microphone.scss");
|
const cssStyle = require("./Microphone.scss");
|
||||||
|
const EventContext = React.createContext<Registry<MicrophoneSettingsEvents>>(undefined);
|
||||||
|
|
||||||
type MicrophoneSelectedState = "selected" | "applying" | "unselected";
|
type MicrophoneSelectedState = "selected" | "applying" | "unselected";
|
||||||
const MicrophoneStatus = (props: { state: MicrophoneSelectedState }) => {
|
const MicrophoneStatus = (props: { state: MicrophoneSelectedState }) => {
|
||||||
|
@ -34,89 +38,134 @@ const MicrophoneStatus = (props: { state: MicrophoneSelectedState }) => {
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case "selected":
|
case "selected":
|
||||||
return <ClientIconRenderer key={"selected"} icon={ClientIcon.Apply}/>;
|
return (
|
||||||
|
<ClientIconRenderer key={"selected"} icon={ClientIcon.Apply} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActivityBarStatus =
|
type ActivityBarStatus =
|
||||||
{ mode: "success" }
|
{ mode: "success", level: number }
|
||||||
| { mode: "error", message: string }
|
| { mode: "error", message: string }
|
||||||
| { mode: "loading" }
|
| { mode: "loading" }
|
||||||
| { mode: "uninitialized" };
|
| { mode: "uninitialized" };
|
||||||
const ActivityBar = (props: { events: Registry<MicrophoneSettingsEvents>, deviceId: string | "none", disabled?: boolean }) => {
|
|
||||||
const refHider = useRef<HTMLDivElement>();
|
const ActivityBarStatusContext = React.createContext<ActivityBarStatus>({ mode: "loading" });
|
||||||
|
|
||||||
|
const DeviceActivityBarStatusProvider = React.memo((props: { deviceId: string, children }) => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
const [ status, setStatus ] = useState<ActivityBarStatus>({ mode: "loading" });
|
const [ status, setStatus ] = useState<ActivityBarStatus>({ mode: "loading" });
|
||||||
|
|
||||||
if(typeof props.deviceId === "undefined") {
|
const updateState = (newState: ActivityBarStatus) => {
|
||||||
throw "invalid device id";
|
if(!_.isEqual(newState, status)) {
|
||||||
|
setStatus(newState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
props.events.reactUse("notify_device_level", event => {
|
events.reactUse("notify_device_level", event => {
|
||||||
refHider.current.style.width = "100%";
|
|
||||||
if(event.status === "uninitialized") {
|
if(event.status === "uninitialized") {
|
||||||
if (status.mode === "uninitialized") {
|
updateState({ mode: "uninitialized" });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus({mode: "uninitialized"});
|
|
||||||
} else if(event.status === "no-permissions") {
|
} else if(event.status === "no-permissions") {
|
||||||
const noPermissionsMessage = tr("no permissions");
|
updateState({ mode: "error", message: tr("no permissions") });
|
||||||
if (status.mode === "error" && status.message === noPermissionsMessage) {
|
} else if(event.status === "healthy") {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus({mode: "error", message: noPermissionsMessage});
|
|
||||||
} else {
|
|
||||||
const device = event.level[props.deviceId];
|
const device = event.level[props.deviceId];
|
||||||
if (!device) {
|
if (!device) {
|
||||||
if (status.mode === "loading") {
|
updateState({ mode: "loading" });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus({mode: "loading"});
|
|
||||||
} else if(device.status === "success") {
|
} else if(device.status === "success") {
|
||||||
if (status.mode !== "success") {
|
updateState({ mode: "success", level: device.level });
|
||||||
setStatus({mode: "success"});
|
|
||||||
}
|
|
||||||
|
|
||||||
refHider.current.style.width = (100 - device.level) + "%";
|
|
||||||
} else {
|
} else {
|
||||||
if (status.mode === "error" && status.message === device.error) {
|
updateState({ mode: "error", message: device.error });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus({mode: "error", message: device.error + ""});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, true, [status]);
|
}, undefined, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActivityBarStatusContext.Provider value={status}>
|
||||||
|
{props.children}
|
||||||
|
</ActivityBarStatusContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
let error;
|
const InputActivityBarStatusProvider = React.memo((props: { children }) => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
const [ status, setStatus ] = useState<ActivityBarStatus>(() => {
|
||||||
|
events.fire("query_input_level");
|
||||||
|
return { mode: "loading" };
|
||||||
|
});
|
||||||
|
|
||||||
|
events.reactUse("notify_input_level", event => {
|
||||||
|
switch (event.level.status) {
|
||||||
|
case "success":
|
||||||
|
setStatus({ mode: "success", level: event.level.level });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
setStatus({ mode: "error", message: event.level.message });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "uninitialized":
|
||||||
|
setStatus({ mode: "uninitialized" });
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
setStatus({ mode: "error", message: tr("unknown status") });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, undefined, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActivityBarStatusContext.Provider value={status}>
|
||||||
|
{props.children}
|
||||||
|
</ActivityBarStatusContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActivityBar = (props: { disabled?: boolean }) => {
|
||||||
|
const status = useContext(ActivityBarStatusContext);
|
||||||
|
|
||||||
|
let error = undefined;
|
||||||
|
let hiderWidth = "100%";
|
||||||
switch (status.mode) {
|
switch (status.mode) {
|
||||||
case "error":
|
case "error":
|
||||||
error = <div className={cssStyle.text + " " + cssStyle.error} key={"error"}>{status.message}</div>;
|
error = (
|
||||||
|
<div className={joinClassList(cssStyle.text, cssStyle.error)} key={"error"}>
|
||||||
|
{status.message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "loading":
|
case "loading":
|
||||||
error =
|
error = (
|
||||||
<div className={cssStyle.text} key={"loading"}><Translatable>Loading</Translatable> <LoadingDots/>
|
<div className={cssStyle.text} key={"loading"}>
|
||||||
</div>;
|
<Translatable>Loading</Translatable> <LoadingDots/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "success":
|
case "success":
|
||||||
error = undefined;
|
hiderWidth = (100 - status.level) + "%";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cssStyle.containerActivityBar + " " + cssStyle.bar + " " + (props.disabled ? cssStyle.disabled : "")}>
|
className={cssStyle.containerActivityBar + " " + cssStyle.bar + " " + (props.disabled ? cssStyle.disabled : "")}>
|
||||||
<div ref={refHider} className={cssStyle.hider} style={{width: "100%"}}/>
|
<div className={cssStyle.hider} style={{ width: hiderWidth }} />
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Microphone = (props: { events: Registry<MicrophoneSettingsEvents>, device: MicrophoneDevice, state: MicrophoneSelectedState, onClick: () => void }) => {
|
const Microphone = React.memo((props: { device: MicrophoneDevice, state: MicrophoneSelectedState, onClick: () => void }) => {
|
||||||
|
let activityBar;
|
||||||
|
if(props.device.id !== InputDevice.NoDeviceId) {
|
||||||
|
activityBar = (
|
||||||
|
<DeviceActivityBarStatusProvider deviceId={props.device.id} key={"bar"}>
|
||||||
|
<ActivityBar />
|
||||||
|
</DeviceActivityBarStatusProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.device + " " + (props.state === "unselected" ? "" : cssStyle.selected)}
|
<div className={cssStyle.device + " " + (props.state === "unselected" ? "" : cssStyle.selected)}
|
||||||
onClick={props.onClick}>
|
onClick={props.onClick}>
|
||||||
|
@ -128,13 +177,11 @@ const Microphone = (props: { events: Registry<MicrophoneSettingsEvents>, device:
|
||||||
<div className={cssStyle.name}>{props.device.name}</div>
|
<div className={cssStyle.name}>{props.device.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.containerActivity}>
|
<div className={cssStyle.containerActivity}>
|
||||||
{props.device.id === InputDevice.NoDeviceId ? undefined :
|
{activityBar}
|
||||||
<ActivityBar key={"a"} events={props.events} deviceId={props.device.id}/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
type MicrophoneListState = {
|
type MicrophoneListState = {
|
||||||
type: "normal" | "loading" | "audio-not-initialized"
|
type: "normal" | "loading" | "audio-not-initialized"
|
||||||
|
@ -284,7 +331,6 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
|
||||||
name: tr("No device"),
|
name: tr("No device"),
|
||||||
default: false
|
default: false
|
||||||
}}
|
}}
|
||||||
events={props.events}
|
|
||||||
state={deviceSelectState("none")}
|
state={deviceSelectState("none")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (state.type !== "normal" || selectedDevice?.selectingDevice) {
|
if (state.type !== "normal" || selectedDevice?.selectingDevice) {
|
||||||
|
@ -295,10 +341,10 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{deviceList.map(device => <Microphone
|
{deviceList.map(device => (
|
||||||
|
<Microphone
|
||||||
key={"d-" + device.id}
|
key={"d-" + device.id}
|
||||||
device={device}
|
device={device}
|
||||||
events={props.events}
|
|
||||||
state={deviceSelectState(device)}
|
state={deviceSelectState(device)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (state.type !== "normal" || selectedDevice?.selectingDevice) {
|
if (state.type !== "normal" || selectedDevice?.selectingDevice) {
|
||||||
|
@ -311,7 +357,8 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
|
||||||
props.events.fire("action_set_selected_device", { target: { type: "device", deviceId: device.id } });
|
props.events.fire("action_set_selected_device", { target: { type: "device", deviceId: device.id } });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>)}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -405,33 +452,36 @@ const PPTKeyButton = React.memo((props: { events: Registry<MicrophoneSettingsEve
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const PPTDelaySettings = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
const PPTDelaySettings = React.memo(() => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
|
||||||
const [delayActive, setDelayActive] = useState<"loading" | boolean>(() => {
|
const [delayActive, setDelayActive] = useState<"loading" | boolean>(() => {
|
||||||
props.events.fire("query_setting", {setting: "ppt-release-delay"});
|
events.fire("query_setting", {setting: "ppt-release-delay"});
|
||||||
return "loading";
|
return "loading";
|
||||||
});
|
});
|
||||||
|
|
||||||
const [delay, setDelay] = useState<"loading" | number>(() => {
|
const [delay, setDelay] = useState<"loading" | number>(() => {
|
||||||
props.events.fire("query_setting", {setting: "ppt-release-delay-active"});
|
events.fire("query_setting", {setting: "ppt-release-delay-active"});
|
||||||
return "loading";
|
return "loading";
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isActive, setActive] = useState(false);
|
const [isActive, setActive] = useState(false);
|
||||||
|
|
||||||
props.events.reactUse("notify_setting", event => {
|
events.reactUse("notify_setting", event => {
|
||||||
if (event.setting === "vad-type")
|
if (event.setting === "vad-type") {
|
||||||
setActive(event.value === "push_to_talk");
|
setActive(event.value === "push_to_talk");
|
||||||
else if (event.setting === "ppt-release-delay")
|
} else if (event.setting === "ppt-release-delay") {
|
||||||
setDelay(event.value);
|
setDelay(event.value);
|
||||||
else if (event.setting === "ppt-release-delay-active")
|
} else if (event.setting === "ppt-release-delay-active") {
|
||||||
setDelayActive(event.value);
|
setDelayActive(event.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.containerPptDelay}>
|
<div className={cssStyle.containerPptDelay}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
props.events.fire("action_set_setting", {setting: "ppt-release-delay-active", value: value})
|
events.fire("action_set_setting", {setting: "ppt-release-delay-active", value: value})
|
||||||
}}
|
}}
|
||||||
disabled={!isActive}
|
disabled={!isActive}
|
||||||
value={delayActive === true}
|
value={delayActive === true}
|
||||||
|
@ -458,45 +508,48 @@ const PPTDelaySettings = (props: { events: Registry<MicrophoneSettingsEvents> })
|
||||||
|
|
||||||
|
|
||||||
const newValue = event.target.valueAsNumber;
|
const newValue = event.target.valueAsNumber;
|
||||||
if (isNaN(newValue))
|
if (isNaN(newValue)) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (newValue < 0 || newValue > 4000)
|
if (newValue < 0 || newValue > 4000) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
props.events.fire("action_set_setting", {setting: "ppt-release-delay", value: newValue});
|
events.fire("action_set_setting", {setting: "ppt-release-delay", value: newValue});
|
||||||
}}
|
}}
|
||||||
/>}
|
/>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
const RNNoiseLabel = () => (
|
const RNNoiseLabel = React.memo(() => (
|
||||||
<VariadicTranslatable text={"Enable RNNoise cancelation ({})"}>
|
<VariadicTranslatable text={"Enable RNNoise cancellation ({})"}>
|
||||||
<a href={"https://jmvalin.ca/demo/rnnoise/"} target={"_blank"} style={{ margin: 0 }}><Translatable>more info</Translatable></a>
|
<a href={"https://jmvalin.ca/demo/rnnoise/"} target={"_blank"} style={{ margin: 0 }}><Translatable>more info</Translatable></a>
|
||||||
</VariadicTranslatable>
|
</VariadicTranslatable>
|
||||||
)
|
));
|
||||||
|
|
||||||
const RNNoiseSettings = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
const RNNoiseSettings = React.memo(() => {
|
||||||
if(__build.target === "web") {
|
if(__build.target === "web") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const events = useContext(EventContext);
|
||||||
const [ enabled, setEnabled ] = useState<boolean | "loading">(() => {
|
const [ enabled, setEnabled ] = useState<boolean | "loading">(() => {
|
||||||
props.events.fire("query_setting", { setting: "rnnoise" });
|
events.fire("query_setting", { setting: "rnnoise" });
|
||||||
return "loading";
|
return "loading";
|
||||||
});
|
});
|
||||||
props.events.reactUse("notify_setting", event => event.setting === "rnnoise" && setEnabled(event.value));
|
events.reactUse("notify_setting", event => event.setting === "rnnoise" && setEnabled(event.value));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Checkbox label={<RNNoiseLabel />}
|
<Checkbox label={<RNNoiseLabel />}
|
||||||
disabled={enabled === "loading"}
|
disabled={enabled === "loading"}
|
||||||
value={enabled === true}
|
value={enabled === true}
|
||||||
onChange={value => props.events.fire("action_set_setting", { setting: "rnnoise", value: value })}
|
onChange={value => events.fire("action_set_setting", { setting: "rnnoise", value: value })}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
});
|
||||||
|
|
||||||
const VadSelector = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
const VadSelector = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||||
const [selectedType, setSelectedType] = useState<VadType | "loading">(() => {
|
const [selectedType, setSelectedType] = useState<VadType | "loading">(() => {
|
||||||
|
@ -558,10 +611,11 @@ const VadSelector = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThresholdSelector = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
const ThresholdSelector = React.memo(() => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
const refSlider = useRef<Slider>();
|
const refSlider = useRef<Slider>();
|
||||||
const [value, setValue] = useState<"loading" | number>(() => {
|
const [value, setValue] = useState<"loading" | number>(() => {
|
||||||
props.events.fire("query_setting", {setting: "threshold-threshold"});
|
events.fire("query_setting", {setting: "threshold-threshold"});
|
||||||
return "loading";
|
return "loading";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -592,7 +646,7 @@ const ThresholdSelector = (props: { events: Registry<MicrophoneSettingsEvents> }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
props.events.reactUse("notify_setting", event => {
|
events.reactUse("notify_setting", event => {
|
||||||
if (event.setting === "threshold-threshold") {
|
if (event.setting === "threshold-threshold") {
|
||||||
refSlider.current?.setState({value: event.value});
|
refSlider.current?.setState({value: event.value});
|
||||||
setValue(event.value);
|
setValue(event.value);
|
||||||
|
@ -601,7 +655,7 @@ const ThresholdSelector = (props: { events: Registry<MicrophoneSettingsEvents> }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
props.events.reactUse("notify_devices", event => {
|
events.reactUse("notify_devices", event => {
|
||||||
if(event.status === "success") {
|
if(event.status === "success") {
|
||||||
const defaultDevice = event.devices.find(device => device.default);
|
const defaultDevice = event.devices.find(device => device.default);
|
||||||
defaultDeviceId.current = defaultDevice?.id;
|
defaultDeviceId.current = defaultDevice?.id;
|
||||||
|
@ -612,13 +666,15 @@ const ThresholdSelector = (props: { events: Registry<MicrophoneSettingsEvents> }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
props.events.reactUse("notify_device_selected", event => changeCurrentDevice(event.device));
|
events.reactUse("notify_device_selected", event => changeCurrentDevice(event.device));
|
||||||
|
|
||||||
let isActive = isVadActive && currentDevice.type === "device";
|
let isActive = isVadActive && currentDevice.type === "device";
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.containerSensitivity}>
|
<div className={cssStyle.containerSensitivity}>
|
||||||
<div className={cssStyle.containerBar}>
|
<div className={cssStyle.containerBar}>
|
||||||
<ActivityBar events={props.events} deviceId={currentDevice.type === "device" ? currentDevice.deviceId : "none"} disabled={!isActive || !currentDevice} key={"activity-" + currentDevice} />
|
<InputActivityBarStatusProvider>
|
||||||
|
<ActivityBar disabled={!isActive || !currentDevice} key={"activity-" + currentDevice} />
|
||||||
|
</InputActivityBarStatusProvider>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
ref={refSlider}
|
ref={refSlider}
|
||||||
|
@ -635,12 +691,12 @@ const ThresholdSelector = (props: { events: Registry<MicrophoneSettingsEvents> }
|
||||||
disabled={value === "loading" || !isActive}
|
disabled={value === "loading" || !isActive}
|
||||||
|
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
props.events.fire("action_set_setting", {setting: "threshold-threshold", value: value})
|
events.fire("action_set_setting", {setting: "threshold-threshold", value: value})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
};
|
});
|
||||||
|
|
||||||
const HelpText0 = () => (
|
const HelpText0 = () => (
|
||||||
<HighlightText highlightId={"hs-0"} className={cssStyle.help}>
|
<HighlightText highlightId={"hs-0"} className={cssStyle.help}>
|
||||||
|
@ -700,6 +756,24 @@ const HelpText2 = () => (
|
||||||
</HighlightText>
|
</HighlightText>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const InputProcessorButton = React.memo(() => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
|
||||||
|
if(__build.target !== "client") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
color={"none"}
|
||||||
|
type={"extra-small"}
|
||||||
|
onClick={() => events.fire("action_open_processor_properties")}
|
||||||
|
>
|
||||||
|
<Translatable>Input processor properties</Translatable>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const MicrophoneSettings = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
export const MicrophoneSettings = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||||
const [highlighted, setHighlighted] = useState(() => {
|
const [highlighted, setHighlighted] = useState(() => {
|
||||||
props.events.fire("query_help");
|
props.events.fire("query_help");
|
||||||
|
@ -709,13 +783,19 @@ export const MicrophoneSettings = (props: { events: Registry<MicrophoneSettingsE
|
||||||
props.events.reactUse("notify_highlight", event => setHighlighted(event.field));
|
props.events.reactUse("notify_highlight", event => setHighlighted(event.field));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HighlightContainer highlightedId={highlighted} onClick={() => props.events.fire("action_help_click")}
|
<EventContext.Provider value={props.events}>
|
||||||
classList={cssStyle.highlightContainer}>
|
<HighlightContainer
|
||||||
|
highlightedId={highlighted}
|
||||||
|
onClick={() => props.events.fire("action_help_click")}
|
||||||
|
classList={cssStyle.highlightContainer}
|
||||||
|
>
|
||||||
<div className={cssStyle.container}>
|
<div className={cssStyle.container}>
|
||||||
<HelpText0/>
|
<HelpText0/>
|
||||||
<HighlightRegion className={cssStyle.left} highlightId={"hs-1"}>
|
<HighlightRegion className={cssStyle.left} highlightId={"hs-1"}>
|
||||||
<div className={cssStyle.header}>
|
<div className={cssStyle.header}>
|
||||||
<a><Translatable>Select your Microphone Device</Translatable></a>
|
<div className={cssStyle.text}>
|
||||||
|
<Translatable>Select your Microphone Device</Translatable>
|
||||||
|
</div>
|
||||||
<ListRefreshButton events={props.events}/>
|
<ListRefreshButton events={props.events}/>
|
||||||
</div>
|
</div>
|
||||||
<MicrophoneList events={props.events}/>
|
<MicrophoneList events={props.events}/>
|
||||||
|
@ -724,29 +804,42 @@ export const MicrophoneSettings = (props: { events: Registry<MicrophoneSettingsE
|
||||||
<HighlightRegion className={cssStyle.right} highlightId={"hs-2"}>
|
<HighlightRegion className={cssStyle.right} highlightId={"hs-2"}>
|
||||||
<HelpText1/>
|
<HelpText1/>
|
||||||
<div className={cssStyle.header}>
|
<div className={cssStyle.header}>
|
||||||
<a><Translatable>Microphone Settings</Translatable></a>
|
<div className={cssStyle.text}>
|
||||||
|
<Translatable>Microphone Settings</Translatable>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.body}>
|
<div className={cssStyle.body}>
|
||||||
<VolumeSettings events={props.events}/>
|
<VolumeSettings events={props.events}/>
|
||||||
<VadSelector events={props.events}/>
|
<VadSelector events={props.events}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.header}>
|
<div className={cssStyle.header}>
|
||||||
<a><Translatable>Sensitivity Settings</Translatable></a>
|
<div className={cssStyle.text}>
|
||||||
|
<Translatable>Sensitivity Settings</Translatable>
|
||||||
|
</div>
|
||||||
|
<IconTooltip className={cssStyle.icon}>
|
||||||
|
<div className={cssStyle.tooltipContainer}>
|
||||||
|
<Translatable>The volume meter will show the processed audio volume as others would hear you.</Translatable>
|
||||||
|
</div>
|
||||||
|
</IconTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.body}>
|
<div className={cssStyle.body}>
|
||||||
<ThresholdSelector events={props.events}/>
|
<ThresholdSelector />
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.header}>
|
<div className={cssStyle.header}>
|
||||||
<a><Translatable>Advanced Settings</Translatable></a>
|
<div className={cssStyle.text}>
|
||||||
|
<Translatable>Advanced Settings</Translatable>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.body}>
|
<div className={cssStyle.body}>
|
||||||
<div className={cssStyle.containerAdvanced}>
|
<div className={cssStyle.containerAdvanced}>
|
||||||
<PPTDelaySettings events={props.events}/>
|
<PPTDelaySettings />
|
||||||
<RNNoiseSettings events={props.events} />
|
<RNNoiseSettings />
|
||||||
|
<InputProcessorButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HighlightRegion>
|
</HighlightRegion>
|
||||||
</div>
|
</div>
|
||||||
</HighlightContainer>
|
</HighlightContainer>
|
||||||
|
</EventContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -98,6 +98,7 @@ html:root {
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
|
@ -64,7 +64,7 @@ export const ControlledBoxedInputField = (props: {
|
||||||
|
|
||||||
ref={props.refInput}
|
ref={props.refInput}
|
||||||
|
|
||||||
value={props.value || ""}
|
value={typeof props.value !== "undefined" ? props.value : ""}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
|
|
||||||
readOnly={typeof props.editable === "boolean" ? !props.editable : false}
|
readOnly={typeof props.editable === "boolean" ? !props.editable : false}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {PermissionModalEvents} from "tc-shared/ui/modal/permission/ModalDefiniti
|
||||||
import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefinitions";
|
import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefinitions";
|
||||||
import {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer";
|
import {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer";
|
||||||
import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions";
|
import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions";
|
||||||
|
import {ModalInputProcessorEvents, ModalInputProcessorVariables} from "tc-shared/ui/modal/input-processor/Definitios";
|
||||||
|
|
||||||
export type ModalType = "error" | "warning" | "info" | "none";
|
export type ModalType = "error" | "warning" | "info" | "none";
|
||||||
export type ModalRenderType = "page" | "dialog";
|
export type ModalRenderType = "page" | "dialog";
|
||||||
|
@ -210,5 +211,9 @@ export interface ModalConstructorArguments {
|
||||||
/* events */ IpcRegistryDescription<ModalAvatarUploadEvents>,
|
/* events */ IpcRegistryDescription<ModalAvatarUploadEvents>,
|
||||||
/* variables */ IpcVariableDescriptor<ModalAvatarUploadVariables>,
|
/* variables */ IpcVariableDescriptor<ModalAvatarUploadVariables>,
|
||||||
/* serverUniqueId */ string
|
/* serverUniqueId */ string
|
||||||
|
],
|
||||||
|
"modal-input-processor": [
|
||||||
|
/* events */ IpcRegistryDescription<ModalInputProcessorEvents>,
|
||||||
|
/* variables */ IpcVariableDescriptor<ModalInputProcessorVariables>,
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -109,4 +109,8 @@ registerModal({
|
||||||
popoutSupported: true
|
popoutSupported: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerModal({
|
||||||
|
modalId: "modal-input-processor",
|
||||||
|
classLoader: async () => await import("tc-shared/ui/modal/input-processor/Renderer"),
|
||||||
|
popoutSupported: true
|
||||||
|
});
|
|
@ -3,7 +3,7 @@ import {setupIpcHandler} from "tc-shared/ipc/BrowserIPC";
|
||||||
import {initializeI18N} from "tc-shared/i18n/localize";
|
import {initializeI18N} from "tc-shared/i18n/localize";
|
||||||
import {Stage} from "tc-loader";
|
import {Stage} from "tc-loader";
|
||||||
import {AbstractModal, constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions";
|
import {AbstractModal, constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
import {AppParameters} from "tc-shared/settings";
|
import {AppParameters, Settings, settings} from "tc-shared/settings";
|
||||||
import {setupJSRender} from "tc-shared/ui/jsrender";
|
import {setupJSRender} from "tc-shared/ui/jsrender";
|
||||||
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
||||||
import {ModalWindowControllerInstance} from "./Controller";
|
import {ModalWindowControllerInstance} from "./Controller";
|
||||||
|
@ -26,8 +26,16 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
await import("tc-shared/proto");
|
await import("tc-shared/proto");
|
||||||
await initializeI18N();
|
await initializeI18N();
|
||||||
setupIpcHandler();
|
setupIpcHandler();
|
||||||
|
|
||||||
setupJSRender();
|
setupJSRender();
|
||||||
|
|
||||||
|
{
|
||||||
|
const font = settings.getValue(Settings.KEY_FONT_SIZE);
|
||||||
|
|
||||||
|
document.body.style.fontSize = font + "px";
|
||||||
|
settings.globalChangeListener(Settings.KEY_FONT_SIZE, value => {
|
||||||
|
document.body.style.fontSize = value + "px";
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import "../../../../../../css/static/mixin";
|
@import "../../../../../../css/static/mixin";
|
||||||
|
@import "../../../../../../css/static/color-variables";
|
||||||
|
|
||||||
/* FIXME: Remove this wired import */
|
/* FIXME: Remove this wired import */
|
||||||
@import "../../../../../../css/static/general";
|
@import "../../../../../../css/static/general";
|
||||||
|
|
|
@ -404,7 +404,7 @@ export abstract class UiVariableConsumer<Variables extends UiVariableMap> {
|
||||||
}, [ variable, customData ]);
|
}, [ variable, customData ]);
|
||||||
|
|
||||||
if(arguments.length >= 3) {
|
if(arguments.length >= 3) {
|
||||||
return cacheEntry.status === "loaded" ? cacheEntry.currentValue : defaultValue;
|
return cacheEntry.status === "loaded" || cacheEntry.status === "applying" ? cacheEntry.currentValue : defaultValue;
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
status: cacheEntry.status,
|
status: cacheEntry.status,
|
||||||
|
|
|
@ -78,6 +78,196 @@ export enum FilterMode {
|
||||||
Block
|
Block
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All available options for input processing.
|
||||||
|
* Since input processing is only available on the native client these are the options
|
||||||
|
* the native client (especially WebRTC audio processing) have.
|
||||||
|
*/
|
||||||
|
export interface InputProcessorConfigWebRTC {
|
||||||
|
"pipeline.maximum_internal_processing_rate": number,
|
||||||
|
"pipeline.multi_channel_render": boolean,
|
||||||
|
"pipeline.multi_channel_capture": boolean,
|
||||||
|
|
||||||
|
"pre_amplifier.enabled": boolean,
|
||||||
|
"pre_amplifier.fixed_gain_factor": number,
|
||||||
|
|
||||||
|
"high_pass_filter.enabled": boolean,
|
||||||
|
"high_pass_filter.apply_in_full_band": boolean,
|
||||||
|
|
||||||
|
"echo_canceller.enabled": boolean,
|
||||||
|
"echo_canceller.mobile_mode": boolean,
|
||||||
|
"echo_canceller.export_linear_aec_output": boolean,
|
||||||
|
"echo_canceller.enforce_high_pass_filtering": boolean,
|
||||||
|
|
||||||
|
"noise_suppression.enabled": boolean,
|
||||||
|
"noise_suppression.level": "low" | "moderate" | "high" | "very-high",
|
||||||
|
"noise_suppression.analyze_linear_aec_output_when_available": boolean,
|
||||||
|
|
||||||
|
"transient_suppression.enabled": boolean,
|
||||||
|
|
||||||
|
"voice_detection.enabled": boolean,
|
||||||
|
|
||||||
|
"gain_controller1.enabled": boolean,
|
||||||
|
"gain_controller1.mode": "adaptive-analog" | "adaptive-digital" | "fixed-digital",
|
||||||
|
"gain_controller1.target_level_dbfs": number,
|
||||||
|
"gain_controller1.compression_gain_db": number,
|
||||||
|
"gain_controller1.enable_limiter": boolean,
|
||||||
|
"gain_controller1.analog_level_minimum": number,
|
||||||
|
"gain_controller1.analog_level_maximum": number,
|
||||||
|
|
||||||
|
"gain_controller1.analog_gain_controller.enabled": boolean,
|
||||||
|
"gain_controller1.analog_gain_controller.startup_min_volume": number,
|
||||||
|
"gain_controller1.analog_gain_controller.clipped_level_min": number,
|
||||||
|
"gain_controller1.analog_gain_controller.enable_agc2_level_estimator": boolean,
|
||||||
|
"gain_controller1.analog_gain_controller.enable_digital_adaptive": boolean,
|
||||||
|
|
||||||
|
"gain_controller2.enabled": boolean,
|
||||||
|
|
||||||
|
"gain_controller2.fixed_digital.gain_db": number,
|
||||||
|
|
||||||
|
"gain_controller2.adaptive_digital.enabled": boolean,
|
||||||
|
"gain_controller2.adaptive_digital.vad_probability_attack": number,
|
||||||
|
"gain_controller2.adaptive_digital.level_estimator": "rms" | "peak",
|
||||||
|
"gain_controller2.adaptive_digital.level_estimator_adjacent_speech_frames_threshold": number,
|
||||||
|
"gain_controller2.adaptive_digital.use_saturation_protector": boolean,
|
||||||
|
"gain_controller2.adaptive_digital.initial_saturation_margin_db": number,
|
||||||
|
"gain_controller2.adaptive_digital.extra_saturation_margin_db": number,
|
||||||
|
"gain_controller2.adaptive_digital.gain_applier_adjacent_speech_frames_threshold": number,
|
||||||
|
"gain_controller2.adaptive_digital.max_gain_change_db_per_second": number,
|
||||||
|
"gain_controller2.adaptive_digital.max_output_noise_level_dbfs": number,
|
||||||
|
|
||||||
|
"residual_echo_detector.enabled": boolean,
|
||||||
|
"level_estimation.enabled": boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attention:
|
||||||
|
* These keys **MUST** be equal to all keys of `InputProcessorConfigWebRTC`.
|
||||||
|
* All keys not registered in here will not be consideration.
|
||||||
|
*/
|
||||||
|
export const kInputProcessorConfigWebRTCKeys: (keyof InputProcessorConfigWebRTC)[] = [
|
||||||
|
"pipeline.maximum_internal_processing_rate",
|
||||||
|
"pipeline.multi_channel_render",
|
||||||
|
"pipeline.multi_channel_capture",
|
||||||
|
|
||||||
|
"pre_amplifier.enabled",
|
||||||
|
"pre_amplifier.fixed_gain_factor",
|
||||||
|
|
||||||
|
"high_pass_filter.enabled",
|
||||||
|
"high_pass_filter.apply_in_full_band",
|
||||||
|
|
||||||
|
"echo_canceller.enabled",
|
||||||
|
"echo_canceller.mobile_mode",
|
||||||
|
"echo_canceller.export_linear_aec_output",
|
||||||
|
"echo_canceller.enforce_high_pass_filtering",
|
||||||
|
|
||||||
|
"noise_suppression.enabled",
|
||||||
|
"noise_suppression.level",
|
||||||
|
"noise_suppression.analyze_linear_aec_output_when_available",
|
||||||
|
|
||||||
|
"transient_suppression.enabled",
|
||||||
|
|
||||||
|
"voice_detection.enabled",
|
||||||
|
|
||||||
|
"gain_controller1.enabled",
|
||||||
|
"gain_controller1.mode",
|
||||||
|
"gain_controller1.target_level_dbfs",
|
||||||
|
"gain_controller1.compression_gain_db",
|
||||||
|
"gain_controller1.enable_limiter",
|
||||||
|
"gain_controller1.analog_level_minimum",
|
||||||
|
"gain_controller1.analog_level_maximum",
|
||||||
|
|
||||||
|
"gain_controller1.analog_gain_controller.enabled",
|
||||||
|
"gain_controller1.analog_gain_controller.startup_min_volume",
|
||||||
|
"gain_controller1.analog_gain_controller.clipped_level_min",
|
||||||
|
"gain_controller1.analog_gain_controller.enable_agc2_level_estimator",
|
||||||
|
"gain_controller1.analog_gain_controller.enable_digital_adaptive",
|
||||||
|
|
||||||
|
"gain_controller2.enabled",
|
||||||
|
|
||||||
|
"gain_controller2.fixed_digital.gain_db",
|
||||||
|
|
||||||
|
"gain_controller2.adaptive_digital.enabled",
|
||||||
|
"gain_controller2.adaptive_digital.vad_probability_attack",
|
||||||
|
"gain_controller2.adaptive_digital.level_estimator",
|
||||||
|
"gain_controller2.adaptive_digital.level_estimator_adjacent_speech_frames_threshold",
|
||||||
|
"gain_controller2.adaptive_digital.use_saturation_protector",
|
||||||
|
"gain_controller2.adaptive_digital.initial_saturation_margin_db",
|
||||||
|
"gain_controller2.adaptive_digital.extra_saturation_margin_db",
|
||||||
|
"gain_controller2.adaptive_digital.gain_applier_adjacent_speech_frames_threshold",
|
||||||
|
"gain_controller2.adaptive_digital.max_gain_change_db_per_second",
|
||||||
|
"gain_controller2.adaptive_digital.max_output_noise_level_dbfs",
|
||||||
|
|
||||||
|
"residual_echo_detector.enabled",
|
||||||
|
"level_estimation.enabled"
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export interface InputProcessorConfigRNNoise {
|
||||||
|
"rnnoise.enabled": boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attention:
|
||||||
|
* These keys **MUST** be equal to all keys of `InputProcessorConfigWebRTC`.
|
||||||
|
* All keys not registered in here will not be consideration.
|
||||||
|
*/
|
||||||
|
export const kInputProcessorConfigRNNoiseKeys: (keyof InputProcessorConfigRNNoise)[] = [
|
||||||
|
"rnnoise.enabled"
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface InputProcessorConfigMapping {
|
||||||
|
"webrtc-processing": InputProcessorConfigWebRTC,
|
||||||
|
"rnnoise": InputProcessorConfigRNNoise
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputProcessorType = keyof InputProcessorConfigMapping;
|
||||||
|
|
||||||
|
export interface InputProcessorStatistics {
|
||||||
|
/* WebRTC processor statistics */
|
||||||
|
output_rms_dbfs: number | undefined,
|
||||||
|
voice_detected: number | undefined,
|
||||||
|
echo_return_loss: number | undefined,
|
||||||
|
echo_return_loss_enhancement: number | undefined,
|
||||||
|
divergent_filter_fraction: number | undefined,
|
||||||
|
delay_median_ms: number | undefined,
|
||||||
|
delay_standard_deviation_ms: number | undefined,
|
||||||
|
residual_echo_likelihood: number | undefined,
|
||||||
|
residual_echo_likelihood_recent_max: number | undefined,
|
||||||
|
delay_ms: number | undefined,
|
||||||
|
|
||||||
|
/* RNNoise processor statistics */
|
||||||
|
rnnoise_volume: number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputProcessor {
|
||||||
|
/**
|
||||||
|
* @param processor Target processor type
|
||||||
|
* @returns `true` if the target processor type is supported and available
|
||||||
|
*/
|
||||||
|
hasProcessor(processor: InputProcessorType): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the processor config of the target type.
|
||||||
|
* This method will throw when the target processor isn't supported.
|
||||||
|
* @param processor Target processor type.
|
||||||
|
* @returns The processor config.
|
||||||
|
*/
|
||||||
|
getProcessorConfig<T extends InputProcessorType>(processor: T) : InputProcessorConfigMapping[T];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the target config.
|
||||||
|
* @param processor
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
applyProcessorConfig<T extends InputProcessorType>(processor: T, config: InputProcessorConfigMapping[T]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current processor statistics.
|
||||||
|
*/
|
||||||
|
getStatistics() : InputProcessorStatistics;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AbstractInput {
|
export interface AbstractInput {
|
||||||
readonly events: Registry<InputEvents>;
|
readonly events: Registry<InputEvents>;
|
||||||
|
|
||||||
|
@ -116,6 +306,15 @@ export interface AbstractInput {
|
||||||
|
|
||||||
getVolume() : number;
|
getVolume() : number;
|
||||||
setVolume(volume: number);
|
setVolume(volume: number);
|
||||||
|
|
||||||
|
getInputProcessor() : InputProcessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new level meter for this audio input.
|
||||||
|
* This level meter will be indicate the audio level after all processing.
|
||||||
|
* Note: Changing the input device or stopping the input will result in no activity.
|
||||||
|
*/
|
||||||
|
createLevelMeter() : LevelMeter;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LevelMeter {
|
export interface LevelMeter {
|
||||||
|
|
|
@ -49,6 +49,12 @@ export function setDefaultRecorder(recorder: RecorderProfile) {
|
||||||
|
|
||||||
export interface RecorderProfileEvents {
|
export interface RecorderProfileEvents {
|
||||||
notify_device_changed: { },
|
notify_device_changed: { },
|
||||||
|
|
||||||
|
notify_voice_start: { },
|
||||||
|
notify_voice_end: { },
|
||||||
|
|
||||||
|
/* attention: this notify will only be called when the audio input hasn't been initialized! */
|
||||||
|
notify_input_initialized: { },
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecorderProfile {
|
export class RecorderProfile {
|
||||||
|
@ -84,8 +90,9 @@ export class RecorderProfile {
|
||||||
|
|
||||||
this.pptHook = {
|
this.pptHook = {
|
||||||
callbackRelease: () => {
|
callbackRelease: () => {
|
||||||
if(this.pptTimeout)
|
if(this.pptTimeout) {
|
||||||
clearTimeout(this.pptTimeout);
|
clearTimeout(this.pptTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
this.pptTimeout = setTimeout(() => {
|
this.pptTimeout = setTimeout(() => {
|
||||||
this.registeredFilter["ppt-gate"]?.setState(true);
|
this.registeredFilter["ppt-gate"]?.setState(true);
|
||||||
|
@ -93,8 +100,9 @@ export class RecorderProfile {
|
||||||
},
|
},
|
||||||
|
|
||||||
callbackPress: () => {
|
callbackPress: () => {
|
||||||
if(this.pptTimeout)
|
if(this.pptTimeout) {
|
||||||
clearTimeout(this.pptTimeout);
|
clearTimeout(this.pptTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
this.registeredFilter["ppt-gate"]?.setState(false);
|
this.registeredFilter["ppt-gate"]?.setState(false);
|
||||||
},
|
},
|
||||||
|
@ -154,14 +162,18 @@ export class RecorderProfile {
|
||||||
|
|
||||||
this.input.events.on("notify_voice_start", () => {
|
this.input.events.on("notify_voice_start", () => {
|
||||||
logDebug(LogCategory.VOICE, "Voice start");
|
logDebug(LogCategory.VOICE, "Voice start");
|
||||||
if(this.callback_start)
|
if(this.callback_start) {
|
||||||
this.callback_start();
|
this.callback_start();
|
||||||
|
}
|
||||||
|
this.events.fire("notify_voice_start");
|
||||||
});
|
});
|
||||||
|
|
||||||
this.input.events.on("notify_voice_end", () => {
|
this.input.events.on("notify_voice_end", () => {
|
||||||
logDebug(LogCategory.VOICE, "Voice end");
|
logDebug(LogCategory.VOICE, "Voice end");
|
||||||
if(this.callback_stop)
|
if(this.callback_stop) {
|
||||||
this.callback_stop();
|
this.callback_stop();
|
||||||
|
}
|
||||||
|
this.events.fire("notify_voice_end");
|
||||||
});
|
});
|
||||||
|
|
||||||
this.input.setFilterMode(FilterMode.Block);
|
this.input.setFilterMode(FilterMode.Block);
|
||||||
|
@ -174,6 +186,7 @@ export class RecorderProfile {
|
||||||
if(this.callback_input_initialized) {
|
if(this.callback_input_initialized) {
|
||||||
this.callback_input_initialized(this.input);
|
this.callback_input_initialized(this.input);
|
||||||
}
|
}
|
||||||
|
this.events.fire("notify_input_initialized");
|
||||||
|
|
||||||
|
|
||||||
/* apply initial config values */
|
/* apply initial config values */
|
||||||
|
|
|
@ -8,10 +8,10 @@ import {
|
||||||
import {assertMainApplication} from "tc-shared/ui/utils";
|
import {assertMainApplication} from "tc-shared/ui/utils";
|
||||||
import {Registry} from "tc-events";
|
import {Registry} from "tc-events";
|
||||||
import {getIpcInstance} from "tc-shared/ipc/BrowserIPC";
|
import {getIpcInstance} from "tc-shared/ipc/BrowserIPC";
|
||||||
import _ from "lodash";
|
|
||||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||||
import {tr, tra} from "tc-shared/i18n/localize";
|
import {tr, tra} from "tc-shared/i18n/localize";
|
||||||
import {guid} from "tc-shared/crypto/uid";
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
assertMainApplication();
|
assertMainApplication();
|
||||||
|
|
||||||
|
@ -27,17 +27,29 @@ type WindowHandle = {
|
||||||
|
|
||||||
export class WebWindowManager implements WindowManager {
|
export class WebWindowManager implements WindowManager {
|
||||||
private readonly events: Registry<WindowManagerEvents>;
|
private readonly events: Registry<WindowManagerEvents>;
|
||||||
|
private readonly listenerUnload;
|
||||||
private registeredWindows: { [key: string]: WindowHandle } = {};
|
private registeredWindows: { [key: string]: WindowHandle } = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.events = new Registry<WindowManagerEvents>();
|
this.events = new Registry<WindowManagerEvents>();
|
||||||
/* TODO: Close all active windows on page unload */
|
|
||||||
|
this.listenerUnload = () => this.destroyAllWindows();
|
||||||
|
window.addEventListener("unload", this.listenerUnload);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEvents(): Registry<WindowManagerEvents> {
|
getEvents(): Registry<WindowManagerEvents> {
|
||||||
return this.events;
|
return this.events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
window.removeEventListener("unload", this.listenerUnload);
|
||||||
|
this.destroyAllWindows();
|
||||||
|
}
|
||||||
|
|
||||||
|
private destroyAllWindows() {
|
||||||
|
Object.values(this.registeredWindows).forEach(window => window.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
async createWindow(options: WindowSpawnOptions): Promise<WindowCreateResult> {
|
async createWindow(options: WindowSpawnOptions): Promise<WindowCreateResult> {
|
||||||
/* Multiple application instance may want to open the same windows */
|
/* Multiple application instance may want to open the same windows */
|
||||||
const windowUniqueId = getIpcInstance().getApplicationChannelId() + "-" + options.uniqueId;
|
const windowUniqueId = getIpcInstance().getApplicationChannelId() + "-" + options.uniqueId;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
FilterMode,
|
FilterMode,
|
||||||
InputConsumer,
|
InputConsumer,
|
||||||
InputConsumerType,
|
InputConsumerType,
|
||||||
InputEvents,
|
InputEvents, InputProcessor, InputProcessorConfigMapping, InputProcessorStatistics, InputProcessorType,
|
||||||
InputStartError,
|
InputStartError,
|
||||||
InputState,
|
InputState,
|
||||||
LevelMeter,
|
LevelMeter,
|
||||||
|
@ -43,12 +43,6 @@ export class WebAudioRecorder implements AudioRecorderBacked {
|
||||||
getDeviceList(): DeviceList {
|
getDeviceList(): DeviceList {
|
||||||
return inputDeviceList;
|
return inputDeviceList;
|
||||||
}
|
}
|
||||||
|
|
||||||
isRnNoiseSupported() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleRnNoise(target: boolean) { throw "not supported"; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class JavascriptInput implements AbstractInput {
|
class JavascriptInput implements AbstractInput {
|
||||||
|
@ -454,8 +448,10 @@ class JavascriptInput implements AbstractInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
setVolume(volume: number) {
|
setVolume(volume: number) {
|
||||||
if(volume === this.volumeModifier)
|
if(volume === this.volumeModifier) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.volumeModifier = volume;
|
this.volumeModifier = volume;
|
||||||
this.audioNodeVolume.gain.value = volume;
|
this.audioNodeVolume.gain.value = volume;
|
||||||
}
|
}
|
||||||
|
@ -482,6 +478,32 @@ class JavascriptInput implements AbstractInput {
|
||||||
newMode: mode
|
newMode: mode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInputProcessor(): InputProcessor {
|
||||||
|
return new JavaScriptInputProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
createLevelMeter(): LevelMeter {
|
||||||
|
throw tr("implement me!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JavaScriptInputProcessor implements InputProcessor {
|
||||||
|
applyProcessorConfig<T extends InputProcessorType>(processor: T, config: InputProcessorConfigMapping[T]) {
|
||||||
|
throw tr("target processor is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
getProcessorConfig<T extends InputProcessorType>(processor: T): InputProcessorConfigMapping[T] {
|
||||||
|
throw tr("target processor is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatistics(): InputProcessorStatistics {
|
||||||
|
return {} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasProcessor(processor: InputProcessorType): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JavascriptLevelMeter implements LevelMeter {
|
class JavascriptLevelMeter implements LevelMeter {
|
||||||
|
|
|
@ -370,7 +370,9 @@ export const config = async (env: any, target: "web" | "client"): Promise<Config
|
||||||
hotOnly: false,
|
hotOnly: false,
|
||||||
|
|
||||||
liveReload: false,
|
liveReload: false,
|
||||||
inline: false
|
inline: false,
|
||||||
|
|
||||||
|
https: process.env["serve_https"] === "1"
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
Loading…
Add table
Reference in a new issue