diff --git a/modules/renderer/audio/AudioFilter.ts b/modules/renderer/audio/AudioFilter.ts index 2a6704f..33eebf8 100644 --- a/modules/renderer/audio/AudioFilter.ts +++ b/modules/renderer/audio/AudioFilter.ts @@ -40,9 +40,9 @@ export class NThresholdFilter extends NativeFilter implements ThresholdFilter { readonly type: FilterType.THRESHOLD; private filter: audio.record.ThresholdConsumeFilter; - private _margin_frames: number = 25; /* 120ms */ - private _threshold: number = 50; - private _callback_level: any; + private marginFrames: number = 25; /* 120ms */ + private threshold: number = 50; + private callbackLevel: () => void; private _attack_smooth = 0; private _release_smooth = 0; @@ -54,14 +54,15 @@ export class NThresholdFilter extends NativeFilter implements ThresholdFilter { Object.defineProperty(this, 'callback_level', { get(): any { - return this._callback_level; + return this.callbackLevel; }, set(v: any): void { - if(v === this._callback_level) + if(v === this.callbackLevel) return; - this._callback_level = v; - if(this.filter) + this.callbackLevel = v; + if(this.filter) { this.filter.set_analyze_filter(v); + } }, enumerable: true, configurable: false, @@ -69,15 +70,15 @@ export class NThresholdFilter extends NativeFilter implements ThresholdFilter { } getMarginFrames(): number { - return this.filter ? this.filter.get_margin_time() * NThresholdFilter.frames_per_second : this._margin_frames; + return this.filter ? this.filter.get_margin_time() * NThresholdFilter.frames_per_second : this.marginFrames; } getThreshold(): number { - return this.filter ? this.filter.get_threshold() : this._threshold; + return this.filter ? this.filter.get_threshold() : this.threshold; } setMarginFrames(value: number) { - this._margin_frames = value; + this.marginFrames = value; if(this.filter) this.filter.set_margin_time(value / 960 / 1000); } @@ -105,7 +106,7 @@ export class NThresholdFilter extends NativeFilter implements ThresholdFilter { setThreshold(value: number): Promise { if(typeof(value) === "string") value = parseInt(value); /* yes... this happens */ - this._threshold = value; + this.threshold = value; if(this.filter) this.filter.set_threshold(value); return Promise.resolve(); @@ -113,35 +114,49 @@ export class NThresholdFilter extends NativeFilter implements ThresholdFilter { finalize() { if(this.filter) { - if(this.handle.getNativeConsumer()) + if(this.handle.getNativeConsumer()) { this.handle.getNativeConsumer().unregister_filter(this.filter); + } + this.filter = undefined; } } initialize() { const consumer = this.handle.getNativeConsumer(); - if(!consumer) + if(!consumer) { return; + } this.finalize(); - this.filter = consumer.create_filter_threshold(this._threshold); - if(this._callback_level) - this.filter.set_analyze_filter(this._callback_level); - this.filter.set_margin_time(this._margin_frames / NThresholdFilter.frames_per_second); + this.filter = consumer.create_filter_threshold(this.threshold); + this.filter.set_margin_time(this.marginFrames / NThresholdFilter.frames_per_second); this.filter.set_attack_smooth(this._attack_smooth); this.filter.set_release_smooth(this._release_smooth); + this.updateAnalyzeFilterCallback(); } registerLevelCallback(callback: (value: number) => void) { this.levelCallbacks.push(callback); + this.updateAnalyzeFilterCallback(); } removeLevelCallback(callback: (value: number) => void) { const index = this.levelCallbacks.indexOf(callback); - if(index === -1) return; + if(index === -1) { + return; + } this.levelCallbacks.splice(index, 1); + this.updateAnalyzeFilterCallback(); + } + + private updateAnalyzeFilterCallback() { + if(this.levelCallbacks.length > 0) { + this.filter.set_analyze_filter(value => this.levelCallbacks.forEach(callback => callback(value))); + } else { + this.filter.set_analyze_filter(undefined); + } } } diff --git a/modules/renderer/audio/AudioRecorder.ts b/modules/renderer/audio/AudioRecorder.ts index b83df0b..dd0502c 100644 --- a/modules/renderer/audio/AudioRecorder.ts +++ b/modules/renderer/audio/AudioRecorder.ts @@ -4,8 +4,9 @@ import { InputConsumer, InputConsumerType, InputEvents, + InputStartError, InputState, - LevelMeter, MediaStreamRequestResult + LevelMeter, } from "tc-shared/voice/RecorderBase"; import {audio} from "tc-native/connection"; import {tr} from "tc-shared/i18n/localize"; @@ -13,9 +14,9 @@ import {Registry} from "tc-shared/events"; import {Filter, FilterType, FilterTypeClass} from "tc-shared/voice/Filter"; import {NativeFilter, NStateFilter, NThresholdFilter, NVoiceLevelFilter} from "./AudioFilter"; import {IDevice} from "tc-shared/audio/recorder"; -import {LogCategory, logWarn} from "tc-shared/log"; -import NativeFilterMode = audio.record.FilterMode; +import {LogCategory, logTrace, logWarn} from "tc-shared/log"; import {Settings, settings} from "tc-shared/settings"; +import NativeFilterMode = audio.record.FilterMode; export class NativeInput implements AbstractInput { static readonly instances = [] as NativeInput[]; @@ -59,19 +60,30 @@ export class NativeInput implements AbstractInput { } } - async start(): Promise { - if(this.state === InputState.RECORDING) { - logWarn(LogCategory.VOICE, tr("Tried to start an input recorder twice.")); - return MediaStreamRequestResult.EBUSY; + private setState(newState: InputState) { + if(this.state === newState) { + return; } - this.state = InputState.INITIALIZING; + const oldState = this.state; + this.state = newState; + this.events.fire("notify_state_changed", { oldState, newState }); + } + + async start(): Promise { + if(this.state !== InputState.PAUSED) { + logWarn(LogCategory.VOICE, tr("Input isn't paused")); + return InputStartError.EBUSY; + } + + this.setState(InputState.INITIALIZING); + logTrace(LogCategory.AUDIO, tr("Starting input for device %o", this.deviceId)); try { const state = await new Promise(resolve => this.nativeHandle.set_device(this.deviceId, resolve)); if(state !== "success") { if(state === "invalid-device") { - return MediaStreamRequestResult.EDEVICEUNKNOWN; + return InputStartError.EDEVICEUNKNOWN; } else if(state === undefined) { throw tr("invalid set device result state"); } @@ -88,21 +100,28 @@ export class NativeInput implements AbstractInput { } })); - this.state = InputState.RECORDING; + this.setState(InputState.RECORDING); return true; } finally { + /* @ts-ignore Typescript isn't smart about awaits in try catch blocks */ if(this.state === InputState.INITIALIZING) { - this.state = InputState.PAUSED; + this.setState(InputState.PAUSED); } } } async stop(): Promise { - if(this.state === InputState.PAUSED) + if(this.state === InputState.PAUSED) { return; + } this.nativeHandle.stop(); - this.state = InputState.PAUSED; + this.setState(InputState.PAUSED); + + if(this.filtered) { + this.filtered = false; + this.events.fire("notify_voice_end"); + } } async setDeviceId(device: string | undefined): Promise { @@ -110,8 +129,15 @@ export class NativeInput implements AbstractInput { return; } + try { + await this.stop(); + } catch (error) { + logWarn(LogCategory.GENERAL, tr("Failed to stop microphone recording after device change: %o"), error); + } + + const oldDeviceId = this.deviceId; this.deviceId = device; - await this.stop(); + this.events.fire("notify_device_changed", { oldDeviceId, newDeviceId: device }); } currentDeviceId(): string | undefined { @@ -124,10 +150,12 @@ export class NativeInput implements AbstractInput { removeFilter(filter: Filter) { const index = this.registeredFilters.indexOf(filter as any); - if(index === -1) return; + if(index === -1) { + return; + } - const [ f ] = this.registeredFilters.splice(index, 1); - f.finalize(); + const [ registeredFilter ] = this.registeredFilters.splice(index, 1); + registeredFilter.finalize(); } createFilter(type: T, priority: number): FilterTypeClass { @@ -223,7 +251,13 @@ export class NativeInput implements AbstractInput { break; } + if(this.nativeConsumer.get_filter_mode() === nativeMode) { + return; + } + + const oldMode = this.getFilterMode(); this.nativeConsumer.set_filter_mode(nativeMode); + this.events.fire("notify_filter_mode_changed", { oldMode, newMode: mode }); } } @@ -262,13 +296,15 @@ export class NativeLevelMeter implements LevelMeter { }); }); } catch (error) { - if (typeof (error) === "string") + if (typeof (error) === "string") { throw error; - console.warn(tr("Failed to initialize levelmeter for device %o: %o"), this.targetDevice, error); + } + + logWarn(LogCategory.AUDIO, tr("Failed to initialize level meter for device %o: %o"), this.targetDevice, error); throw "initialize failed (lookup console)"; } - /* references this variable, needs a destory() call, else memory leak */ + /* references this variable, needs a destroy() call, else memory leak */ this.nativeFilter.set_analyze_filter(value => { if(this.callback) this.callback(value); }); diff --git a/modules/renderer/audio/InputDeviceList.ts b/modules/renderer/audio/InputDeviceList.ts index b1f72cd..f296497 100644 --- a/modules/renderer/audio/InputDeviceList.ts +++ b/modules/renderer/audio/InputDeviceList.ts @@ -4,6 +4,7 @@ import * as loader from "tc-loader"; import * as native from "tc-native/connection"; import {audio} from "tc-native/connection"; +import {LogCategory, logTrace} from "tc-shared/log"; interface NativeIDevice extends IDevice { isDefault: boolean @@ -39,7 +40,9 @@ class InputDeviceList extends AbstractDeviceList { return this.cachedDevices; } - this.cachedDevices = audio.available_devices() + const nativeDeviceList = audio.available_devices(); + logTrace(LogCategory.AUDIO, tr("Native device list: %o"), nativeDeviceList); + this.cachedDevices = nativeDeviceList .filter(e => e.input_supported || e.input_default) .filter(e => e.driver !== "Windows WDM-KS") /* If we're using WDM-KS and opening the microphone view, for some reason the channels get blocked an never release.... */ .map(device => { diff --git a/modules/renderer/connection/VoiceConnection.ts b/modules/renderer/connection/VoiceConnection.ts index fb547bb..1343e3b 100644 --- a/modules/renderer/connection/VoiceConnection.ts +++ b/modules/renderer/connection/VoiceConnection.ts @@ -93,6 +93,7 @@ export class NativeVoiceConnectionWrapper extends AbstractVoiceConnection { } await recorder?.unmount(); + const oldRecorder = recorder; this.currentRecorder = recorder; try { @@ -117,7 +118,10 @@ export class NativeVoiceConnectionWrapper extends AbstractVoiceConnection { this.currentRecorder = undefined; throw error; } - this.events.fire("notify_recorder_changed", {}); + this.events.fire("notify_recorder_changed", { + oldRecorder, + newRecorder: recorder + }); } voiceRecorder(): RecorderProfile { diff --git a/native/serverconnection/exports/exports.d.ts b/native/serverconnection/exports/exports.d.ts index 5a3417e..74efbf2 100644 --- a/native/serverconnection/exports/exports.d.ts +++ b/native/serverconnection/exports/exports.d.ts @@ -228,7 +228,6 @@ declare module "tc-native/connection" { readonly channelCount: number; readonly frameSize: number; - /* TODO add some kind of order to may improve CPU performance (Some filters are more intense then others) */ get_filters() : ConsumeFilter[]; unregister_filter(filter: ConsumeFilter); diff --git a/package.json b/package.json index a199095..792db59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "TeaClient", - "version": "1.5.0-6", + "version": "1.5.0-7", "description": "", "main": "main.js", "scripts": {