Adding an UI for the new client audio processing unit
parent
1878c92a57
commit
1dfa10b09b
|
@ -1,4 +1,9 @@
|
|||
# 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**
|
||||
- Allowing to directly select 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-settings.scss"
|
||||
import "./static/overlay-image-preview.scss"
|
||||
import "./static/color-variables.scss"
|
||||
|
||||
import "./static/ts/tab.scss"
|
||||
import "./static/ts/country.scss"
|
|
@ -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 {ConnectionProfile} from "./profiles/ConnectionProfile";
|
||||
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 {HandshakeHandler} from "./connection/HandshakeHandler";
|
||||
import * as htmltags from "./ui/htmltags";
|
||||
import {FilterMode, InputStartError, InputState} from "./voice/RecorderBase";
|
||||
import {CommandResult} from "./connection/ServerConnectionDeclaration";
|
||||
import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile";
|
||||
import {Regex} from "./ui/modal/ModalConnect";
|
||||
import {formatMessage} from "./ui/frames/chat";
|
||||
import {spawnAvatarUpload} from "./ui/modal/ModalAvatar";
|
||||
import {EventHandler, Registry} from "./events";
|
||||
import {FileManager} from "./file/FileManager";
|
||||
import {FileTransferState, TransferProvider} from "./file/Transfer";
|
||||
import {tr, traj} from "./i18n/localize";
|
||||
import {md5} from "./crypto/md5";
|
||||
import {tr} from "./i18n/localize";
|
||||
import {guid} from "./crypto/uid";
|
||||
import {PluginCmdRegistry} from "./connection/PluginCmdHandler";
|
||||
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 {Registry} from "../events";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
|
||||
export interface AudioRecorderBacked {
|
||||
createInput() : AbstractInput;
|
||||
createLevelMeter(device: InputDevice) : Promise<LevelMeter>;
|
||||
|
||||
getDeviceList() : DeviceList;
|
||||
|
||||
isRnNoiseSupported() : boolean;
|
||||
toggleRnNoise(target: boolean);
|
||||
}
|
||||
|
||||
export interface DeviceListEvents {
|
||||
|
@ -165,16 +159,4 @@ export function setRecorderBackend(instance: AudioRecorderBacked) {
|
|||
}
|
||||
|
||||
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/mixin";
|
||||
|
||||
html:root {
|
||||
--hostbanner-background: #2e2e2e;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
@ -14,6 +10,7 @@ html:root {
|
|||
justify-content: stretch;
|
||||
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
|
||||
.withBackground {
|
||||
background-color: var(--hostbanner-background);
|
||||
|
|
|
@ -3,10 +3,11 @@ import {tra} from "tc-shared/i18n/localize";
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {modal_settings, SettingProfileEvents} from "tc-shared/ui/modal/ModalSettings";
|
||||
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 * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import {MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/MicrophoneDefinitions";
|
||||
|
||||
export interface EventModalNewcomer {
|
||||
"show_step": {
|
||||
|
|
|
@ -23,9 +23,10 @@ import {KeyMapSettings} from "tc-shared/ui/modal/settings/Keymap";
|
|||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
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 {getBackend} from "tc-shared/backend";
|
||||
import {MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/MicrophoneDefinitions";
|
||||
|
||||
type ProfileInfoEvent = {
|
||||
id: string,
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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/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 {
|
||||
@include user-select(none);
|
||||
|
||||
|
|
|
@ -260,7 +260,7 @@
|
|||
|
||||
padding-bottom: .5em;
|
||||
|
||||
a {
|
||||
.text {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
|
@ -284,6 +284,20 @@
|
|||
margin-left: 1em;
|
||||
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 {
|
||||
|
|
|
@ -1,92 +1,21 @@
|
|||
import * as React from "react";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {LevelMeter} from "tc-shared/voice/RecorderBase";
|
||||
import {LogCategory, logTrace, logWarn} from "tc-shared/log";
|
||||
import {AbstractInput, FilterMode, LevelMeter} from "tc-shared/voice/RecorderBase";
|
||||
import {LogCategory, logError, logTrace, logWarn} from "tc-shared/log";
|
||||
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 {getBackend} from "tc-shared/backend";
|
||||
import * as _ from "lodash";
|
||||
import {getAudioBackend} from "tc-shared/audio/Player";
|
||||
|
||||
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 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: {}
|
||||
}
|
||||
import {
|
||||
InputDeviceLevel,
|
||||
MicrophoneSettingsEvents,
|
||||
SelectedMicrophone
|
||||
} from "tc-shared/ui/modal/settings/MicrophoneDefinitions";
|
||||
import {spawnInputProcessorModal} from "tc-shared/ui/modal/input-processor/Controller";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
|
||||
export function initialize_audio_microphone_controller(events: Registry<MicrophoneSettingsEvents>) {
|
||||
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 */
|
||||
{
|
||||
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.fire("query_devices");
|
||||
}));
|
||||
|
@ -455,45 +435,28 @@ export function initialize_audio_microphone_controller(events: Registry<Micropho
|
|||
if(!getAudioBackend().isInitialized()) {
|
||||
getAudioBackend().executeWhenInitialized(() => events.fire_react("query_devices"));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
|
||||
|
||||
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";
|
||||
}
|
||||
/* TODO: Only do this on user request? */
|
||||
{
|
||||
const ownDefaultRecorder = () => {
|
||||
const originalHandlerId = defaultRecorder.current_handler?.handlerId;
|
||||
defaultRecorder.unmount().then(() => {
|
||||
defaultRecorder.input.start().catch(error => {
|
||||
logError(LogCategory.AUDIO, tr("Failed to start default input: %o"), error);
|
||||
});
|
||||
});
|
||||
|
||||
modal.show();
|
||||
});
|
||||
},
|
||||
priority: -2
|
||||
});
|
||||
*/
|
||||
events.on("notify_destroy", () => {
|
||||
server_connections.findConnection(originalHandlerId)?.acquireInputHardware().catch(error => {
|
||||
logError(LogCategory.GENERAL, tr("Failed to acquire microphone after settings detach: %o"), error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if(defaultRecorder.input) {
|
||||
ownDefaultRecorder();
|
||||
} else {
|
||||
events.on("notify_destroy", defaultRecorder.events.one("notify_input_initialized", () => ownDefaultRecorder()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {useEffect, useRef, useState} from "react";
|
||||
import {useContext, useEffect, useRef, useState} from "react";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
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 {ClientIcon} from "svg-sprites/client-icons";
|
||||
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 {HighlightContainer, HighlightRegion, HighlightText} from "./Heighlight";
|
||||
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 EventContext = React.createContext<Registry<MicrophoneSettingsEvents>>(undefined);
|
||||
|
||||
type MicrophoneSelectedState = "selected" | "applying" | "unselected";
|
||||
const MicrophoneStatus = (props: { state: MicrophoneSelectedState }) => {
|
||||
|
@ -34,89 +38,134 @@ const MicrophoneStatus = (props: { state: MicrophoneSelectedState }) => {
|
|||
return null;
|
||||
|
||||
case "selected":
|
||||
return <ClientIconRenderer key={"selected"} icon={ClientIcon.Apply}/>;
|
||||
return (
|
||||
<ClientIconRenderer key={"selected"} icon={ClientIcon.Apply} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type ActivityBarStatus =
|
||||
{ mode: "success" }
|
||||
{ mode: "success", level: number }
|
||||
| { mode: "error", message: string }
|
||||
| { mode: "loading" }
|
||||
| { mode: "uninitialized" };
|
||||
const ActivityBar = (props: { events: Registry<MicrophoneSettingsEvents>, deviceId: string | "none", disabled?: boolean }) => {
|
||||
const refHider = useRef<HTMLDivElement>();
|
||||
const [status, setStatus] = useState<ActivityBarStatus>({ mode: "loading" });
|
||||
|
||||
if(typeof props.deviceId === "undefined") {
|
||||
throw "invalid device id";
|
||||
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 updateState = (newState: ActivityBarStatus) => {
|
||||
if(!_.isEqual(newState, status)) {
|
||||
setStatus(newState);
|
||||
}
|
||||
}
|
||||
|
||||
props.events.reactUse("notify_device_level", event => {
|
||||
refHider.current.style.width = "100%";
|
||||
if (event.status === "uninitialized") {
|
||||
if (status.mode === "uninitialized") {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus({mode: "uninitialized"});
|
||||
} else if (event.status === "no-permissions") {
|
||||
const noPermissionsMessage = tr("no permissions");
|
||||
if (status.mode === "error" && status.message === noPermissionsMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus({mode: "error", message: noPermissionsMessage});
|
||||
} else {
|
||||
events.reactUse("notify_device_level", event => {
|
||||
if(event.status === "uninitialized") {
|
||||
updateState({ mode: "uninitialized" });
|
||||
} else if(event.status === "no-permissions") {
|
||||
updateState({ mode: "error", message: tr("no permissions") });
|
||||
} else if(event.status === "healthy") {
|
||||
const device = event.level[props.deviceId];
|
||||
if (!device) {
|
||||
if (status.mode === "loading") {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus({mode: "loading"});
|
||||
} else if (device.status === "success") {
|
||||
if (status.mode !== "success") {
|
||||
setStatus({mode: "success"});
|
||||
}
|
||||
|
||||
refHider.current.style.width = (100 - device.level) + "%";
|
||||
updateState({ mode: "loading" });
|
||||
} else if(device.status === "success") {
|
||||
updateState({ mode: "success", level: device.level });
|
||||
} else {
|
||||
if (status.mode === "error" && status.message === device.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus({mode: "error", message: device.error + ""});
|
||||
updateState({ 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) {
|
||||
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;
|
||||
|
||||
case "loading":
|
||||
error =
|
||||
<div className={cssStyle.text} key={"loading"}><Translatable>Loading</Translatable> <LoadingDots/>
|
||||
</div>;
|
||||
error = (
|
||||
<div className={cssStyle.text} key={"loading"}>
|
||||
<Translatable>Loading</Translatable> <LoadingDots/>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
case "success":
|
||||
error = undefined;
|
||||
hiderWidth = (100 - status.level) + "%";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
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}
|
||||
</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 (
|
||||
<div className={cssStyle.device + " " + (props.state === "unselected" ? "" : cssStyle.selected)}
|
||||
onClick={props.onClick}>
|
||||
|
@ -128,13 +177,11 @@ const Microphone = (props: { events: Registry<MicrophoneSettingsEvents>, device:
|
|||
<div className={cssStyle.name}>{props.device.name}</div>
|
||||
</div>
|
||||
<div className={cssStyle.containerActivity}>
|
||||
{props.device.id === InputDevice.NoDeviceId ? undefined :
|
||||
<ActivityBar key={"a"} events={props.events} deviceId={props.device.id}/>
|
||||
}
|
||||
{activityBar}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
type MicrophoneListState = {
|
||||
type: "normal" | "loading" | "audio-not-initialized"
|
||||
|
@ -284,7 +331,6 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
|
|||
name: tr("No device"),
|
||||
default: false
|
||||
}}
|
||||
events={props.events}
|
||||
state={deviceSelectState("none")}
|
||||
onClick={() => {
|
||||
if (state.type !== "normal" || selectedDevice?.selectingDevice) {
|
||||
|
@ -295,23 +341,24 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
|
|||
}}
|
||||
/>
|
||||
|
||||
{deviceList.map(device => <Microphone
|
||||
key={"d-" + device.id}
|
||||
device={device}
|
||||
events={props.events}
|
||||
state={deviceSelectState(device)}
|
||||
onClick={() => {
|
||||
if (state.type !== "normal" || selectedDevice?.selectingDevice) {
|
||||
return;
|
||||
}
|
||||
{deviceList.map(device => (
|
||||
<Microphone
|
||||
key={"d-" + device.id}
|
||||
device={device}
|
||||
state={deviceSelectState(device)}
|
||||
onClick={() => {
|
||||
if (state.type !== "normal" || selectedDevice?.selectingDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(device.default) {
|
||||
props.events.fire("action_set_selected_device", { target: { type: "default" } });
|
||||
} else {
|
||||
props.events.fire("action_set_selected_device", { target: { type: "device", deviceId: device.id } });
|
||||
}
|
||||
}}
|
||||
/>)}
|
||||
if(device.default) {
|
||||
props.events.fire("action_set_selected_device", { target: { type: "default" } });
|
||||
} else {
|
||||
props.events.fire("action_set_selected_device", { target: { type: "device", deviceId: device.id } });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</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>(() => {
|
||||
props.events.fire("query_setting", {setting: "ppt-release-delay"});
|
||||
events.fire("query_setting", {setting: "ppt-release-delay"});
|
||||
return "loading";
|
||||
});
|
||||
|
||||
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";
|
||||
});
|
||||
|
||||
const [isActive, setActive] = useState(false);
|
||||
|
||||
props.events.reactUse("notify_setting", event => {
|
||||
if (event.setting === "vad-type")
|
||||
events.reactUse("notify_setting", event => {
|
||||
if (event.setting === "vad-type") {
|
||||
setActive(event.value === "push_to_talk");
|
||||
else if (event.setting === "ppt-release-delay")
|
||||
} else if (event.setting === "ppt-release-delay") {
|
||||
setDelay(event.value);
|
||||
else if (event.setting === "ppt-release-delay-active")
|
||||
} else if (event.setting === "ppt-release-delay-active") {
|
||||
setDelayActive(event.value);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cssStyle.containerPptDelay}>
|
||||
<Checkbox
|
||||
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}
|
||||
value={delayActive === true}
|
||||
|
@ -458,45 +508,48 @@ const PPTDelaySettings = (props: { events: Registry<MicrophoneSettingsEvents> })
|
|||
|
||||
|
||||
const newValue = event.target.valueAsNumber;
|
||||
if (isNaN(newValue))
|
||||
if (isNaN(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue < 0 || newValue > 4000)
|
||||
if (newValue < 0 || newValue > 4000) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const RNNoiseLabel = () => (
|
||||
<VariadicTranslatable text={"Enable RNNoise cancelation ({})"}>
|
||||
const RNNoiseLabel = React.memo(() => (
|
||||
<VariadicTranslatable text={"Enable RNNoise cancellation ({})"}>
|
||||
<a href={"https://jmvalin.ca/demo/rnnoise/"} target={"_blank"} style={{ margin: 0 }}><Translatable>more info</Translatable></a>
|
||||
</VariadicTranslatable>
|
||||
)
|
||||
));
|
||||
|
||||
const RNNoiseSettings = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||
const RNNoiseSettings = React.memo(() => {
|
||||
if(__build.target === "web") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const events = useContext(EventContext);
|
||||
const [ enabled, setEnabled ] = useState<boolean | "loading">(() => {
|
||||
props.events.fire("query_setting", { setting: "rnnoise" });
|
||||
events.fire("query_setting", { setting: "rnnoise" });
|
||||
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 (
|
||||
<Checkbox label={<RNNoiseLabel />}
|
||||
disabled={enabled === "loading"}
|
||||
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 [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 [value, setValue] = useState<"loading" | number>(() => {
|
||||
props.events.fire("query_setting", {setting: "threshold-threshold"});
|
||||
events.fire("query_setting", {setting: "threshold-threshold"});
|
||||
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") {
|
||||
refSlider.current?.setState({value: 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") {
|
||||
const defaultDevice = event.devices.find(device => device.default);
|
||||
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";
|
||||
return (
|
||||
<div className={cssStyle.containerSensitivity}>
|
||||
<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>
|
||||
<Slider
|
||||
ref={refSlider}
|
||||
|
@ -635,12 +691,12 @@ const ThresholdSelector = (props: { events: Registry<MicrophoneSettingsEvents> }
|
|||
disabled={value === "loading" || !isActive}
|
||||
|
||||
onChange={value => {
|
||||
props.events.fire("action_set_setting", {setting: "threshold-threshold", value: value})
|
||||
events.fire("action_set_setting", {setting: "threshold-threshold", value: value})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
const HelpText0 = () => (
|
||||
<HighlightText highlightId={"hs-0"} className={cssStyle.help}>
|
||||
|
@ -700,6 +756,24 @@ const HelpText2 = () => (
|
|||
</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> }) => {
|
||||
const [highlighted, setHighlighted] = useState(() => {
|
||||
props.events.fire("query_help");
|
||||
|
@ -709,44 +783,63 @@ export const MicrophoneSettings = (props: { events: Registry<MicrophoneSettingsE
|
|||
props.events.reactUse("notify_highlight", event => setHighlighted(event.field));
|
||||
|
||||
return (
|
||||
<HighlightContainer highlightedId={highlighted} onClick={() => props.events.fire("action_help_click")}
|
||||
classList={cssStyle.highlightContainer}>
|
||||
<div className={cssStyle.container}>
|
||||
<HelpText0/>
|
||||
<HighlightRegion className={cssStyle.left} highlightId={"hs-1"}>
|
||||
<div className={cssStyle.header}>
|
||||
<a><Translatable>Select your Microphone Device</Translatable></a>
|
||||
<ListRefreshButton events={props.events}/>
|
||||
</div>
|
||||
<MicrophoneList events={props.events}/>
|
||||
<HelpText2/>
|
||||
</HighlightRegion>
|
||||
<HighlightRegion className={cssStyle.right} highlightId={"hs-2"}>
|
||||
<HelpText1/>
|
||||
<div className={cssStyle.header}>
|
||||
<a><Translatable>Microphone Settings</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.body}>
|
||||
<VolumeSettings events={props.events}/>
|
||||
<VadSelector events={props.events}/>
|
||||
</div>
|
||||
<div className={cssStyle.header}>
|
||||
<a><Translatable>Sensitivity Settings</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.body}>
|
||||
<ThresholdSelector events={props.events}/>
|
||||
</div>
|
||||
<div className={cssStyle.header}>
|
||||
<a><Translatable>Advanced Settings</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.body}>
|
||||
<div className={cssStyle.containerAdvanced}>
|
||||
<PPTDelaySettings events={props.events}/>
|
||||
<RNNoiseSettings events={props.events} />
|
||||
<EventContext.Provider value={props.events}>
|
||||
<HighlightContainer
|
||||
highlightedId={highlighted}
|
||||
onClick={() => props.events.fire("action_help_click")}
|
||||
classList={cssStyle.highlightContainer}
|
||||
>
|
||||
<div className={cssStyle.container}>
|
||||
<HelpText0/>
|
||||
<HighlightRegion className={cssStyle.left} highlightId={"hs-1"}>
|
||||
<div className={cssStyle.header}>
|
||||
<div className={cssStyle.text}>
|
||||
<Translatable>Select your Microphone Device</Translatable>
|
||||
</div>
|
||||
<ListRefreshButton events={props.events}/>
|
||||
</div>
|
||||
</div>
|
||||
</HighlightRegion>
|
||||
</div>
|
||||
</HighlightContainer>
|
||||
<MicrophoneList events={props.events}/>
|
||||
<HelpText2/>
|
||||
</HighlightRegion>
|
||||
<HighlightRegion className={cssStyle.right} highlightId={"hs-2"}>
|
||||
<HelpText1/>
|
||||
<div className={cssStyle.header}>
|
||||
<div className={cssStyle.text}>
|
||||
<Translatable>Microphone Settings</Translatable>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.body}>
|
||||
<VolumeSettings events={props.events}/>
|
||||
<VadSelector events={props.events}/>
|
||||
</div>
|
||||
<div className={cssStyle.header}>
|
||||
<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 className={cssStyle.body}>
|
||||
<ThresholdSelector />
|
||||
</div>
|
||||
<div className={cssStyle.header}>
|
||||
<div className={cssStyle.text}>
|
||||
<Translatable>Advanced Settings</Translatable>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.body}>
|
||||
<div className={cssStyle.containerAdvanced}>
|
||||
<PPTDelaySettings />
|
||||
<RNNoiseSettings />
|
||||
<InputProcessorButton />
|
||||
</div>
|
||||
</div>
|
||||
</HighlightRegion>
|
||||
</div>
|
||||
</HighlightContainer>
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
|
@ -98,6 +98,7 @@ html:root {
|
|||
flex-shrink: 1;
|
||||
|
||||
background: transparent;
|
||||
align-self: center;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
|
|
|
@ -64,7 +64,7 @@ export const ControlledBoxedInputField = (props: {
|
|||
|
||||
ref={props.refInput}
|
||||
|
||||
value={props.value || ""}
|
||||
value={typeof props.value !== "undefined" ? props.value : ""}
|
||||
placeholder={props.placeholder}
|
||||
|
||||
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 {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer";
|
||||
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 ModalRenderType = "page" | "dialog";
|
||||
|
@ -210,5 +211,9 @@ export interface ModalConstructorArguments {
|
|||
/* events */ IpcRegistryDescription<ModalAvatarUploadEvents>,
|
||||
/* variables */ IpcVariableDescriptor<ModalAvatarUploadVariables>,
|
||||
/* serverUniqueId */ string
|
||||
],
|
||||
"modal-input-processor": [
|
||||
/* events */ IpcRegistryDescription<ModalInputProcessorEvents>,
|
||||
/* variables */ IpcVariableDescriptor<ModalInputProcessorVariables>,
|
||||
]
|
||||
}
|
|
@ -109,4 +109,8 @@ registerModal({
|
|||
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 {Stage} from "tc-loader";
|
||||
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 {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
||||
import {ModalWindowControllerInstance} from "./Controller";
|
||||
|
@ -26,8 +26,16 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|||
await import("tc-shared/proto");
|
||||
await initializeI18N();
|
||||
setupIpcHandler();
|
||||
|
||||
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/color-variables";
|
||||
|
||||
/* FIXME: Remove this wired import */
|
||||
@import "../../../../../../css/static/general";
|
||||
|
|
|
@ -404,7 +404,7 @@ export abstract class UiVariableConsumer<Variables extends UiVariableMap> {
|
|||
}, [ variable, customData ]);
|
||||
|
||||
if(arguments.length >= 3) {
|
||||
return cacheEntry.status === "loaded" ? cacheEntry.currentValue : defaultValue;
|
||||
return cacheEntry.status === "loaded" || cacheEntry.status === "applying" ? cacheEntry.currentValue : defaultValue;
|
||||
} else {
|
||||
return {
|
||||
status: cacheEntry.status,
|
||||
|
|
|
@ -78,6 +78,196 @@ export enum FilterMode {
|
|||
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 {
|
||||
readonly events: Registry<InputEvents>;
|
||||
|
||||
|
@ -116,6 +306,15 @@ export interface AbstractInput {
|
|||
|
||||
getVolume() : 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 {
|
||||
|
|
|
@ -49,6 +49,12 @@ export function setDefaultRecorder(recorder: RecorderProfile) {
|
|||
|
||||
export interface RecorderProfileEvents {
|
||||
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 {
|
||||
|
@ -84,8 +90,9 @@ export class RecorderProfile {
|
|||
|
||||
this.pptHook = {
|
||||
callbackRelease: () => {
|
||||
if(this.pptTimeout)
|
||||
if(this.pptTimeout) {
|
||||
clearTimeout(this.pptTimeout);
|
||||
}
|
||||
|
||||
this.pptTimeout = setTimeout(() => {
|
||||
this.registeredFilter["ppt-gate"]?.setState(true);
|
||||
|
@ -93,8 +100,9 @@ export class RecorderProfile {
|
|||
},
|
||||
|
||||
callbackPress: () => {
|
||||
if(this.pptTimeout)
|
||||
if(this.pptTimeout) {
|
||||
clearTimeout(this.pptTimeout);
|
||||
}
|
||||
|
||||
this.registeredFilter["ppt-gate"]?.setState(false);
|
||||
},
|
||||
|
@ -154,14 +162,18 @@ export class RecorderProfile {
|
|||
|
||||
this.input.events.on("notify_voice_start", () => {
|
||||
logDebug(LogCategory.VOICE, "Voice start");
|
||||
if(this.callback_start)
|
||||
if(this.callback_start) {
|
||||
this.callback_start();
|
||||
}
|
||||
this.events.fire("notify_voice_start");
|
||||
});
|
||||
|
||||
this.input.events.on("notify_voice_end", () => {
|
||||
logDebug(LogCategory.VOICE, "Voice end");
|
||||
if(this.callback_stop)
|
||||
if(this.callback_stop) {
|
||||
this.callback_stop();
|
||||
}
|
||||
this.events.fire("notify_voice_end");
|
||||
});
|
||||
|
||||
this.input.setFilterMode(FilterMode.Block);
|
||||
|
@ -174,6 +186,7 @@ export class RecorderProfile {
|
|||
if(this.callback_input_initialized) {
|
||||
this.callback_input_initialized(this.input);
|
||||
}
|
||||
this.events.fire("notify_input_initialized");
|
||||
|
||||
|
||||
/* apply initial config values */
|
||||
|
|
|
@ -8,10 +8,10 @@ import {
|
|||
import {assertMainApplication} from "tc-shared/ui/utils";
|
||||
import {Registry} from "tc-events";
|
||||
import {getIpcInstance} from "tc-shared/ipc/BrowserIPC";
|
||||
import _ from "lodash";
|
||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
import {tr, tra} from "tc-shared/i18n/localize";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import _ from "lodash";
|
||||
|
||||
assertMainApplication();
|
||||
|
||||
|
@ -27,17 +27,29 @@ type WindowHandle = {
|
|||
|
||||
export class WebWindowManager implements WindowManager {
|
||||
private readonly events: Registry<WindowManagerEvents>;
|
||||
private readonly listenerUnload;
|
||||
private registeredWindows: { [key: string]: WindowHandle } = {};
|
||||
|
||||
constructor() {
|
||||
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> {
|
||||
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> {
|
||||
/* Multiple application instance may want to open the same windows */
|
||||
const windowUniqueId = getIpcInstance().getApplicationChannelId() + "-" + options.uniqueId;
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
FilterMode,
|
||||
InputConsumer,
|
||||
InputConsumerType,
|
||||
InputEvents,
|
||||
InputEvents, InputProcessor, InputProcessorConfigMapping, InputProcessorStatistics, InputProcessorType,
|
||||
InputStartError,
|
||||
InputState,
|
||||
LevelMeter,
|
||||
|
@ -43,12 +43,6 @@ export class WebAudioRecorder implements AudioRecorderBacked {
|
|||
getDeviceList(): DeviceList {
|
||||
return inputDeviceList;
|
||||
}
|
||||
|
||||
isRnNoiseSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
toggleRnNoise(target: boolean) { throw "not supported"; }
|
||||
}
|
||||
|
||||
class JavascriptInput implements AbstractInput {
|
||||
|
@ -454,8 +448,10 @@ class JavascriptInput implements AbstractInput {
|
|||
}
|
||||
|
||||
setVolume(volume: number) {
|
||||
if(volume === this.volumeModifier)
|
||||
if(volume === this.volumeModifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.volumeModifier = volume;
|
||||
this.audioNodeVolume.gain.value = volume;
|
||||
}
|
||||
|
@ -482,6 +478,32 @@ class JavascriptInput implements AbstractInput {
|
|||
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 {
|
||||
|
|
|
@ -370,7 +370,9 @@ export const config = async (env: any, target: "web" | "client"): Promise<Config
|
|||
hotOnly: false,
|
||||
|
||||
liveReload: false,
|
||||
inline: false
|
||||
inline: false,
|
||||
|
||||
https: process.env["serve_https"] === "1"
|
||||
},
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue