415 lines
No EOL
18 KiB
TypeScript
415 lines
No EOL
18 KiB
TypeScript
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; |