TeaSpeak-Client/modules/renderer/audio/AudioRecorder.ts

393 lines
14 KiB
TypeScript
Raw Normal View History

2019-06-26 16:09:01 -04:00
/// <reference path="../imports/imports_shared.d.ts" />
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<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 {
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<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._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;
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<void> {
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<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;
}
}
}
Object.assign(window["audio"] || (window["audio"] = {}), _audio);
_audio.recorder.devices(); /* query devices */