TeaWeb/shared/js/voice/VoiceRecorder.ts

476 lines
16 KiB
TypeScript
Raw Normal View History

2018-03-07 19:06:52 +01:00
/// <reference path="VoiceHandler.ts" />
2019-04-04 21:47:52 +02:00
/// <reference path="../ui/elements/modal.ts" />
2018-03-07 19:06:52 +01:00
abstract class VoiceActivityDetector {
2018-03-08 15:40:31 +01:00
protected handle: VoiceRecorder;
2018-03-07 19:06:52 +01:00
abstract shouldRecord(buffer: AudioBuffer) : boolean;
2018-03-08 15:40:31 +01:00
initialise() {}
2018-03-07 19:06:52 +01:00
finalize() {}
2018-03-08 15:40:31 +01:00
initialiseNewStream(old: MediaStreamAudioSourceNode, _new: MediaStreamAudioSourceNode) : void {}
changeHandle(handle: VoiceRecorder, triggerNewStream: boolean) {
const oldStream = !this.handle ? undefined : this.handle.getMicrophoneStream();
this.handle = handle;
if(triggerNewStream) this.initialiseNewStream(oldStream, !handle ? undefined : handle.getMicrophoneStream());
}
2018-03-07 19:06:52 +01:00
}
2018-05-07 11:51:50 +02:00
//A small class extention
interface MediaStreamConstraints {
deviceId?: string;
2018-06-19 20:31:05 +02:00
groupId?: string;
2018-05-07 11:51:50 +02:00
}
2018-09-26 15:04:56 +02:00
if(!AudioBuffer.prototype.copyToChannel) { //Webkit does not implement this function
AudioBuffer.prototype.copyToChannel = function (source: Float32Array, channelNumber: number, startInChannel?: number) {
if(!startInChannel) startInChannel = 0;
let destination = this.getChannelData(channelNumber);
for(let index = 0; index < source.length; index++)
if(destination.length < index + startInChannel)
destination[index + startInChannel] = source[index];
}
}
2019-04-04 21:47:52 +02:00
let voice_recoder: VoiceRecorder;
2018-03-07 19:06:52 +01:00
class VoiceRecorder {
private static readonly CHANNEL = 0;
2018-10-16 19:46:30 +02:00
private static readonly CHANNELS = 2;
2018-05-07 11:51:50 +02:00
private static readonly BUFFER_SIZE = 1024 * 4;
2018-03-07 19:06:52 +01:00
2019-04-04 21:47:52 +02:00
on_support_state_change: () => any;
2018-09-21 23:25:03 +02:00
on_data: (data: AudioBuffer, head: boolean) => void = undefined;
2018-09-22 15:14:39 +02:00
on_end: () => any;
on_start: () => any;
2019-04-04 21:47:52 +02:00
on_yield: () => any; /* called when owner looses ownership */
owner: connection.voice.AbstractVoiceConnection | undefined;
private on_ready_callbacks: (() => any)[] = [];
2018-03-07 19:06:52 +01:00
private _recording: boolean = false;
2019-04-04 21:47:52 +02:00
private _recording_supported: boolean = true; /* recording is supported until anything else had been set */
private _tag_favicon: JQuery;
2018-03-07 19:06:52 +01:00
private microphoneStream: MediaStreamAudioSourceNode = undefined;
private mediaStream: MediaStream = undefined;
private audioContext: AudioContext;
2018-09-21 23:25:03 +02:00
private processor: ScriptProcessorNode;
get_output_stream() : ScriptProcessorNode { return this.processor; }
2018-03-07 19:06:52 +01:00
private vadHandler: VoiceActivityDetector;
private _chunkCount: number = 0;
2018-04-11 17:56:09 +02:00
private _deviceId: string;
2018-06-19 20:31:05 +02:00
private _deviceGroup: string;
2018-03-17 08:05:37 +01:00
2019-04-04 21:47:52 +02:00
private current_handler: ConnectionHandler;
2018-03-07 19:06:52 +01:00
2019-04-04 21:47:52 +02:00
constructor() {
2018-06-19 20:31:05 +02:00
this._deviceId = settings.global("microphone_device_id", "default");
this._deviceGroup = settings.global("microphone_device_group", "default");
2018-04-11 17:56:09 +02:00
audio.player.on_ready(() => {
this.audioContext = audio.player.context();
this.processor = this.audioContext.createScriptProcessor(VoiceRecorder.BUFFER_SIZE, VoiceRecorder.CHANNELS, VoiceRecorder.CHANNELS);
2018-09-22 15:14:39 +02:00
const empty_buffer = this.audioContext.createBuffer(VoiceRecorder.CHANNELS, VoiceRecorder.BUFFER_SIZE, 48000);
this.processor.addEventListener('audioprocess', ev => {
2018-09-21 23:25:03 +02:00
if(this.microphoneStream && this.vadHandler.shouldRecord(ev.inputBuffer)) {
2019-04-04 21:47:52 +02:00
if(this._chunkCount == 0)
this.on_voice_start();
2018-09-22 15:14:39 +02:00
2018-09-21 23:25:03 +02:00
if(this.on_data)
2018-09-22 15:14:39 +02:00
this.on_data(ev.inputBuffer, this._chunkCount == 0);
2018-09-21 23:25:03 +02:00
else {
for(let channel = 0; channel < ev.inputBuffer.numberOfChannels; channel++)
ev.outputBuffer.copyToChannel(ev.inputBuffer.getChannelData(channel), channel);
}
2018-09-22 15:14:39 +02:00
this._chunkCount++;
2018-09-21 23:25:03 +02:00
} else {
2019-04-04 21:47:52 +02:00
if(this._chunkCount != 0 )
this.on_voice_end();
2018-09-22 15:14:39 +02:00
this._chunkCount = 0;
for(let channel = 0; channel < ev.inputBuffer.numberOfChannels; channel++)
ev.outputBuffer.copyToChannel(empty_buffer.getChannelData(channel), channel);
}
});
2018-08-12 22:31:40 +02:00
this.processor.connect(this.audioContext.destination);
2018-08-12 22:31:40 +02:00
if(this.vadHandler)
this.vadHandler.initialise();
2018-08-12 22:31:40 +02:00
this.on_microphone(this.mediaStream);
2019-04-04 21:47:52 +02:00
for(const callback of this.on_ready_callbacks)
callback();
this.on_ready_callbacks = [];
2018-03-07 19:06:52 +01:00
});
2018-06-19 20:31:05 +02:00
this.setVADHandler(new PassThroughVAD());
2019-04-04 21:47:52 +02:00
this._tag_favicon = $("head link[rel='icon']");
2018-03-07 19:06:52 +01:00
}
2019-04-04 21:47:52 +02:00
own_recoder(connection: connection.voice.AbstractVoiceConnection | undefined) {
if(connection === this.owner)
return;
if(this.on_yield)
this.on_yield();
this.owner = connection;
this.on_end = undefined;
this.on_start = undefined;
this.on_data = undefined;
this.on_yield = undefined;
this.on_support_state_change = undefined;
this.on_ready_callbacks = [];
this._chunkCount = 0;
if(this.processor) /* processor stream might be null because of the late audio initialisation */
this.processor.connect(this.audioContext.destination);
2018-03-07 19:06:52 +01:00
}
2019-04-04 21:47:52 +02:00
input_available() : boolean {
return !!getUserMediaFunction();
2018-03-17 08:05:37 +01:00
}
2018-03-08 15:40:31 +01:00
getMediaStream() : MediaStream {
return this.mediaStream;
}
getMicrophoneStream() : MediaStreamAudioSourceNode {
return this.microphoneStream;
}
2018-04-11 17:56:09 +02:00
reinitialiseVAD() {
2018-04-16 20:38:35 +02:00
let type = settings.global("vad_type", "vad");
2018-03-07 19:06:52 +01:00
if(type == "ppt") {
if(settings.global('vad_ppt_key', undefined)) {
//TODO remove that because its legacy shit
createErrorModal(tr("VAD changed!"), tr("VAD key detection changed.<br>Please reset your PPT key!")).open();
}
let ppt_settings: PPTKeySettings = settings.global('vad_ppt_settings', undefined);
ppt_settings = ppt_settings ? JSON.parse(ppt_settings as any as string) : {};
if(ppt_settings.version === undefined)
ppt_settings.version = 1;
if(ppt_settings.key_code === undefined)
ppt_settings.key_code = "KeyT";
if(ppt_settings.key_ctrl === undefined)
ppt_settings.key_ctrl = false;
if(ppt_settings.key_shift === undefined)
ppt_settings.key_shift = false;
if(ppt_settings.key_alt === undefined)
ppt_settings.key_alt = false;
if(ppt_settings.key_windows === undefined)
ppt_settings.key_windows = false;
2019-01-27 13:11:40 +01:00
if(ppt_settings.delay === undefined)
ppt_settings.delay = 300;
2018-03-07 19:06:52 +01:00
if(!(this.getVADHandler() instanceof PushToTalkVAD))
this.setVADHandler(new PushToTalkVAD(ppt_settings));
2019-01-27 13:11:40 +01:00
else (this.getVADHandler() as PushToTalkVAD).settings = ppt_settings;
2018-03-07 19:06:52 +01:00
} else if(type == "pt") {
if(!(this.getVADHandler() instanceof PassThroughVAD))
2018-06-19 20:31:05 +02:00
this.setVADHandler(new PassThroughVAD());
2018-03-08 15:40:31 +01:00
} else if(type == "vad") {
if(!(this.getVADHandler() instanceof VoiceActivityDetectorVAD))
2018-06-19 20:31:05 +02:00
this.setVADHandler(new VoiceActivityDetectorVAD());
(this.getVADHandler() as VoiceActivityDetectorVAD).percentageThreshold = settings.global("vad_threshold", 50);
2018-03-07 19:06:52 +01:00
} else {
console.warn(tr("Invalid VAD (Voice activation detector) handler! (%o)"), type);
2018-03-07 19:06:52 +01:00
}
}
2018-06-19 20:31:05 +02:00
setVADHandler(handler: VoiceActivityDetector) {
2018-03-08 15:40:31 +01:00
if(this.vadHandler) {
this.vadHandler.changeHandle(null, true);
this.vadHandler.finalize();
}
2018-08-12 22:31:40 +02:00
2018-03-07 19:06:52 +01:00
this.vadHandler = handler;
2018-03-08 15:40:31 +01:00
this.vadHandler.changeHandle(this, false);
if(this.audioContext) {
this.vadHandler.initialise();
if(this.microphoneStream)
this.vadHandler.initialiseNewStream(undefined, this.microphoneStream);
}
2018-03-07 19:06:52 +01:00
}
getVADHandler() : VoiceActivityDetector {
return this.vadHandler;
}
2019-04-04 21:47:52 +02:00
set_recording(flag_enabled: boolean) {
if(this._recording == flag_enabled)
return;
if(flag_enabled)
this.start_recording(this._deviceId, this._deviceGroup);
else
this.stop_recording();
2018-03-07 19:06:52 +01:00
}
2019-04-04 21:47:52 +02:00
clean_recording_supported() { this._recording_supported = true; }
is_recording_supported() { return this._recording_supported; }
is_recording() { return this._recording; }
2018-08-12 22:31:40 +02:00
device_group_id() : string { return this._deviceGroup; }
device_id() : string { return this._deviceId; }
change_device(device: string, group: string) {
2018-06-19 20:31:05 +02:00
if(this._deviceId == device && this._deviceGroup == group) return;
2018-03-17 08:05:37 +01:00
this._deviceId = device;
2018-06-19 20:31:05 +02:00
this._deviceGroup = group;
settings.changeGlobal("microphone_device_id", device);
2018-08-12 22:31:40 +02:00
settings.changeGlobal("microphone_device_group", group);
2018-03-17 08:05:37 +01:00
if(this._recording) {
2019-04-04 21:47:52 +02:00
this.stop_recording();
this.start_recording(device, group);
2018-03-17 08:05:37 +01:00
}
}
2019-04-04 21:47:52 +02:00
start_recording(device: string, groupId: string){
2018-03-17 08:05:37 +01:00
this._deviceId = device;
2018-08-12 22:31:40 +02:00
this._deviceGroup = groupId;
console.log(tr("[VoiceRecorder] Start recording! (Device: %o | Group: %o)"), device, groupId);
2018-03-07 19:06:52 +01:00
this._recording = true;
2018-08-12 22:31:40 +02:00
//FIXME Implement that here for thew client as well
getUserMediaFunction()({
2018-06-19 20:31:05 +02:00
audio: {
2018-08-08 19:32:12 +02:00
deviceId: device,
groupId: groupId,
echoCancellation: true,
echoCancellationType: 'browser'
2018-06-19 20:31:05 +02:00
}
2018-03-17 08:05:37 +01:00
}, this.on_microphone.bind(this), error => {
2019-04-04 21:47:52 +02:00
this._recording = false;
if(this._recording_supported) {
this._recording_supported = false;
if(this.on_support_state_change)
this.on_support_state_change();
}
createErrorModal(tr("Could not resolve microphone!"), tr("Could not resolve microphone!<br>Message: ") + error).open();
console.error(tr("Could not get microphone!"));
2018-03-07 19:06:52 +01:00
console.error(error);
});
}
2019-04-04 21:47:52 +02:00
stop_recording(stop_media_stream: boolean = true){
console.log(tr("Stop recording!"));
2018-03-07 19:06:52 +01:00
this._recording = false;
if(this.microphoneStream) this.microphoneStream.disconnect();
this.microphoneStream = undefined;
2018-06-19 20:31:05 +02:00
2018-08-12 22:31:40 +02:00
if(stop_media_stream && this.mediaStream) {
2018-03-07 19:06:52 +01:00
if(this.mediaStream.stop)
this.mediaStream.stop();
else
this.mediaStream.getTracks().forEach(value => {
value.stop();
});
2018-08-12 22:31:40 +02:00
this.mediaStream = undefined;
2018-03-07 19:06:52 +01:00
}
}
2019-04-04 21:47:52 +02:00
on_initialized(callback: () => any) {
if(this.processor)
callback();
else
this.on_ready_callbacks.push(callback);
}
2018-03-07 19:06:52 +01:00
private on_microphone(stream: MediaStream) {
2018-08-12 22:31:40 +02:00
const old_microphone_stream = this.microphoneStream;
if(old_microphone_stream)
2019-04-04 21:47:52 +02:00
this.stop_recording(this.mediaStream != stream); //Disconnect old stream
2018-03-07 19:06:52 +01:00
2018-06-19 20:31:05 +02:00
this.mediaStream = stream;
2019-04-04 21:47:52 +02:00
if(!this.mediaStream)
return;
2018-08-12 22:31:40 +02:00
if(!this.audioContext) {
console.log(tr("[VoiceRecorder] Got microphone stream, but havn't a audio context. Waiting until its initialized"));
2018-08-12 22:31:40 +02:00
return;
}
2018-08-10 15:16:35 +02:00
2018-03-07 19:06:52 +01:00
this.microphoneStream = this.audioContext.createMediaStreamSource(stream);
this.microphoneStream.connect(this.processor);
2018-08-12 22:31:40 +02:00
if(this.vadHandler)
this.vadHandler.initialiseNewStream(old_microphone_stream, this.microphoneStream);
2019-04-04 21:47:52 +02:00
if(!this._recording_supported) {
this._recording_supported = true;
if(this.on_support_state_change)
this.on_support_state_change();
}
}
private on_voice_start() {
this._tag_favicon.attr('href', "img/favicon/speaking.png");
if(this.on_start)
this.on_start();
}
private on_voice_end() {
this._tag_favicon.attr('href', "img/favicon/teacup.png");
if(this.on_end)
this.on_end();
2018-03-07 19:06:52 +01:00
}
}
class MuteVAD extends VoiceActivityDetector {
shouldRecord(buffer: AudioBuffer): boolean {
return false;
}
}
class PassThroughVAD extends VoiceActivityDetector {
shouldRecord(buffer: AudioBuffer): boolean {
return true;
}
}
2018-03-08 15:40:31 +01:00
class VoiceActivityDetectorVAD extends VoiceActivityDetector {
analyzer: AnalyserNode;
buffer: Uint8Array;
continuesCount: number = 0;
maxContinuesCount: number = 12;
percentageThreshold: number = 50;
percentage_listener: (per: number) => void = ($) => {};
initialise() {
this.analyzer = audio.player.context().createAnalyser();
2018-03-08 15:40:31 +01:00
this.analyzer.smoothingTimeConstant = 1; //TODO test
this.buffer = new Uint8Array(this.analyzer.fftSize);
return super.initialise();
}
initialiseNewStream(old: MediaStreamAudioSourceNode, _new: MediaStreamAudioSourceNode): void {
if(this.analyzer)
this.analyzer.disconnect();
if(_new)
_new.connect(this.analyzer);
}
shouldRecord(buffer: AudioBuffer): boolean {
let usage = this.calculateUsage();
if($.isFunction(this.percentage_listener)) this.percentage_listener(usage);
if(usage >= this.percentageThreshold) {
this.continuesCount = 0;
} else this.continuesCount++;
return this.continuesCount < this.maxContinuesCount;
}
calculateUsage() : number {
let total = 0
,float
,rms;
this.analyzer.getByteTimeDomainData(this.buffer);
for(let index = 0; index < this.analyzer.fftSize; index++) {
float = ( this.buffer[index++] / 0x7f ) - 1;
total += (float * float);
}
rms = Math.sqrt(total / this.analyzer.fftSize);
let db = 20 * ( Math.log(rms) / Math.log(10) );
// sanity check
db = Math.max(-192, Math.min(db, 0));
let percentage = 100 + ( db * 1.92 );
return percentage;
}
}
2019-01-27 13:11:40 +01:00
interface PPTKeySettings extends ppt.KeyDescriptor {
version?: number;
2019-01-27 13:11:40 +01:00
delay: number;
}
2018-03-07 19:06:52 +01:00
class PushToTalkVAD extends VoiceActivityDetector {
2019-01-27 13:11:40 +01:00
private _settings: PPTKeySettings;
private _key_hook: ppt.KeyHook;
private _timeout: NodeJS.Timer;
2018-03-07 19:06:52 +01:00
private _pushed: boolean = false;
2019-01-27 13:11:40 +01:00
constructor(settings: PPTKeySettings) {
2018-03-07 19:06:52 +01:00
super();
2019-01-27 13:11:40 +01:00
this._settings = settings;
this._key_hook = {
callback_release: () => {
if(this._timeout)
clearTimeout(this._timeout);
2019-01-27 13:11:40 +01:00
if(this._settings.delay > 0)
this._timeout = setTimeout(() => this._pushed = false, this._settings.delay);
else
this._pushed = false;
},
callback_press: () => {
if(this._timeout)
clearTimeout(this._timeout);
this._pushed = true;
},
cancel: false
} as ppt.KeyHook;
2019-01-27 13:11:40 +01:00
this.initialize_hook();
2018-03-07 19:06:52 +01:00
}
2019-01-27 13:11:40 +01:00
private initialize_hook() {
this._key_hook.key_code = this._settings.key_code;
this._key_hook.key_alt = this._settings.key_alt;
this._key_hook.key_ctrl = this._settings.key_ctrl;
this._key_hook.key_shift = this._settings.key_shift;
this._key_hook.key_windows = this._settings.key_windows;
}
2018-03-07 19:06:52 +01:00
2018-03-08 15:40:31 +01:00
initialise() {
ppt.register_key_hook(this._key_hook);
2018-03-08 15:40:31 +01:00
return super.initialise();
2018-03-07 19:06:52 +01:00
}
finalize() {
ppt.unregister_key_hook(this._key_hook);
2018-03-07 19:06:52 +01:00
return super.finalize();
}
set pushed(flag: boolean) {
this._pushed = flag;
}
2019-01-27 13:11:40 +01:00
set settings(settings: PPTKeySettings) {
2018-11-18 11:32:16 +01:00
ppt.unregister_key_hook(this._key_hook);
2019-01-27 13:11:40 +01:00
this._settings = settings;
this.initialize_hook();
2018-03-07 19:06:52 +01:00
this._pushed = false;
2019-01-27 13:11:40 +01:00
2018-11-18 11:32:16 +01:00
ppt.register_key_hook(this._key_hook);
2018-03-07 19:06:52 +01:00
}
shouldRecord(buffer: AudioBuffer): boolean {
return this._pushed;
}
}