Adding an UI for the new client audio processing unit

master
WolverinDEV 2021-03-30 11:20:27 +02:00
parent 1878c92a57
commit 1dfa10b09b
29 changed files with 1631 additions and 354 deletions

View File

@ -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

View File

@ -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"

View 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;
}

View File

@ -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";

View File

@ -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 {
@ -166,15 +160,3 @@ 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));
}
}
})

View File

@ -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);

View File

@ -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": {

View File

@ -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,

View 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);
}

View 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,
}
}

View 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;
}
}

View 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;

View File

@ -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);

View File

@ -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 {

View File

@ -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"));
}
/* 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);
});
});
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()));
}
}
}
/*
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";
}
});
modal.show();
});
},
priority: -2
});
*/

View 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: {}
}

View File

@ -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>&nbsp;<LoadingDots/>
</div>;
error = (
<div className={cssStyle.text} key={"loading"}>
<Translatable>Loading</Translatable>&nbsp;<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,10 +341,10 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
}}
/>
{deviceList.map(device => <Microphone
{deviceList.map(device => (
<Microphone
key={"d-" + device.id}
device={device}
events={props.events}
state={deviceSelectState(device)}
onClick={() => {
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 } });
}
}}
/>)}
/>
))}
</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,13 +783,19 @@ 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}>
<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}>
<a><Translatable>Select your Microphone Device</Translatable></a>
<div className={cssStyle.text}>
<Translatable>Select your Microphone Device</Translatable>
</div>
<ListRefreshButton events={props.events}/>
</div>
<MicrophoneList events={props.events}/>
@ -724,29 +804,42 @@ export const MicrophoneSettings = (props: { events: Registry<MicrophoneSettingsE
<HighlightRegion className={cssStyle.right} highlightId={"hs-2"}>
<HelpText1/>
<div className={cssStyle.header}>
<a><Translatable>Microphone Settings</Translatable></a>
<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}>
<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 className={cssStyle.body}>
<ThresholdSelector events={props.events}/>
<ThresholdSelector />
</div>
<div className={cssStyle.header}>
<a><Translatable>Advanced Settings</Translatable></a>
<div className={cssStyle.text}>
<Translatable>Advanced Settings</Translatable>
</div>
</div>
<div className={cssStyle.body}>
<div className={cssStyle.containerAdvanced}>
<PPTDelaySettings events={props.events}/>
<RNNoiseSettings events={props.events} />
<PPTDelaySettings />
<RNNoiseSettings />
<InputProcessorButton />
</div>
</div>
</HighlightRegion>
</div>
</HighlightContainer>
</EventContext.Provider>
);
}

View File

@ -98,6 +98,7 @@ html:root {
flex-shrink: 1;
background: transparent;
align-self: center;
border: none;
outline: none;

View File

@ -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}

View File

@ -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>,
]
}

View File

@ -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
});

View File

@ -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";
});
}
}
});

View File

@ -1,4 +1,5 @@
@import "../../../../../../css/static/mixin";
@import "../../../../../../css/static/color-variables";
/* FIXME: Remove this wired import */
@import "../../../../../../css/static/general";

View File

@ -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,

View File

@ -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 {

View File

@ -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 */

View File

@ -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;

View File

@ -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 {

View File

@ -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"
},
};
};