510 lines
18 KiB
TypeScript
510 lines
18 KiB
TypeScript
window["require_setup"](module);
|
|
|
|
import {audio as naudio} from "teaclient_connection";
|
|
//import {audio, tr} from "../imports/imports_shared";
|
|
/// <reference types="./imports/import_shared.d.ts" />
|
|
|
|
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,
|
|
driver: e.driver,
|
|
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 {
|
|
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;
|
|
|
|
const device = _device as NativeDevice; /* TODO: test for? */
|
|
this._current_device = _device;
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
this.handle.set_device(device ? device.device_index : -1, flag => {
|
|
if(typeof(flag) === "boolean" && flag)
|
|
resolve();
|
|
else
|
|
reject("failed to set device" + (typeof(flag) === "string" ? (": " + flag) : ""));
|
|
});
|
|
});
|
|
if(!device) return;
|
|
|
|
await new Promise((resolve, reject) => {
|
|
this.handle.start(flag => {
|
|
if(flag)
|
|
resolve();
|
|
else
|
|
reject("start failed");
|
|
});
|
|
});
|
|
} 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 {
|
|
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, reject) => {
|
|
this._recorder.set_device(this._device.device_index, flag => {
|
|
if(typeof(flag) === "boolean" && flag)
|
|
resolve();
|
|
else
|
|
reject("initialize failed" + (typeof(flag) === "string" ? (": " + flag) : ""));
|
|
});
|
|
});
|
|
await new Promise((resolve, reject) => {
|
|
this._recorder.start(flag => {
|
|
if(flag)
|
|
resolve();
|
|
else
|
|
reject("start failed");
|
|
});
|
|
});
|
|
} 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(-1, () => {}); /* -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);
|
|
_audio.recorder.devices(); /* query devices */ |