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