498 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			498 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| window["require_setup"](module);
 | |
| 
 | |
| import {audio as naudio} from "teaclient_connection";
 | |
| // <reference types="../imports/import_shared.d.ts" />
 | |
| /// <reference types="../../modules/renderer/imports/imports_shared.d.ts" />
 | |
| 
 | |
| export namespace _audio.recorder {
 | |
|     import InputDevice = audio.recorder.InputDevice;
 | |
|     import AbstractInput = audio.recorder.AbstractInput;
 | |
| 
 | |
|     interface NativeDevice extends InputDevice {
 | |
|         device_index: number;
 | |
|         native: any;
 | |
|     }
 | |
| 
 | |
|     let _device_cache: NativeDevice[] = undefined;
 | |
|     export function devices() : InputDevice[] {
 | |
|         //TODO: Handle device updates!
 | |
|         if(!naudio.initialized()) return [];
 | |
| 
 | |
|         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,
 | |
|                 driver: e.driver,
 | |
|                 sample_rate: 48000, /* TODO! */
 | |
|                 native: e
 | |
|             } as NativeDevice
 | |
|         }));
 | |
|     }
 | |
| 
 | |
|     export function device_refresh_available() : boolean { return false; }
 | |
|     export function refresh_devices() : Promise<void> { 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 {
 | |
|                         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<void> {
 | |
|                 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<void> {
 | |
|                 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.consumer = this.handle.create_consumer();
 | |
|             this.consumer.callback_ended = () => {
 | |
|                 if(this._current_state !== audio.recorder.InputState.RECORDING)
 | |
|                     return;
 | |
| 
 | |
|                 this._current_state = audio.recorder.InputState.DRY;
 | |
|                 if(this.callback_end)
 | |
|                     this.callback_end();
 | |
|             };
 | |
|             this.consumer.callback_started = () => {
 | |
|                 if(this._current_state !== audio.recorder.InputState.DRY)
 | |
|                     return;
 | |
| 
 | |
|                 this._current_state = audio.recorder.InputState.RECORDING;
 | |
|                 if(this.callback_begin)
 | |
|                     this.callback_begin();
 | |
|             };
 | |
| 
 | |
|             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<void> {
 | |
|             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<void> {
 | |
|             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 !== audio.recorder.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(): 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<audio.recorder.InputStartResult> {
 | |
|             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._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 audio.recorder.InputStartResult.EOK;
 | |
|             } catch(error) {
 | |
|                 this._current_state = audio.recorder.InputState.PAUSED;
 | |
|                 throw error;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         async stop(): Promise<void> {
 | |
|             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;
 | |
|         }
 | |
| 
 | |
|         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<audio.recorder.LevelMeter> {
 | |
|         const meter = new NativeLevelmenter(device as any);
 | |
|         await meter.initialize();
 | |
|         return meter;
 | |
|     }
 | |
| 
 | |
|     class NativeLevelmenter implements audio.recorder.LevelMeter {
 | |
|         readonly _device: NativeDevice;
 | |
| 
 | |
|         private _callback: (num: number) => any;
 | |
|         private _recorder: naudio.record.AudioRecorder;
 | |
|         private _consumer: naudio.record.AudioConsumer;
 | |
|         private _filter: naudio.record.ThresholdConsumeFilter;
 | |
| 
 | |
|         constructor(device: NativeDevice) {
 | |
|             this._device = device;
 | |
|         }
 | |
| 
 | |
|         async initialize() {
 | |
|             try {
 | |
|                 this._recorder = naudio.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(): audio.recorder.InputDevice {
 | |
|             return this._device;
 | |
|         }
 | |
| 
 | |
|         set_observer(callback: (value: number) => any) {
 | |
|             this._callback = callback;
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| Object.assign(window["audio"] || (window["audio"] = {} as any), _audio); |