/// window["require_setup"](module); import {audio as naudio} from "teaclient_connection"; export namespace _audio.recorder { import InputDevice = audio.recorder.InputDevice; import AbstractInput = audio.recorder.AbstractInput; interface NativeDevice extends InputDevice { device_index: number; } let _device_cache: NativeDevice[] = undefined; export function devices() : InputDevice[] { return _device_cache || (_device_cache = naudio.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, sample_rate: 44100, /* TODO! */ device_index: e.device_index } 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 filter { export abstract class NativeFilter implements audio.recorder.filter.Filter { type: audio.recorder.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 audio.recorder.filter.ThresholdFilter { private filter: naudio.record.ThresholdConsumeFilter; private _margin_frames: number = 6; /* 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, audio.recorder.filter.Type.THRESHOLD); Object.defineProperty(this, 'callback_level', { get(): any { return this._callback_level; }, set(v: any): void { console.log("SET CALLBACK LEVEL! %o", v); 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_frames() : 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_frames(value); } 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_frames(this._margin_frames); this.filter.set_attack_smooth(this._attack_smooth); this.filter.set_release_smooth(this._release_smooth); } } export class NStateFilter extends NativeFilter implements audio.recorder.filter.StateFilter { private filter: naudio.record.StateConsumeFilter; private active = false; constructor(handle) { super(handle, audio.recorder.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 audio.recorder.filter.VoiceLevelFilter { private filter: naudio.record.VADConsumeFilter; private level = 3; private _margin_frames = 5; constructor(handle) { super(handle, audio.recorder.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_frames(this._margin_frames); } 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_frames(value); } get_margin_frames(): number { return this.filter ? this.filter.get_margin_frames() : this._margin_frames; } } } export class NativeInput implements AbstractInput { private handle: naudio.record.AudioRecorder; consumer: naudio.record.AudioConsumer; private _current_device: audio.recorder.InputDevice; private _current_state: audio.recorder.InputState = audio.recorder.InputState.PAUSED; callback_begin: () => any; callback_end: () => any; private filters: filter.NativeFilter[] = []; constructor() { this.handle = naudio.record.create_recorder(); this._current_state = audio.recorder.InputState.PAUSED; } /* TODO: some kind of finalize? */ current_consumer(): audio.recorder.InputConsumer | undefined { return { type: audio.recorder.InputConsumerType.NATIVE }; } async set_consumer(consumer: audio.recorder.InputConsumer): Promise { if(typeof(consumer) !== "undefined") throw "we only support native consumers!"; /* TODO: May create a general wrapper? */ return; } async set_device(_device: audio.recorder.InputDevice | undefined): Promise { if(_device === this._current_device) return; const device = _device as NativeDevice; /* TODO: test for? */ this._current_device = _device; this.handle.set_device(device ? device.device_index : -1); try { this.handle.start(); /* TODO: Test for state! */ } catch(error) { console.warn(tr("Failed to start playback on new input device (%o)"), error); throw error; } } current_device(): audio.recorder.InputDevice | undefined { return this._current_device; } current_state(): audio.recorder.InputState { return this._current_state; } disable_filter(type: audio.recorder.filter.Type) { const filter = this.get_filter(type) as filter.NativeFilter; if(filter.is_enabled()) filter.enabled = false; filter.finalize(); } enable_filter(type: audio.recorder.filter.Type) { const filter = this.get_filter(type) as filter.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: audio.recorder.filter.Type): audio.recorder.filter.Filter | undefined { for(const filter of this.filters) if(filter.type === type) return filter; let _filter: filter.NativeFilter; switch (type) { case audio.recorder.filter.Type.THRESHOLD: _filter = new filter.NThresholdFilter(this); break; case audio.recorder.filter.Type.STATE: _filter = new filter.NStateFilter(this); break; case audio.recorder.filter.Type.VOICE_LEVEL: _filter = new filter.NVoiceLevelFilter(this); break; default: throw "this filter isn't supported!"; } this.filters.push(_filter); return _filter; } supports_filter(type: audio.recorder.filter.Type) : boolean { switch (type) { case audio.recorder.filter.Type.THRESHOLD: case audio.recorder.filter.Type.STATE: case audio.recorder.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 = audio.recorder.InputState.DRY; try { if(!this.consumer) { this.consumer = this.handle.create_consumer(); this.consumer.callback_ended = () => { this._current_state = audio.recorder.InputState.RECORDING; if(this.callback_end) this.callback_end(); }; this.consumer.callback_started = () => { this._current_state = audio.recorder.InputState.DRY; if(this.callback_begin) this.callback_begin(); }; } this.handle.start(); for(const filter of this.filters) if(filter.is_enabled()) filter.initialize(); } catch(error) { this._current_state = audio.recorder.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 = audio.recorder.InputState.PAUSED; } } } Object.assign(window["audio"] || (window["audio"] = {}), _audio); _audio.recorder.devices(); /* query devices */