2020-04-01 21:56:23 +02:00
|
|
|
import {
|
|
|
|
AbstractInput,
|
2020-09-24 22:06:52 +02:00
|
|
|
FilterMode,
|
2020-04-01 21:56:23 +02:00
|
|
|
InputConsumer,
|
2020-08-21 13:37:10 +02:00
|
|
|
InputConsumerType,
|
|
|
|
InputEvents,
|
2021-01-08 22:04:27 +01:00
|
|
|
InputStartError,
|
2020-08-21 13:37:10 +02:00
|
|
|
InputState,
|
2021-01-08 22:04:27 +01:00
|
|
|
LevelMeter,
|
2020-04-01 21:56:23 +02:00
|
|
|
} from "tc-shared/voice/RecorderBase";
|
|
|
|
import {audio} from "tc-native/connection";
|
|
|
|
import {tr} from "tc-shared/i18n/localize";
|
2020-08-21 13:37:10 +02:00
|
|
|
import {Registry} from "tc-shared/events";
|
|
|
|
import {Filter, FilterType, FilterTypeClass} from "tc-shared/voice/Filter";
|
|
|
|
import {NativeFilter, NStateFilter, NThresholdFilter, NVoiceLevelFilter} from "./AudioFilter";
|
|
|
|
import {IDevice} from "tc-shared/audio/recorder";
|
2021-01-08 22:04:27 +01:00
|
|
|
import {LogCategory, logTrace, logWarn} from "tc-shared/log";
|
2020-10-05 15:10:49 +02:00
|
|
|
import {Settings, settings} from "tc-shared/settings";
|
2021-01-08 22:04:27 +01:00
|
|
|
import NativeFilterMode = audio.record.FilterMode;
|
2020-04-01 21:56:23 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
export class NativeInput implements AbstractInput {
|
2020-10-01 10:56:22 +02:00
|
|
|
static readonly instances = [] as NativeInput[];
|
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
readonly events: Registry<InputEvents>;
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-10-01 10:56:22 +02:00
|
|
|
readonly nativeHandle: audio.record.AudioRecorder;
|
|
|
|
readonly nativeConsumer: audio.record.AudioConsumer;
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
private state: InputState;
|
|
|
|
private deviceId: string | undefined;
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
private registeredFilters: (Filter & NativeFilter)[] = [];
|
|
|
|
private filtered = false;
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
constructor() {
|
|
|
|
this.events = new Registry<InputEvents>();
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
this.nativeHandle = audio.record.create_recorder();
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
this.nativeConsumer = this.nativeHandle.create_consumer();
|
2020-10-05 15:10:49 +02:00
|
|
|
this.nativeConsumer.toggle_rnnoise(settings.static_global(Settings.KEY_RNNOISE_FILTER));
|
2020-10-01 10:56:22 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
this.nativeConsumer.callback_ended = () => {
|
|
|
|
this.filtered = true;
|
|
|
|
this.events.fire("notify_voice_end");
|
|
|
|
};
|
|
|
|
this.nativeConsumer.callback_started = () => {
|
|
|
|
this.filtered = false;
|
|
|
|
this.events.fire("notify_voice_start");
|
|
|
|
};
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
this.state = InputState.PAUSED;
|
2020-10-01 10:56:22 +02:00
|
|
|
NativeInput.instances.push(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
const index = NativeInput.instances.indexOf(this);
|
|
|
|
if(index !== -1) {
|
|
|
|
NativeInput.instances.splice(index, 1);
|
|
|
|
}
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2021-01-08 22:04:27 +01:00
|
|
|
private setState(newState: InputState) {
|
|
|
|
if(this.state === newState) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const oldState = this.state;
|
|
|
|
this.state = newState;
|
|
|
|
this.events.fire("notify_state_changed", { oldState, newState });
|
|
|
|
}
|
|
|
|
|
|
|
|
async start(): Promise<InputStartError | true> {
|
|
|
|
if(this.state !== InputState.PAUSED) {
|
|
|
|
logWarn(LogCategory.VOICE, tr("Input isn't paused"));
|
|
|
|
return InputStartError.EBUSY;
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2021-01-08 22:04:27 +01:00
|
|
|
this.setState(InputState.INITIALIZING);
|
|
|
|
logTrace(LogCategory.AUDIO, tr("Starting input for device %o", this.deviceId));
|
2020-08-21 13:37:10 +02:00
|
|
|
try {
|
|
|
|
const state = await new Promise<audio.record.DeviceSetResult>(resolve => this.nativeHandle.set_device(this.deviceId, resolve));
|
|
|
|
|
|
|
|
if(state !== "success") {
|
|
|
|
if(state === "invalid-device") {
|
2021-01-08 22:04:27 +01:00
|
|
|
return InputStartError.EDEVICEUNKNOWN;
|
2020-08-21 13:37:10 +02:00
|
|
|
} else if(state === undefined) {
|
|
|
|
throw tr("invalid set device result state");
|
|
|
|
}
|
2020-11-29 21:06:57 +01:00
|
|
|
|
|
|
|
/* FIXME! */
|
2020-08-21 13:37:10 +02:00
|
|
|
throw state;
|
2019-10-26 01:51:40 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
await new Promise((resolve, reject) => this.nativeHandle.start(result => {
|
|
|
|
if(result === true) {
|
|
|
|
resolve();
|
|
|
|
} else {
|
|
|
|
reject(typeof result === "string" ? result : tr("failed to start input"));
|
|
|
|
}
|
|
|
|
}));
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2021-01-08 22:04:27 +01:00
|
|
|
this.setState(InputState.RECORDING);
|
2020-11-29 21:06:57 +01:00
|
|
|
return true;
|
2020-08-21 13:37:10 +02:00
|
|
|
} finally {
|
2021-01-08 22:04:27 +01:00
|
|
|
/* @ts-ignore Typescript isn't smart about awaits in try catch blocks */
|
2020-08-21 13:37:10 +02:00
|
|
|
if(this.state === InputState.INITIALIZING) {
|
2021-01-08 22:04:27 +01:00
|
|
|
this.setState(InputState.PAUSED);
|
2019-10-26 01:51:40 +02:00
|
|
|
}
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
async stop(): Promise<void> {
|
2021-01-08 22:04:27 +01:00
|
|
|
if(this.state === InputState.PAUSED) {
|
2020-08-21 13:37:10 +02:00
|
|
|
return;
|
2021-01-08 22:04:27 +01:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
this.nativeHandle.stop();
|
2021-01-08 22:04:27 +01:00
|
|
|
this.setState(InputState.PAUSED);
|
|
|
|
|
|
|
|
if(this.filtered) {
|
|
|
|
this.filtered = false;
|
|
|
|
this.events.fire("notify_voice_end");
|
|
|
|
}
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
async setDeviceId(device: string | undefined): Promise<void> {
|
2020-09-24 22:06:52 +02:00
|
|
|
if(this.deviceId === device) {
|
2020-04-01 21:56:23 +02:00
|
|
|
return;
|
2020-09-24 22:06:52 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2021-01-08 22:04:27 +01:00
|
|
|
try {
|
|
|
|
await this.stop();
|
|
|
|
} catch (error) {
|
|
|
|
logWarn(LogCategory.GENERAL, tr("Failed to stop microphone recording after device change: %o"), error);
|
|
|
|
}
|
|
|
|
|
|
|
|
const oldDeviceId = this.deviceId;
|
2020-08-21 13:37:10 +02:00
|
|
|
this.deviceId = device;
|
2021-01-08 22:04:27 +01:00
|
|
|
this.events.fire("notify_device_changed", { oldDeviceId, newDeviceId: device });
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
currentDeviceId(): string | undefined {
|
|
|
|
return this.deviceId;
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
isFiltered(): boolean {
|
|
|
|
return this.filtered;
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
removeFilter(filter: Filter) {
|
|
|
|
const index = this.registeredFilters.indexOf(filter as any);
|
2021-01-08 22:04:27 +01:00
|
|
|
if(index === -1) {
|
|
|
|
return;
|
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2021-01-08 22:04:27 +01:00
|
|
|
const [ registeredFilter ] = this.registeredFilters.splice(index, 1);
|
|
|
|
registeredFilter.finalize();
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
createFilter<T extends FilterType>(type: T, priority: number): FilterTypeClass<T> {
|
|
|
|
let filter;
|
2020-04-01 21:56:23 +02:00
|
|
|
switch (type) {
|
2020-08-21 13:37:10 +02:00
|
|
|
case FilterType.STATE:
|
|
|
|
filter = new NStateFilter(this, priority);
|
2020-04-01 21:56:23 +02:00
|
|
|
break;
|
2020-08-21 13:37:10 +02:00
|
|
|
|
|
|
|
case FilterType.THRESHOLD:
|
|
|
|
filter = new NThresholdFilter(this, priority);
|
2020-04-01 21:56:23 +02:00
|
|
|
break;
|
2020-08-21 13:37:10 +02:00
|
|
|
|
|
|
|
case FilterType.VOICE_LEVEL:
|
|
|
|
filter = new NVoiceLevelFilter(this, priority);
|
2020-04-01 21:56:23 +02:00
|
|
|
break;
|
2019-10-26 01:51:40 +02:00
|
|
|
}
|
2020-08-21 13:37:10 +02:00
|
|
|
|
|
|
|
this.registeredFilters.push(filter);
|
|
|
|
return filter;
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
supportsFilter(type: FilterType): boolean {
|
2020-04-01 21:56:23 +02:00
|
|
|
switch (type) {
|
2020-08-21 13:37:10 +02:00
|
|
|
case FilterType.VOICE_LEVEL:
|
|
|
|
case FilterType.THRESHOLD:
|
|
|
|
case FilterType.STATE:
|
2020-04-01 21:56:23 +02:00
|
|
|
return true;
|
2020-08-21 13:37:10 +02:00
|
|
|
|
2020-04-01 21:56:23 +02:00
|
|
|
default:
|
|
|
|
return false;
|
2019-10-26 01:51:40 +02:00
|
|
|
}
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
currentState(): InputState {
|
|
|
|
return this.state;
|
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
currentConsumer(): InputConsumer | undefined {
|
|
|
|
return {
|
|
|
|
type: InputConsumerType.NATIVE
|
|
|
|
};
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
getNativeConsumer() : audio.record.AudioConsumer {
|
|
|
|
return this.nativeConsumer;
|
2019-10-26 01:51:40 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
async setConsumer(consumer: InputConsumer): Promise<void> {
|
2020-09-24 22:06:52 +02:00
|
|
|
if(typeof(consumer) !== "undefined") {
|
2020-08-21 13:37:10 +02:00
|
|
|
throw "we only support native consumers!"; // TODO: May create a general wrapper?
|
2020-09-24 22:06:52 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
return;
|
2019-10-26 01:51:40 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
setVolume(volume: number) {
|
|
|
|
this.nativeHandle.set_volume(volume);
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
getVolume(): number {
|
|
|
|
return this.nativeHandle.get_volume();
|
|
|
|
}
|
2020-09-24 22:06:52 +02:00
|
|
|
|
|
|
|
getFilterMode(): FilterMode {
|
|
|
|
const mode = this.nativeConsumer.get_filter_mode();
|
|
|
|
switch (mode) {
|
|
|
|
case NativeFilterMode.Block:
|
|
|
|
return FilterMode.Block;
|
|
|
|
|
|
|
|
case NativeFilterMode.Bypass:
|
|
|
|
return FilterMode.Bypass;
|
|
|
|
|
|
|
|
case NativeFilterMode.Filter:
|
|
|
|
default:
|
|
|
|
return FilterMode.Filter;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setFilterMode(mode: FilterMode) {
|
|
|
|
let nativeMode: NativeFilterMode;
|
|
|
|
switch (mode) {
|
|
|
|
case FilterMode.Filter:
|
|
|
|
nativeMode = NativeFilterMode.Filter;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case FilterMode.Bypass:
|
|
|
|
nativeMode = NativeFilterMode.Bypass;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case FilterMode.Block:
|
|
|
|
nativeMode = NativeFilterMode.Block;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-01-08 22:04:27 +01:00
|
|
|
if(this.nativeConsumer.get_filter_mode() === nativeMode) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const oldMode = this.getFilterMode();
|
2020-09-24 22:06:52 +02:00
|
|
|
this.nativeConsumer.set_filter_mode(nativeMode);
|
2021-01-08 22:04:27 +01:00
|
|
|
this.events.fire("notify_filter_mode_changed", { oldMode, newMode: mode });
|
2020-09-24 22:06:52 +02:00
|
|
|
}
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
export class NativeLevelMeter implements LevelMeter {
|
2020-10-01 10:56:22 +02:00
|
|
|
static readonly instances: NativeLevelMeter[] = [];
|
|
|
|
readonly targetDevice: IDevice;
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-10-01 10:56:22 +02:00
|
|
|
public nativeRecorder: audio.record.AudioRecorder;
|
|
|
|
public nativeConsumer: audio.record.AudioConsumer;
|
|
|
|
|
|
|
|
private callback: (num: number) => any;
|
|
|
|
private nativeFilter: audio.record.ThresholdConsumeFilter;
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
constructor(device: IDevice) {
|
2020-10-01 10:56:22 +02:00
|
|
|
this.targetDevice = device;
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-04-01 21:56:23 +02:00
|
|
|
async initialize() {
|
|
|
|
try {
|
2020-10-01 10:56:22 +02:00
|
|
|
this.nativeRecorder = audio.record.create_recorder();
|
|
|
|
this.nativeConsumer = this.nativeRecorder.create_consumer();
|
|
|
|
|
2020-10-05 15:10:49 +02:00
|
|
|
this.nativeConsumer.toggle_rnnoise(settings.static_global(Settings.KEY_RNNOISE_FILTER));
|
2020-04-01 21:56:23 +02:00
|
|
|
|
2020-10-01 10:56:22 +02:00
|
|
|
this.nativeFilter = this.nativeConsumer.create_filter_threshold(.5);
|
|
|
|
this.nativeFilter.set_attack_smooth(.75);
|
|
|
|
this.nativeFilter.set_release_smooth(.75);
|
2020-04-01 21:56:23 +02:00
|
|
|
|
2020-10-01 10:56:22 +02:00
|
|
|
await new Promise(resolve => this.nativeRecorder.set_device(this.targetDevice.deviceId, resolve));
|
2020-04-01 21:56:23 +02:00
|
|
|
await new Promise((resolve, reject) => {
|
2020-10-01 10:56:22 +02:00
|
|
|
this.nativeRecorder.start(flag => {
|
2020-04-01 21:56:23 +02:00
|
|
|
if (typeof flag === "boolean" && flag)
|
|
|
|
resolve();
|
|
|
|
else
|
|
|
|
reject(typeof flag === "string" ? flag : "failed to start");
|
2019-10-26 01:51:40 +02:00
|
|
|
});
|
|
|
|
});
|
2020-04-01 21:56:23 +02:00
|
|
|
} catch (error) {
|
2021-01-08 22:04:27 +01:00
|
|
|
if (typeof (error) === "string") {
|
2020-04-01 21:56:23 +02:00
|
|
|
throw error;
|
2021-01-08 22:04:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
logWarn(LogCategory.AUDIO, tr("Failed to initialize level meter for device %o: %o"), this.targetDevice, error);
|
2020-04-01 21:56:23 +02:00
|
|
|
throw "initialize failed (lookup console)";
|
2019-10-26 01:51:40 +02:00
|
|
|
}
|
|
|
|
|
2021-01-08 22:04:27 +01:00
|
|
|
/* references this variable, needs a destroy() call, else memory leak */
|
2020-10-01 10:56:22 +02:00
|
|
|
this.nativeFilter.set_analyze_filter(value => {
|
|
|
|
if(this.callback) this.callback(value);
|
2020-04-01 21:56:23 +02:00
|
|
|
});
|
2020-10-01 10:56:22 +02:00
|
|
|
|
|
|
|
NativeLevelMeter.instances.push(this);
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-08-21 13:37:10 +02:00
|
|
|
destroy() {
|
2020-10-01 10:56:22 +02:00
|
|
|
const index = NativeLevelMeter.instances.indexOf(this);
|
|
|
|
if(index !== -1) {
|
|
|
|
NativeLevelMeter.instances.splice(index, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.nativeFilter) {
|
|
|
|
this.nativeFilter.set_analyze_filter(undefined);
|
|
|
|
this.nativeConsumer.unregister_filter(this.nativeFilter);
|
2019-10-26 01:51:40 +02:00
|
|
|
}
|
2020-08-21 13:37:10 +02:00
|
|
|
|
2020-10-01 10:56:22 +02:00
|
|
|
if (this.nativeConsumer) {
|
|
|
|
this.nativeRecorder.delete_consumer(this.nativeConsumer);
|
2020-08-21 13:37:10 +02:00
|
|
|
}
|
|
|
|
|
2020-10-01 10:56:22 +02:00
|
|
|
if(this.nativeRecorder) {
|
|
|
|
this.nativeRecorder.stop();
|
2020-08-21 13:37:10 +02:00
|
|
|
}
|
2020-10-01 10:56:22 +02:00
|
|
|
this.nativeRecorder = undefined;
|
|
|
|
this.nativeConsumer = undefined;
|
|
|
|
this.nativeFilter = undefined;
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
2019-10-26 01:51:40 +02:00
|
|
|
|
2020-09-24 22:06:52 +02:00
|
|
|
getDevice(): IDevice {
|
2020-10-01 10:56:22 +02:00
|
|
|
return this.targetDevice;
|
2019-10-26 01:51:40 +02:00
|
|
|
}
|
|
|
|
|
2020-09-24 22:06:52 +02:00
|
|
|
setObserver(callback: (value: number) => any) {
|
2020-10-01 10:56:22 +02:00
|
|
|
this.callback = callback;
|
2020-04-01 21:56:23 +02:00
|
|
|
}
|
|
|
|
}
|