import { AbstractInput, InputConsumer, InputConsumerType, InputEvents, InputStartResult, 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, logWarn} from "tc-shared/log"; export class NativeInput implements AbstractInput { readonly events: Registry; private nativeHandle: audio.record.AudioRecorder; private 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.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; } async start(): Promise { if(this.state === InputState.RECORDING) { logWarn(LogCategory.VOICE, tr("Tried to start an input recorder twice.")); return InputStartResult.EOK; } 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 InputStartResult.EDEVICEUNKNOWN; } else if(state === undefined) { throw tr("invalid set device result state"); } 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 InputStartResult.EOK; } 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(); } } export class NativeLevelMeter implements LevelMeter { readonly _device: IDevice; private _callback: (num: number) => any; private _recorder: audio.record.AudioRecorder; private _consumer: audio.record.AudioConsumer; private _filter: audio.record.ThresholdConsumeFilter; constructor(device: IDevice) { this._device = device; } async initialize() { try { this._recorder = audio.record.create_recorder(); this._consumer = this._recorder.create_consumer(); this._filter = this._consumer.create_filter_threshold(.5); this._filter.set_attack_smooth(.75); this._filter.set_release_smooth(.75); await new Promise(resolve => this._recorder.set_device(this._device.deviceId, resolve)); await new Promise((resolve, reject) => { this._recorder.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._device, error); throw "initialize failed (lookup console)"; } /* references this variable, needs a destory() call, else memory leak */ this._filter.set_analyze_filter(value => { (this._callback || (() => { }))(value); }); } destroy() { if (this._filter) { this._filter.set_analyze_filter(undefined); this._consumer.unregister_filter(this._filter); } if (this._consumer) { this._recorder.delete_consumer(this._consumer); } if(this._recorder) { this._recorder.stop(); } this._recorder = undefined; this._consumer = undefined; this._filter = undefined; } device(): IDevice { return this._device; } set_observer(callback: (value: number) => any) { this._callback = callback; } }