import { AbstractInput, FilterMode, InputConsumer, InputConsumerType, InputEvents, InputState, LevelMeter, MediaStreamRequestResult } 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, logWarn} from "tc-shared/log"; import NativeFilterMode = audio.record.FilterMode; import {Settings, settings} from "tc-shared/settings"; 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.static_global(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); } } async start(): Promise { if(this.state === InputState.RECORDING) { logWarn(LogCategory.VOICE, tr("Tried to start an input recorder twice.")); return MediaStreamRequestResult.EBUSY; } this.state = InputState.INITIALIZING; try { const state = await new Promise(resolve => this.nativeHandle.set_device(this.deviceId, resolve)); if(state !== "success") { if(state === "invalid-device") { return MediaStreamRequestResult.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.state = InputState.RECORDING; return true; } finally { if(this.state === InputState.INITIALIZING) { this.state = InputState.PAUSED; } } } async stop(): Promise { if(this.state === InputState.PAUSED) return; this.nativeHandle.stop(); this.state = InputState.PAUSED; } async setDeviceId(device: string | undefined): Promise { if(this.deviceId === device) { return; } this.deviceId = device; await this.stop(); } 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 [ f ] = this.registeredFilters.splice(index, 1); f.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; } this.nativeConsumer.set_filter_mode(nativeMode); } } 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.static_global(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; console.warn(tr("Failed to initialize levelmeter for device %o: %o"), this.targetDevice, error); throw "initialize failed (lookup console)"; } /* references this variable, needs a destory() 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; } }