import { filter, AbstractInput, InputDevice, InputState, InputConsumer, InputConsumerType, InputStartResult, LevelMeter } from "tc-shared/voice/RecorderBase"; import {audio} from "tc-native/connection"; import {tr} from "tc-shared/i18n/localize"; interface NativeDevice extends InputDevice { device_index: number; native: any; } let _device_cache: NativeDevice[] = undefined; export function devices() : InputDevice[] { //TODO: Handle device updates! if(!audio.initialized()) return []; return _device_cache || (_device_cache = audio.available_devices().filter(e => e.input_supported || e.input_default).map(e => { return { unique_id: e.device_id, channels: 2, /* TODO */ default_input: e.input_default, supported: e.input_supported, name: e.name, driver: e.driver, sample_rate: 48000, /* TODO! */ native: e } as NativeDevice })); } export function device_refresh_available() : boolean { return false; } export function refresh_devices() : Promise { throw "not supported yet!"; } export function create_input() : AbstractInput { return new NativeInput(); } namespace filters { export abstract class NativeFilter implements filter.Filter { type: filter.Type; handle: NativeInput; enabled: boolean = false; protected constructor(handle, type) { this.handle = handle; this.type = type; } abstract initialize(); abstract finalize(); is_enabled(): boolean { return this.enabled; } } export class NThresholdFilter extends NativeFilter implements filter.ThresholdFilter { static readonly frames_per_second = 1 / (960 / 48000); private filter: audio.record.ThresholdConsumeFilter; private _margin_frames: number = 25; /* 120ms */ private _threshold: number = 50; private _callback_level: any; private _attack_smooth = 0; private _release_smooth = 0; callback_level: (level: number) => any; constructor(handle) { super(handle, filter.Type.THRESHOLD); Object.defineProperty(this, 'callback_level', { get(): any { return this._callback_level; }, set(v: any): void { if(v === this._callback_level) return; this._callback_level = v; if(this.filter) this.filter.set_analyze_filter(v); }, enumerable: true, configurable: false, }) } get_margin_frames(): number { return this.filter ? this.filter.get_margin_time() * NThresholdFilter.frames_per_second : this._margin_frames; } get_threshold(): number { return this.filter ? this.filter.get_threshold() : this._threshold; } set_margin_frames(value: number) { this._margin_frames = value; if(this.filter) this.filter.set_margin_time(value / 960 / 1000); } get_attack_smooth(): number { return this.filter ? this.filter.get_attack_smooth() : this._attack_smooth; } get_release_smooth(): number { return this.filter ? this.filter.get_release_smooth() : this._release_smooth; } set_attack_smooth(value: number) { this._attack_smooth = value; if(this.filter) this.filter.set_attack_smooth(value); } set_release_smooth(value: number) { this._release_smooth = value; if(this.filter) this.filter.set_release_smooth(value); } set_threshold(value: number): Promise { if(typeof(value) === "string") value = parseInt(value); /* yes... this happens */ this._threshold = value; if(this.filter) this.filter.set_threshold(value); return Promise.resolve(); } finalize() { if(this.filter) { if(this.handle.consumer) this.handle.consumer.unregister_filter(this.filter); this.filter = undefined; } } initialize() { if(!this.handle.consumer) return; this.finalize(); this.filter = this.handle.consumer.create_filter_threshold(this._threshold); if(this._callback_level) this.filter.set_analyze_filter(this._callback_level); this.filter.set_margin_time(this._margin_frames / NThresholdFilter.frames_per_second); this.filter.set_attack_smooth(this._attack_smooth); this.filter.set_release_smooth(this._release_smooth); } } export class NStateFilter extends NativeFilter implements filter.StateFilter { private filter: audio.record.StateConsumeFilter; private active = false; constructor(handle) { super(handle, filter.Type.STATE); } finalize() { if(this.filter) { if(this.handle.consumer) this.handle.consumer.unregister_filter(this.filter); this.filter = undefined; } } initialize() { if(!this.handle.consumer) return; this.finalize(); this.filter = this.handle.consumer.create_filter_state(); this.filter.set_consuming(this.active); } is_active(): boolean { return this.active; } async set_state(state: boolean): Promise { if(this.active === state) return; this.active = state; if(this.filter) this.filter.set_consuming(state); } } export class NVoiceLevelFilter extends NativeFilter implements filter.VoiceLevelFilter { static readonly frames_per_second = 1 / (960 / 48000); private filter: audio.record.VADConsumeFilter; private level = 3; private _margin_frames = 6; constructor(handle) { super(handle, filter.Type.VOICE_LEVEL); } finalize() { if(this.filter) { if(this.handle.consumer) this.handle.consumer.unregister_filter(this.filter); this.filter = undefined; } } initialize() { if(!this.handle.consumer) return; this.finalize(); this.filter = this.handle.consumer.create_filter_vad(this.level); this.filter.set_margin_time(this._margin_frames / NVoiceLevelFilter.frames_per_second); } get_level(): number { return this.level; } set_level(value: number) { if(this.level === value) return; this.level = value; if(this.filter) { this.finalize(); this.initialize(); } } set_margin_frames(value: number) { this._margin_frames = value; if(this.filter) this.filter.set_margin_time(value / NVoiceLevelFilter.frames_per_second); } get_margin_frames(): number { return this.filter ? this.filter.get_margin_time() * NVoiceLevelFilter.frames_per_second : this._margin_frames; } } } export class NativeInput implements AbstractInput { private handle: audio.record.AudioRecorder; consumer: audio.record.AudioConsumer; private _current_device: InputDevice; private _current_state: InputState = InputState.PAUSED; callback_begin: () => any; callback_end: () => any; private filters: filters.NativeFilter[] = []; constructor() { this.handle = audio.record.create_recorder(); this.consumer = this.handle.create_consumer(); this.consumer.callback_ended = () => { if(this._current_state !== InputState.RECORDING) return; this._current_state = InputState.DRY; if(this.callback_end) this.callback_end(); }; this.consumer.callback_started = () => { if(this._current_state !== InputState.DRY) return; this._current_state = InputState.RECORDING; if(this.callback_begin) this.callback_begin(); }; this._current_state = InputState.PAUSED; } /* TODO: some kind of finalize? */ current_consumer(): InputConsumer | undefined { return { type: InputConsumerType.NATIVE }; } async set_consumer(consumer: InputConsumer): Promise { if(typeof(consumer) !== "undefined") throw "we only support native consumers!"; /* TODO: May create a general wrapper? */ return; } async set_device(_device: InputDevice | undefined): Promise { if(_device === this._current_device) return; this._current_device = _device; try { await new Promise(resolve => this.handle.set_device(this._current_device ? this._current_device.unique_id : undefined, resolve)); if(this._current_state !== InputState.PAUSED && this._current_device) await new Promise((resolve, reject) => { this.handle.start(flag => { if(typeof flag === "boolean" && flag) resolve(); else reject(typeof flag === "string" ? flag : "failed to start"); }); }); } catch(error) { console.warn(tr("Failed to start playback on new input device (%o)"), error); throw error; } } current_device(): InputDevice | undefined { return this._current_device; } current_state(): InputState { return this._current_state; } disable_filter(type: filter.Type) { const filter = this.get_filter(type) as filters.NativeFilter; if(filter.is_enabled()) filter.enabled = false; filter.finalize(); } enable_filter(type: filter.Type) { const filter = this.get_filter(type) as filters.NativeFilter; if(!filter.is_enabled()) { filter.enabled = true; filter.initialize(); } } clear_filter() { for(const filter of this.filters) { filter.enabled = false; filter.finalize(); } } get_filter(type: filter.Type): filter.Filter | undefined { for(const filter of this.filters) if(filter.type === type) return filter; let _filter: filters.NativeFilter; switch (type) { case filter.Type.THRESHOLD: _filter = new filters.NThresholdFilter(this); break; case filter.Type.STATE: _filter = new filters.NStateFilter(this); break; case filter.Type.VOICE_LEVEL: _filter = new filters.NVoiceLevelFilter(this); break; default: throw "this filter isn't supported!"; } this.filters.push(_filter); return _filter; } supports_filter(type: filter.Type) : boolean { switch (type) { case filter.Type.THRESHOLD: case filter.Type.STATE: case filter.Type.VOICE_LEVEL: return true; default: return false; } } async start(): Promise { try { await this.stop(); } catch(error) { console.warn(tr("Failed to stop old record session before start (%o)"), error); } this._current_state = InputState.DRY; try { if(this._current_device) await new Promise((resolve, reject) => { this.handle.start(flag => { if(flag) resolve(); else reject("start failed"); }); }); for(const filter of this.filters) if(filter.is_enabled()) filter.initialize(); return InputStartResult.EOK; } catch(error) { this._current_state = InputState.PAUSED; throw error; } } async stop(): Promise { this.handle.stop(); for(const filter of this.filters) filter.finalize(); if(this.callback_end) this.callback_end(); this._current_state = InputState.PAUSED; } get_volume(): number { return this.handle.get_volume(); } set_volume(volume: number) { this.handle.set_volume(volume); } } export async function create_levelmeter(device: InputDevice) : Promise { const meter = new NativeLevelmenter(device as any); await meter.initialize(); return meter; } class NativeLevelmenter implements LevelMeter { readonly _device: NativeDevice; private _callback: (num: number) => any; private _recorder: audio.record.AudioRecorder; private _consumer: audio.record.AudioConsumer; private _filter: audio.record.ThresholdConsumeFilter; constructor(device: NativeDevice) { 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 ? this._device.unique_id : undefined, 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); }); } destory() { if (this._filter) { this._filter.set_analyze_filter(undefined); this._consumer.unregister_filter(this._filter); } if (this._consumer) this._recorder.delete_consumer(this._consumer); this._recorder.stop(); this._recorder.set_device(undefined, () => { }); /* -1 := No device */ this._recorder = undefined; this._consumer = undefined; this._filter = undefined; } device(): InputDevice { return this._device; } set_observer(callback: (value: number) => any) { this._callback = callback; } }