Some audio fixes
This commit is contained in:
parent
93b4521673
commit
f3add08a92
@ -121,6 +121,6 @@ function deploy_client() {
|
||||
|
||||
#install_npm
|
||||
#compile_scripts
|
||||
compile_native
|
||||
package_client
|
||||
#compile_native
|
||||
#package_client
|
||||
deploy_client
|
||||
|
@ -20,9 +20,9 @@ import {Arguments, process_args} from "../../shared/process-arguments";
|
||||
import * as electron from "electron";
|
||||
import {PassThrough} from "stream";
|
||||
import ErrnoException = NodeJS.ErrnoException;
|
||||
import * as winmgr from "../window";
|
||||
import {reference_app} from "../main_window";
|
||||
import * as url from "url";
|
||||
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
|
||||
|
||||
const is_debug = false;
|
||||
export function server_url() : string {
|
||||
@ -697,9 +697,10 @@ export async function execute_graphical(channel: string, ask_install: boolean) :
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
await new Promise(resolve => window.on('ready-to-show', resolve));
|
||||
await loadWindowBounds('update-installer', window);
|
||||
startTrackWindowBounds('update-installer', window);
|
||||
|
||||
window.show();
|
||||
await winmgr.apply_bounds('update-installer', window);
|
||||
winmgr.track_bounds('update-installer', window);
|
||||
|
||||
const current_vers = await current_version();
|
||||
console.log("Current version: " + current_vers.toString(true));
|
||||
|
@ -1,6 +1,4 @@
|
||||
import {BrowserWindow, Menu, MenuItem, MessageBoxOptions, app, dialog} from "electron";
|
||||
import * as electron from "electron";
|
||||
import * as winmgr from "./window";
|
||||
import * as path from "path";
|
||||
|
||||
let app_references = 0;
|
||||
@ -21,6 +19,7 @@ import * as updater from "./app-updater";
|
||||
import * as loader from "./ui-loader";
|
||||
import * as crash_handler from "../crash_handler";
|
||||
import * as url from "url";
|
||||
import {loadWindowBounds, startTrackWindowBounds} from "../shared/window";
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
@ -62,8 +61,8 @@ function spawn_main_window(entry_point: string) {
|
||||
|
||||
main_window.once('ready-to-show', () => {
|
||||
main_window.show();
|
||||
winmgr.apply_bounds('main-window', main_window).then(() => {
|
||||
winmgr.track_bounds('main-window', main_window);
|
||||
loadWindowBounds('main-window', main_window).then(() => {
|
||||
startTrackWindowBounds('main-window', main_window);
|
||||
|
||||
main_window.focus();
|
||||
loader.ui.cleanup();
|
||||
|
@ -5,8 +5,8 @@ import {screen} from "electron";
|
||||
import {Arguments, process_args} from "../../shared/process-arguments";
|
||||
import * as loader from "./loader";
|
||||
import * as updater from "../app-updater";
|
||||
import * as winmgr from "../window";
|
||||
import * as url from "url";
|
||||
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
|
||||
|
||||
export namespace ui {
|
||||
let gui: electron.BrowserWindow;
|
||||
@ -124,8 +124,9 @@ export namespace ui {
|
||||
console.log("Setting UI position to %ox%o", x, y);
|
||||
if(typeof x === "number" && typeof y === "number")
|
||||
gui.setPosition(x, y);
|
||||
winmgr.apply_bounds('ui-load-window', gui, undefined, { apply_size: false }).then(() => {
|
||||
winmgr.track_bounds('ui-load-window', gui);
|
||||
|
||||
loadWindowBounds('ui-load-window', gui, undefined, { applySize: false }).then(() => {
|
||||
startTrackWindowBounds('ui-load-window', gui);
|
||||
|
||||
const call_loader = () => load_files().catch(reject);
|
||||
if(!process_args.has_flag(...Arguments.DISABLE_ANIMATION))
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
import * as winmgr from "../window";
|
||||
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
|
||||
|
||||
let global_window: electron.BrowserWindow;
|
||||
let global_window_promise: Promise<void>;
|
||||
@ -49,8 +49,8 @@ export async function open_preview(url: string) {
|
||||
});
|
||||
|
||||
try {
|
||||
await winmgr.apply_bounds('url-preview', global_window);
|
||||
winmgr.track_bounds('url-preview', global_window);
|
||||
await loadWindowBounds('url-preview', global_window);
|
||||
startTrackWindowBounds('url-preview', global_window);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject("timeout"), 5000);
|
||||
|
@ -1,15 +1,15 @@
|
||||
import {AbstractExternalModalController} from "tc-shared/ui/react-elements/external-modal/Controller";
|
||||
import {setExternalModalControllerFactory} from "tc-shared/ui/react-elements/external-modal";
|
||||
import * as ipc from "tc-shared/ipc/BrowserIPC";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {BrowserWindow, remote} from "electron";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
import * as path from "path";
|
||||
import {Arguments, process_args} from "../shared/process-arguments";
|
||||
import {Popout2ControllerMessages, PopoutIPCMessage} from "tc-shared/ui/react-elements/external-modal/IPCMessage";
|
||||
import {loadWindowBounds, startTrackWindowBounds} from "../shared/window";
|
||||
|
||||
class ExternalModalController extends AbstractExternalModalController {
|
||||
export class ExternalModalController extends AbstractExternalModalController {
|
||||
private window: BrowserWindow;
|
||||
|
||||
constructor(a, b, c) {
|
||||
@ -26,21 +26,33 @@ class ExternalModalController extends AbstractExternalModalController {
|
||||
autoHideMenuBar: true,
|
||||
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
},
|
||||
icon: path.join(__dirname, "..", "..", "resources", "logo.ico"),
|
||||
minWidth: 600,
|
||||
minHeight: 300
|
||||
minHeight: 300,
|
||||
|
||||
frame: false,
|
||||
transparent: true,
|
||||
|
||||
show: true
|
||||
});
|
||||
|
||||
loadWindowBounds("modal-" + this.modalType, this.window).then(() => {
|
||||
startTrackWindowBounds("modal-" + this.modalType, this.window);
|
||||
});
|
||||
|
||||
if(process_args.has_flag(Arguments.DEV_TOOLS))
|
||||
this.window.webContents.openDevTools();
|
||||
|
||||
const parameters = {
|
||||
"loader-target": "manifest",
|
||||
"chunk": "modal-external",
|
||||
"modal-target": this.modalType,
|
||||
"ipc-channel": this.ipcChannel.channelId,
|
||||
"ipc-address": ipc.getInstance().getLocalAddress(),
|
||||
//"disableGlobalContextMenu": is_debug ? 1 : 0,
|
||||
//"loader-abort": is_debug ? 1 : 0,
|
||||
"loader-abort": 0,
|
||||
"animation-short": 1
|
||||
};
|
||||
|
||||
const baseUrl = location.origin + location.pathname + "?";
|
||||
@ -54,7 +66,6 @@ class ExternalModalController extends AbstractExternalModalController {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.window.show();
|
||||
this.window.on("closed", () => {
|
||||
this.window = undefined;
|
||||
this.handleWindowClosed();
|
||||
@ -73,12 +84,26 @@ class ExternalModalController extends AbstractExternalModalController {
|
||||
protected focusWindow(): void {
|
||||
this.window?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
priority: 50,
|
||||
name: "external modal controller factory setup",
|
||||
function: async () => {
|
||||
setExternalModalControllerFactory((modal, events, userData) => new ExternalModalController(modal, events, userData));
|
||||
protected handleTypedIPCMessage<T extends Popout2ControllerMessages>(type: T, payload: PopoutIPCMessage[T]) {
|
||||
super.handleTypedIPCMessage(type, payload);
|
||||
|
||||
switch (type) {
|
||||
case "invoke-modal-action":
|
||||
const data = payload as PopoutIPCMessage["invoke-modal-action"];
|
||||
switch (data.action) {
|
||||
case "close":
|
||||
this.destroy();
|
||||
break;
|
||||
|
||||
case "minimize":
|
||||
this.window?.minimize();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case "hello-popout":
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
60
modules/renderer/UnloadHandler.ts
Normal file
60
modules/renderer/UnloadHandler.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
import {Arguments, process_args} from "../shared/process-arguments";
|
||||
import {remote} from "electron";
|
||||
|
||||
const unloadListener = event => {
|
||||
if(settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG))
|
||||
return;
|
||||
|
||||
const active_connections = server_connections.all_connections().filter(e => e.connected);
|
||||
if(active_connections.length == 0) return;
|
||||
|
||||
const do_exit = (closeWindow: boolean) => {
|
||||
const dp = server_connections.all_connections().map(e => {
|
||||
if(e.serverConnection.connected())
|
||||
return e.serverConnection.disconnect(tr("client closed"))
|
||||
.catch(error => {
|
||||
console.warn(tr("Failed to disconnect from server %s on client close: %o"),
|
||||
e.serverConnection.remote_address().host + ":" + e.serverConnection.remote_address().port,
|
||||
error
|
||||
);
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
if(closeWindow) {
|
||||
const exit = () => {
|
||||
const {remote} = window.require('electron');
|
||||
remote.getCurrentWindow().close();
|
||||
};
|
||||
|
||||
Promise.all(dp).then(exit);
|
||||
/* force exit after 2500ms */
|
||||
setTimeout(exit, 2500);
|
||||
}
|
||||
};
|
||||
|
||||
if(process_args.has_flag(Arguments.DEBUG)) {
|
||||
do_exit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'question',
|
||||
buttons: ['Yes', 'No'],
|
||||
title: 'Confirm',
|
||||
message: 'Are you really sure?\nYou\'re still connected!'
|
||||
}).then(result => {
|
||||
if(result.response === 0) {
|
||||
/* prevent quitting because we try to disconnect */
|
||||
window.removeEventListener("beforeunload", unloadListener);
|
||||
do_exit(true);
|
||||
}
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", unloadListener);
|
243
modules/renderer/audio/AudioFilter.ts
Normal file
243
modules/renderer/audio/AudioFilter.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import {audio} from "tc-native/connection";
|
||||
import {FilterType, StateFilter, ThresholdFilter, VoiceLevelFilter} from "tc-shared/voice/Filter";
|
||||
import {NativeInput} from "./AudioRecorder";
|
||||
|
||||
export abstract class NativeFilter {
|
||||
readonly priority: number;
|
||||
|
||||
handle: NativeInput;
|
||||
enabled: boolean = false;
|
||||
|
||||
protected constructor(handle, priority: number) {
|
||||
this.handle = handle;
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
abstract initialize();
|
||||
abstract finalize();
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
setEnabled(flag: boolean): void {
|
||||
if(this.enabled === flag)
|
||||
return;
|
||||
|
||||
this.enabled = flag;
|
||||
|
||||
if(this.enabled) {
|
||||
this.initialize();
|
||||
} else {
|
||||
this.finalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NThresholdFilter extends NativeFilter implements ThresholdFilter {
|
||||
static readonly frames_per_second = 1 / (960 / 48000);
|
||||
|
||||
readonly type: FilterType.THRESHOLD;
|
||||
private filter: audio.record.ThresholdConsumeFilter;
|
||||
|
||||
private _margin_frames: number = 25; /* 120ms */
|
||||
private _threshold: number = 50;
|
||||
private _callback_level: any;
|
||||
|
||||
private _attack_smooth = 0;
|
||||
private _release_smooth = 0;
|
||||
|
||||
private levelCallbacks: ((level: number) => void)[] = [];
|
||||
|
||||
constructor(handle, priority: number) {
|
||||
super(handle, priority);
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
getMarginFrames(): number {
|
||||
return this.filter ? this.filter.get_margin_time() * NThresholdFilter.frames_per_second : this._margin_frames;
|
||||
}
|
||||
|
||||
getThreshold(): number {
|
||||
return this.filter ? this.filter.get_threshold() : this._threshold;
|
||||
}
|
||||
|
||||
setMarginFrames(value: number) {
|
||||
this._margin_frames = value;
|
||||
if(this.filter)
|
||||
this.filter.set_margin_time(value / 960 / 1000);
|
||||
}
|
||||
|
||||
getAttackSmooth(): number {
|
||||
return this.filter ? this.filter.get_attack_smooth() : this._attack_smooth;
|
||||
}
|
||||
|
||||
getReleaseSmooth(): number {
|
||||
return this.filter ? this.filter.get_release_smooth() : this._release_smooth;
|
||||
}
|
||||
|
||||
setAttackSmooth(value: number) {
|
||||
this._attack_smooth = value;
|
||||
if(this.filter)
|
||||
this.filter.set_attack_smooth(value);
|
||||
}
|
||||
|
||||
setReleaseSmooth(value: number) {
|
||||
this._release_smooth = value;
|
||||
if(this.filter)
|
||||
this.filter.set_release_smooth(value);
|
||||
}
|
||||
|
||||
setThreshold(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.getNativeConsumer())
|
||||
this.handle.getNativeConsumer().unregister_filter(this.filter);
|
||||
this.filter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
const consumer = this.handle.getNativeConsumer();
|
||||
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.set_attack_smooth(this._attack_smooth);
|
||||
this.filter.set_release_smooth(this._release_smooth);
|
||||
}
|
||||
|
||||
registerLevelCallback(callback: (value: number) => void) {
|
||||
this.levelCallbacks.push(callback);
|
||||
}
|
||||
|
||||
removeLevelCallback(callback: (value: number) => void) {
|
||||
const index = this.levelCallbacks.indexOf(callback);
|
||||
if(index === -1) return;
|
||||
|
||||
this.levelCallbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export class NStateFilter extends NativeFilter implements StateFilter {
|
||||
readonly type: FilterType.STATE;
|
||||
private filter: audio.record.StateConsumeFilter;
|
||||
private active = false;
|
||||
|
||||
constructor(handle, priority: number) {
|
||||
super(handle, priority);
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this.filter) {
|
||||
const consumer = this.handle.getNativeConsumer();
|
||||
consumer?.unregister_filter(this.filter);
|
||||
this.filter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
const consumer = this.handle.getNativeConsumer();
|
||||
if(!consumer)
|
||||
return;
|
||||
|
||||
this.finalize();
|
||||
this.filter = consumer.create_filter_state();
|
||||
this.filter.set_consuming(this.active);
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
setState(state: boolean) {
|
||||
if(this.active === state)
|
||||
return;
|
||||
this.active = state;
|
||||
if(this.filter) {
|
||||
this.filter.set_consuming(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NVoiceLevelFilter extends NativeFilter implements VoiceLevelFilter {
|
||||
static readonly frames_per_second = 1 / (960 / 48000);
|
||||
|
||||
readonly type: FilterType.VOICE_LEVEL;
|
||||
private filter: audio.record.VADConsumeFilter;
|
||||
private level = 3;
|
||||
private _margin_frames = 6;
|
||||
|
||||
constructor(handle, priority: number) {
|
||||
super(handle, priority);
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this.filter) {
|
||||
const consumer = this.handle.getNativeConsumer();
|
||||
consumer?.unregister_filter(this.filter);
|
||||
this.filter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
const consumer = this.handle.getNativeConsumer();
|
||||
if(!consumer)
|
||||
return;
|
||||
|
||||
this.finalize();
|
||||
this.filter = consumer.create_filter_vad(this.level);
|
||||
this.filter.set_margin_time(this._margin_frames / NVoiceLevelFilter.frames_per_second);
|
||||
}
|
||||
|
||||
getLevel(): number {
|
||||
return this.level;
|
||||
}
|
||||
|
||||
setLevel(value: number) {
|
||||
if(this.level === value)
|
||||
return;
|
||||
|
||||
this.level = value;
|
||||
if(this.filter) {
|
||||
this.finalize();
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
setMarginFrames(value: number) {
|
||||
this._margin_frames = value;
|
||||
if(this.filter)
|
||||
this.filter.set_margin_time(value / NVoiceLevelFilter.frames_per_second);
|
||||
}
|
||||
|
||||
getMarginFrames(): number {
|
||||
return this.filter ? this.filter.get_margin_time() * NVoiceLevelFilter.frames_per_second : this._margin_frames;
|
||||
}
|
||||
}
|
@ -1,445 +1,188 @@
|
||||
import {
|
||||
filter,
|
||||
AbstractInput,
|
||||
InputDevice,
|
||||
InputState,
|
||||
InputConsumer,
|
||||
InputConsumerType, InputStartResult, LevelMeter
|
||||
InputConsumerType,
|
||||
InputEvents,
|
||||
InputStartResult,
|
||||
InputState,
|
||||
LevelMeter
|
||||
} from "tc-shared/voice/RecorderBase";
|
||||
import {audio} from "tc-native/connection";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
|
||||
interface NativeDevice extends InputDevice {
|
||||
device_index: number;
|
||||
native: any;
|
||||
}
|
||||
|
||||
let _device_cache: NativeDevice[] = undefined;
|
||||
export function devices() : InputDevice[] {
|
||||
//TODO: Handle device updates!
|
||||
if(!audio.initialized()) return [];
|
||||
|
||||
return _device_cache || (_device_cache = audio.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 filters {
|
||||
export abstract class NativeFilter implements filter.Filter {
|
||||
type: 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 filter.ThresholdFilter {
|
||||
static readonly frames_per_second = 1 / (960 / 48000);
|
||||
private filter: audio.record.ThresholdConsumeFilter;
|
||||
|
||||
private _margin_frames: number = 25; /* 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, 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_time() * NThresholdFilter.frames_per_second : 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_time(value / 960 / 1000);
|
||||
}
|
||||
|
||||
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_time(this._margin_frames / NThresholdFilter.frames_per_second);
|
||||
this.filter.set_attack_smooth(this._attack_smooth);
|
||||
this.filter.set_release_smooth(this._release_smooth);
|
||||
}
|
||||
}
|
||||
|
||||
export class NStateFilter extends NativeFilter implements filter.StateFilter {
|
||||
private filter: audio.record.StateConsumeFilter;
|
||||
private active = false;
|
||||
|
||||
constructor(handle) {
|
||||
super(handle, 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 filter.VoiceLevelFilter {
|
||||
static readonly frames_per_second = 1 / (960 / 48000);
|
||||
private filter: audio.record.VADConsumeFilter;
|
||||
private level = 3;
|
||||
private _margin_frames = 6;
|
||||
|
||||
constructor(handle) {
|
||||
super(handle, 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_time(this._margin_frames / NVoiceLevelFilter.frames_per_second);
|
||||
}
|
||||
|
||||
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_time(value / NVoiceLevelFilter.frames_per_second);
|
||||
}
|
||||
|
||||
get_margin_frames(): number {
|
||||
return this.filter ? this.filter.get_margin_time() * NVoiceLevelFilter.frames_per_second : this._margin_frames;
|
||||
}
|
||||
}
|
||||
}
|
||||
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";
|
||||
|
||||
export class NativeInput implements AbstractInput {
|
||||
private handle: audio.record.AudioRecorder;
|
||||
consumer: audio.record.AudioConsumer;
|
||||
readonly events: Registry<InputEvents>;
|
||||
|
||||
private _current_device: InputDevice;
|
||||
private _current_state: InputState = InputState.PAUSED;
|
||||
private nativeHandle: audio.record.AudioRecorder;
|
||||
private nativeConsumer: audio.record.AudioConsumer;
|
||||
|
||||
callback_begin: () => any;
|
||||
callback_end: () => any;
|
||||
private state: InputState;
|
||||
private deviceId: string | undefined;
|
||||
|
||||
private filters: filters.NativeFilter[] = [];
|
||||
private registeredFilters: (Filter & NativeFilter)[] = [];
|
||||
private filtered = false;
|
||||
|
||||
constructor() {
|
||||
this.handle = audio.record.create_recorder();
|
||||
this.events = new Registry<InputEvents>();
|
||||
|
||||
this.consumer = this.handle.create_consumer();
|
||||
this.consumer.callback_ended = () => {
|
||||
if(this._current_state !== InputState.RECORDING)
|
||||
return;
|
||||
this.nativeHandle = audio.record.create_recorder();
|
||||
|
||||
this._current_state = InputState.DRY;
|
||||
if(this.callback_end)
|
||||
this.callback_end();
|
||||
this.nativeConsumer = this.nativeHandle.create_consumer();
|
||||
this.nativeConsumer.callback_ended = () => {
|
||||
this.filtered = true;
|
||||
this.events.fire("notify_voice_end");
|
||||
};
|
||||
this.consumer.callback_started = () => {
|
||||
if(this._current_state !== InputState.DRY)
|
||||
return;
|
||||
|
||||
this._current_state = InputState.RECORDING;
|
||||
if(this.callback_begin)
|
||||
this.callback_begin();
|
||||
this.nativeConsumer.callback_started = () => {
|
||||
this.filtered = false;
|
||||
this.events.fire("notify_voice_start");
|
||||
};
|
||||
|
||||
this._current_state = InputState.PAUSED;
|
||||
this.state = InputState.PAUSED;
|
||||
}
|
||||
|
||||
/* TODO: some kind of finalize? */
|
||||
current_consumer(): InputConsumer | undefined {
|
||||
return {
|
||||
type: InputConsumerType.NATIVE
|
||||
};
|
||||
async start(): Promise<InputStartResult> {
|
||||
if(this.state === InputState.RECORDING) {
|
||||
logWarn(LogCategory.VOICE, tr("Tried to start an input recorder twice."));
|
||||
return InputStartResult.EOK;
|
||||
}
|
||||
|
||||
async set_consumer(consumer: InputConsumer): Promise<void> {
|
||||
if(typeof(consumer) !== "undefined")
|
||||
throw "we only support native consumers!"; /* TODO: May create a general wrapper? */
|
||||
return;
|
||||
}
|
||||
|
||||
async set_device(_device: InputDevice | undefined): Promise<void> {
|
||||
if(_device === this._current_device)
|
||||
return;
|
||||
|
||||
this._current_device = _device;
|
||||
this.state = InputState.INITIALIZING;
|
||||
try {
|
||||
await new Promise(resolve => this.handle.set_device(this._current_device ? this._current_device.unique_id : undefined, resolve));
|
||||
if(this._current_state !== InputState.PAUSED && this._current_device)
|
||||
await new Promise((resolve, reject) => {
|
||||
this.handle.start(flag => {
|
||||
if(typeof flag === "boolean" && flag)
|
||||
const state = await new Promise<audio.record.DeviceSetResult>(resolve => this.nativeHandle.set_device(this.deviceId, resolve));
|
||||
|
||||
if(state !== "success") {
|
||||
if(state === "invalid-device") {
|
||||
return InputStartResult.EDEVICEUNKNOWN;
|
||||
} else if(state === undefined) {
|
||||
throw tr("invalid set device result state");
|
||||
}
|
||||
throw state;
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => this.nativeHandle.start(result => {
|
||||
if(result === true) {
|
||||
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;
|
||||
} else {
|
||||
reject(typeof result === "string" ? result : tr("failed to start input"));
|
||||
}
|
||||
}));
|
||||
|
||||
this.state = InputState.RECORDING;
|
||||
return InputStartResult.EOK;
|
||||
} finally {
|
||||
if(this.state === InputState.INITIALIZING) {
|
||||
this.state = InputState.PAUSED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current_device(): InputDevice | undefined {
|
||||
return this._current_device;
|
||||
async stop(): Promise<void> {
|
||||
if(this.state === InputState.PAUSED)
|
||||
return;
|
||||
|
||||
this.nativeHandle.stop();
|
||||
this.state = InputState.PAUSED;
|
||||
}
|
||||
|
||||
current_state(): InputState {
|
||||
return this._current_state;
|
||||
async setDeviceId(device: string | undefined): Promise<void> {
|
||||
if(this.deviceId === device)
|
||||
return;
|
||||
|
||||
this.deviceId = device;
|
||||
await this.stop();
|
||||
}
|
||||
|
||||
disable_filter(type: filter.Type) {
|
||||
const filter = this.get_filter(type) as filters.NativeFilter;
|
||||
if(filter.is_enabled())
|
||||
filter.enabled = false;
|
||||
filter.finalize();
|
||||
currentDeviceId(): string | undefined {
|
||||
return this.deviceId;
|
||||
}
|
||||
|
||||
enable_filter(type: filter.Type) {
|
||||
const filter = this.get_filter(type) as filters.NativeFilter;
|
||||
if(!filter.is_enabled()) {
|
||||
filter.enabled = true;
|
||||
filter.initialize();
|
||||
}
|
||||
isFiltered(): boolean {
|
||||
return this.filtered;
|
||||
}
|
||||
|
||||
clear_filter() {
|
||||
for(const filter of this.filters) {
|
||||
filter.enabled = false;
|
||||
filter.finalize();
|
||||
}
|
||||
removeFilter(filter: Filter) {
|
||||
const index = this.registeredFilters.indexOf(filter as any);
|
||||
if(index === -1) return;
|
||||
|
||||
const [ f ] = this.registeredFilters.splice(index, 1);
|
||||
f.finalize();
|
||||
}
|
||||
|
||||
get_filter(type: filter.Type): filter.Filter | undefined {
|
||||
for(const filter of this.filters)
|
||||
if(filter.type === type)
|
||||
createFilter<T extends FilterType>(type: T, priority: number): FilterTypeClass<T> {
|
||||
let filter;
|
||||
switch (type) {
|
||||
case FilterType.STATE:
|
||||
filter = new NStateFilter(this, priority);
|
||||
break;
|
||||
|
||||
case FilterType.THRESHOLD:
|
||||
filter = new NThresholdFilter(this, priority);
|
||||
break;
|
||||
|
||||
case FilterType.VOICE_LEVEL:
|
||||
filter = new NVoiceLevelFilter(this, priority);
|
||||
break;
|
||||
}
|
||||
|
||||
this.registeredFilters.push(filter);
|
||||
return filter;
|
||||
|
||||
let _filter: filters.NativeFilter;
|
||||
switch (type) {
|
||||
case filter.Type.THRESHOLD:
|
||||
_filter = new filters.NThresholdFilter(this);
|
||||
break;
|
||||
case filter.Type.STATE:
|
||||
_filter = new filters.NStateFilter(this);
|
||||
break;
|
||||
case filter.Type.VOICE_LEVEL:
|
||||
_filter = new filters.NVoiceLevelFilter(this);
|
||||
break;
|
||||
default:
|
||||
throw "this filter isn't supported!";
|
||||
}
|
||||
this.filters.push(_filter);
|
||||
return _filter;
|
||||
}
|
||||
|
||||
supports_filter(type: filter.Type) : boolean {
|
||||
supportsFilter(type: FilterType): boolean {
|
||||
switch (type) {
|
||||
case filter.Type.THRESHOLD:
|
||||
case filter.Type.STATE:
|
||||
case filter.Type.VOICE_LEVEL:
|
||||
case FilterType.VOICE_LEVEL:
|
||||
case FilterType.THRESHOLD:
|
||||
case FilterType.STATE:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<InputStartResult> {
|
||||
try {
|
||||
await this.stop();
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to stop old record session before start (%o)"), error);
|
||||
currentState(): InputState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
this._current_state = 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 InputStartResult.EOK;
|
||||
} catch(error) {
|
||||
this._current_state = InputState.PAUSED;
|
||||
throw error;
|
||||
currentConsumer(): InputConsumer | undefined {
|
||||
return {
|
||||
type: InputConsumerType.NATIVE
|
||||
};
|
||||
}
|
||||
|
||||
getNativeConsumer() : audio.record.AudioConsumer {
|
||||
return this.nativeConsumer;
|
||||
}
|
||||
|
||||
async setConsumer(consumer: InputConsumer): Promise<void> {
|
||||
if(typeof(consumer) !== "undefined")
|
||||
throw "we only support native consumers!"; // TODO: May create a general wrapper?
|
||||
return;
|
||||
}
|
||||
|
||||
setVolume(volume: number) {
|
||||
this.nativeHandle.set_volume(volume);
|
||||
}
|
||||
|
||||
getVolume(): number {
|
||||
return this.nativeHandle.get_volume();
|
||||
}
|
||||
}
|
||||
|
||||
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 = 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<LevelMeter> {
|
||||
const meter = new NativeLevelmenter(device as any);
|
||||
await meter.initialize();
|
||||
return meter;
|
||||
}
|
||||
|
||||
class NativeLevelmenter implements LevelMeter {
|
||||
readonly _device: NativeDevice;
|
||||
export class NativeLevelMeter implements LevelMeter {
|
||||
readonly _device: IDevice;
|
||||
|
||||
private _callback: (num: number) => any;
|
||||
private _recorder: audio.record.AudioRecorder;
|
||||
private _consumer: audio.record.AudioConsumer;
|
||||
private _filter: audio.record.ThresholdConsumeFilter;
|
||||
|
||||
constructor(device: NativeDevice) {
|
||||
constructor(device: IDevice) {
|
||||
this._device = device;
|
||||
}
|
||||
|
||||
@ -452,7 +195,7 @@ class NativeLevelmenter implements LevelMeter {
|
||||
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 => this._recorder.set_device(this._device.deviceId, resolve));
|
||||
await new Promise((resolve, reject) => {
|
||||
this._recorder.start(flag => {
|
||||
if (typeof flag === "boolean" && flag)
|
||||
@ -475,22 +218,25 @@ class NativeLevelmenter implements LevelMeter {
|
||||
});
|
||||
}
|
||||
|
||||
destory() {
|
||||
destroy() {
|
||||
if (this._filter) {
|
||||
this._filter.set_analyze_filter(undefined);
|
||||
this._consumer.unregister_filter(this._filter);
|
||||
}
|
||||
if (this._consumer)
|
||||
|
||||
if (this._consumer) {
|
||||
this._recorder.delete_consumer(this._consumer);
|
||||
}
|
||||
|
||||
if(this._recorder) {
|
||||
this._recorder.stop();
|
||||
this._recorder.set_device(undefined, () => {
|
||||
}); /* -1 := No device */
|
||||
}
|
||||
this._recorder = undefined;
|
||||
this._consumer = undefined;
|
||||
this._filter = undefined;
|
||||
}
|
||||
|
||||
device(): InputDevice {
|
||||
device(): IDevice {
|
||||
return this._device;
|
||||
}
|
||||
|
||||
|
69
modules/renderer/audio/InputDeviceList.ts
Normal file
69
modules/renderer/audio/InputDeviceList.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import {AbstractDeviceList, DeviceListEvents, IDevice, PermissionState} from "tc-shared/audio/recorder";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import * as loader from "tc-loader";
|
||||
|
||||
import {audio} from "tc-native/connection";
|
||||
|
||||
interface NativeIDevice extends IDevice {
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
class InputDeviceList extends AbstractDeviceList {
|
||||
private cachedDevices: NativeIDevice[];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.setPermissionState("granted");
|
||||
}
|
||||
|
||||
isRefreshAvailable(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
throw "not supported";
|
||||
}
|
||||
|
||||
async requestPermissions(): Promise<PermissionState> {
|
||||
return "granted";
|
||||
}
|
||||
|
||||
getDefaultDeviceId(): string {
|
||||
return this.getDevices().find(e => e.isDefault)?.deviceId || "default";
|
||||
}
|
||||
|
||||
getDevices(): NativeIDevice[] {
|
||||
if(this.cachedDevices)
|
||||
return this.cachedDevices;
|
||||
|
||||
this.cachedDevices = audio.available_devices()
|
||||
.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 => {
|
||||
return {
|
||||
deviceId: device.device_id,
|
||||
name: device.name,
|
||||
driver: device.driver,
|
||||
isDefault: device.input_default
|
||||
}
|
||||
});
|
||||
this.setState("healthy");
|
||||
return this.cachedDevices;
|
||||
}
|
||||
|
||||
getEvents(): Registry<DeviceListEvents> {
|
||||
return this.events;
|
||||
}
|
||||
}
|
||||
|
||||
export let inputDeviceList;
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
function: async () => {
|
||||
inputDeviceList = new InputDeviceList();
|
||||
inputDeviceList.getDevices();
|
||||
},
|
||||
priority: 80,
|
||||
name: "initialize input devices"
|
||||
});
|
@ -1,8 +0,0 @@
|
||||
import * as handler from "../../audio/AudioRecorder";
|
||||
|
||||
export const devices = handler.devices;
|
||||
export const device_refresh_available = handler.device_refresh_available;
|
||||
export const refresh_devices = handler.refresh_devices;
|
||||
|
||||
export const create_input = handler.create_input;
|
||||
export const create_levelmeter = handler.create_levelmeter;
|
@ -2,8 +2,7 @@ import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "tc-shared/conn
|
||||
import {
|
||||
AbstractServerConnection, CommandOptionDefaults, CommandOptions,
|
||||
ConnectionStateListener,
|
||||
ServerCommand,
|
||||
voice
|
||||
ServerCommand
|
||||
} from "tc-shared/connection/ConnectionBase";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
@ -13,8 +12,8 @@ import {ConnectionCommandHandler} from "tc-shared/connection/CommandHandler";
|
||||
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
|
||||
import {ServerAddress} from "tc-shared/ui/server";
|
||||
import {TeaSpeakHandshakeHandler} from "tc-shared/profiles/identities/TeamSpeakIdentity";
|
||||
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
|
||||
import {VoiceConnection} from "./VoiceConnection";
|
||||
import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection";
|
||||
|
||||
class ErrorCommandHandler extends AbstractCommandHandler {
|
||||
private _handle: ServerConnection;
|
||||
@ -236,7 +235,7 @@ export class ServerConnection extends AbstractServerConnection {
|
||||
return true;
|
||||
}
|
||||
|
||||
voice_connection(): AbstractVoiceConnection {
|
||||
getVoiceConnection(): AbstractVoiceConnection {
|
||||
return this._voice_connection;
|
||||
}
|
||||
|
||||
@ -303,17 +302,3 @@ export class NativeConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||
super(connection);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* override the "normal" connection */
|
||||
export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection {
|
||||
console.log("Spawning native connection");
|
||||
return new ServerConnection(handle); /* will be overridden by the client */
|
||||
}
|
||||
|
||||
export function destroy_server_connection(handle: AbstractServerConnection) {
|
||||
if(!(handle instanceof ServerConnection))
|
||||
throw "invalid handle";
|
||||
//TODO: Here!
|
||||
console.log("Call to destroy a server connection");
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
import {voice} from "tc-shared/connection/ConnectionBase";
|
||||
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
|
||||
import {ServerConnection} from "./ServerConnection";
|
||||
import {NativeVoiceConnection} from "tc-native/connection";
|
||||
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import * as log from "tc-shared/log";
|
||||
import VoiceClient = voice.VoiceClient;
|
||||
import LatencySettings = voice.LatencySettings;
|
||||
import {
|
||||
AbstractVoiceConnection,
|
||||
LatencySettings,
|
||||
VoiceClient,
|
||||
VoiceConnectionStatus
|
||||
} from "tc-shared/connection/VoiceConnection";
|
||||
import {NativeInput} from "../audio/AudioRecorder";
|
||||
|
||||
export class VoiceConnection extends AbstractVoiceConnection {
|
||||
@ -56,7 +58,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||
this.connection.client.update_voice_status(undefined);
|
||||
};
|
||||
|
||||
this.handle.set_audio_source((recorder.input as NativeInput).consumer);
|
||||
this.handle.set_audio_source((recorder.input as NativeInput).getNativeConsumer());
|
||||
}
|
||||
this.connection.client.update_voice_status(undefined);
|
||||
}
|
||||
@ -99,15 +101,15 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(LogCategory.VOICE, tr("Local voice started"));
|
||||
log.info(LogCategory.VOICE, tr("Local voice started (Native)"));
|
||||
this.handle.enable_voice_send(true);
|
||||
|
||||
const ch = chandler.getClient();
|
||||
if(ch) ch.speaking = true;
|
||||
}
|
||||
|
||||
connected(): boolean {
|
||||
return true; /* we cant be disconnected at any time! */
|
||||
getConnectionState(): VoiceConnectionStatus {
|
||||
return VoiceConnectionStatus.Connected;
|
||||
}
|
||||
|
||||
voice_recorder(): RecorderProfile {
|
||||
|
20
modules/renderer/hooks/AudioInput.ts
Normal file
20
modules/renderer/hooks/AudioInput.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {AudioRecorderBacked, DeviceList, IDevice, setRecorderBackend} from "tc-shared/audio/recorder";
|
||||
import {AbstractInput, LevelMeter} from "tc-shared/voice/RecorderBase";
|
||||
import {inputDeviceList} from "../audio/InputDeviceList";
|
||||
import {NativeInput, NativeLevelMeter} from "../audio/AudioRecorder";
|
||||
|
||||
setRecorderBackend(new class implements AudioRecorderBacked {
|
||||
createInput(): AbstractInput {
|
||||
return new NativeInput();
|
||||
}
|
||||
|
||||
async createLevelMeter(device: IDevice): Promise<LevelMeter> {
|
||||
const meter = new NativeLevelMeter(device);
|
||||
await meter.initialize();
|
||||
return meter;
|
||||
}
|
||||
|
||||
getDeviceList(): DeviceList {
|
||||
return inputDeviceList;
|
||||
}
|
||||
});
|
12
modules/renderer/hooks/ExternalModal.ts
Normal file
12
modules/renderer/hooks/ExternalModal.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {setExternalModalControllerFactory} from "tc-shared/ui/react-elements/external-modal";
|
||||
import {ExternalModalController} from "../ExternalModalHandler";
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
priority: 50,
|
||||
name: "external modal controller factory setup",
|
||||
function: async () => {
|
||||
setExternalModalControllerFactory((modal, events, userData) => new ExternalModalController(modal, events, userData));
|
||||
}
|
||||
});
|
25
modules/renderer/hooks/ServerConnection.ts
Normal file
25
modules/renderer/hooks/ServerConnection.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {ServerConnectionFactory, setServerConnectionFactory} from "tc-shared/connection/ConnectionFactory";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
|
||||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {ServerConnection} from "../connection/ServerConnection";
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
priority: 50,
|
||||
name: "server connection factory setup",
|
||||
function: async () => {
|
||||
setServerConnectionFactory(new class implements ServerConnectionFactory {
|
||||
create(client: ConnectionHandler): AbstractServerConnection {
|
||||
return new ServerConnection(client);
|
||||
}
|
||||
|
||||
destroy(instance: AbstractServerConnection) {
|
||||
if(!(instance instanceof ServerConnection))
|
||||
throw "invalid handle";
|
||||
|
||||
instance.finalize();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -28,8 +28,6 @@ declare global {
|
||||
impl_display_critical_error: any;
|
||||
displayCriticalError: any;
|
||||
teaclient_initialize: any;
|
||||
|
||||
open_connected_question: () => Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,8 +54,7 @@ loader.register_task(loader.Stage.INITIALIZING, {
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize logging",
|
||||
function: async () => {
|
||||
const logger = require("./logger");
|
||||
logger.setup();
|
||||
(await import("./Logger")).setup();
|
||||
},
|
||||
priority: 80
|
||||
});
|
||||
@ -100,19 +97,11 @@ loader.register_task(loader.Stage.INITIALIZING, {
|
||||
if(process_args.has_value(Arguments.DUMMY_CRASH_RENDERER))
|
||||
crash_handler.handler.crash();
|
||||
|
||||
if(!process_args.has_flag(Arguments.DEBUG)) {
|
||||
window.open_connected_question = () => remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'question',
|
||||
buttons: ['Yes', 'No'],
|
||||
title: 'Confirm',
|
||||
message: 'Are you really sure?\nYou\'re still connected!'
|
||||
}).then(result => result.response === 0);
|
||||
}
|
||||
|
||||
/* loader url setup */
|
||||
{
|
||||
const baseUrl = process_args.value(Arguments.SERVER_URL);
|
||||
if(typeof baseUrl === "string") {
|
||||
console.error(process_args.value(Arguments.UPDATER_UI_LOAD_TYPE));
|
||||
if(typeof baseUrl === "string" && parseFloat((process_args.value(Arguments.UPDATER_UI_LOAD_TYPE)?.toString() || "").trim()) === 3) {
|
||||
loader.config.baseUrl = baseUrl;
|
||||
}
|
||||
}
|
||||
@ -160,7 +149,12 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
await import("./SingleInstanceHandler");
|
||||
await import("./IconHelper");
|
||||
await import("./connection/FileTransfer");
|
||||
await import("./ExternalModalHandler");
|
||||
|
||||
await import("./hooks/AudioInput");
|
||||
await import("./hooks/ExternalModal");
|
||||
await import("./hooks/ServerConnection");
|
||||
|
||||
await import("./UnloadHandler");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
window.displayCriticalError("Failed to load native extensions: " + error);
|
||||
|
@ -2,27 +2,27 @@ import * as electron from "electron";
|
||||
import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
|
||||
/* We read/write to this file every time again because this file could be used by multible processes */
|
||||
const data_file: string = path.join(electron.app.getPath('userData'), "window-bounds.json");
|
||||
/* We read/write to this file every time again because this file could be used by multiple processes */
|
||||
const data_file: string = path.join((electron.app || electron.remote.app).getPath('userData'), "window-bounds.json");
|
||||
|
||||
import BrowserWindow = Electron.BrowserWindow;
|
||||
import Rectangle = Electron.Rectangle;
|
||||
|
||||
let _changed_data: {[key: string]:Rectangle} = {};
|
||||
let _changed_saver: NodeJS.Timer;
|
||||
let changedData: {[key: string]:Rectangle} = {};
|
||||
let changedDataSaveTimeout: NodeJS.Timer;
|
||||
|
||||
export async function save_changes() {
|
||||
clearTimeout(_changed_saver);
|
||||
clearTimeout(changedDataSaveTimeout);
|
||||
|
||||
try {
|
||||
const data = (await fs.pathExists(data_file) ? await fs.readJson(data_file) : {}) || {};
|
||||
Object.assign(data, _changed_data);
|
||||
Object.assign(data, changedData);
|
||||
|
||||
await fs.ensureFile(data_file);
|
||||
await fs.writeJson(data_file, data);
|
||||
path_exists = true;
|
||||
|
||||
_changed_data = {};
|
||||
changedData = {};
|
||||
} catch(error) {
|
||||
console.warn("Failed to save window bounds: %o", error);
|
||||
}
|
||||
@ -51,44 +51,47 @@ export async function get_last_bounds(key: string) : Promise<Rectangle> {
|
||||
}
|
||||
}
|
||||
|
||||
export function track_bounds(key: string, window: BrowserWindow) {
|
||||
export function startTrackWindowBounds(windowId: string, window: BrowserWindow) {
|
||||
const events = ['move', 'moved', 'resize'];
|
||||
|
||||
const update_bounds = () => {
|
||||
_changed_data[key] = window.getBounds();
|
||||
const onWindowBoundsChanged = () => {
|
||||
changedData[windowId] = window.getBounds();
|
||||
|
||||
clearTimeout(_changed_saver);
|
||||
_changed_saver = setTimeout(save_changes, 1000);
|
||||
clearTimeout(changedDataSaveTimeout);
|
||||
changedDataSaveTimeout = setTimeout(save_changes, 1000);
|
||||
};
|
||||
|
||||
for(const event of events)
|
||||
window.on(event as any, update_bounds);
|
||||
window.on(event as any, onWindowBoundsChanged);
|
||||
|
||||
window.on('closed', () => {
|
||||
for(const event of events)
|
||||
window.removeListener(event as any, update_bounds);
|
||||
})
|
||||
window.removeListener(event as any, onWindowBoundsChanged);
|
||||
});
|
||||
}
|
||||
|
||||
export async function apply_bounds(key: string, window: BrowserWindow, bounds?: Rectangle, options?: { apply_size?: boolean; apply_position?: boolean }) {
|
||||
const screen = electron.screen;
|
||||
export async function loadWindowBounds(windowId: string, window: BrowserWindow, bounds?: Rectangle, options?: { applySize?: boolean; applyPosition?: boolean }) {
|
||||
const screen = electron.screen || electron.remote.screen;
|
||||
|
||||
if(!bounds)
|
||||
bounds = await get_last_bounds(key);
|
||||
if(!bounds) {
|
||||
bounds = await get_last_bounds(windowId);
|
||||
}
|
||||
|
||||
if(!options)
|
||||
if(!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
const original_bounds = window.getBounds();
|
||||
|
||||
if(typeof(options.apply_size) !== "boolean" || options.apply_size) {
|
||||
if(typeof(options.applySize) !== "boolean" || options.applySize) {
|
||||
let height = bounds.height > 0 ? bounds.height : original_bounds.height;
|
||||
let width = bounds.width > 0 ? bounds.width : original_bounds.width;
|
||||
|
||||
if(height != original_bounds.height || width != original_bounds.width)
|
||||
window.setSize(width, height, true);
|
||||
}
|
||||
if(typeof(options.apply_position) !== "boolean" || options.apply_position) {
|
||||
|
||||
if(typeof(options.applyPosition) !== "boolean" || options.applyPosition) {
|
||||
let x = typeof(bounds.x) === "number" ? bounds.x : original_bounds.x;
|
||||
let y = typeof(bounds.y) === "number" ? bounds.y : original_bounds.y;
|
||||
|
||||
@ -101,7 +104,7 @@ export async function apply_bounds(key: string, window: BrowserWindow, bounds?:
|
||||
flag_invalid = flag_invalid || bounds.y > x || (bounds.y + bounds.height) < y;
|
||||
if(!flag_invalid) {
|
||||
window.setPosition(x, y, true);
|
||||
console.log("Updating position for %s", key);
|
||||
console.log("Updating position for %s", windowId);
|
||||
}
|
||||
}
|
||||
}
|
@ -57,7 +57,13 @@ function(setup_nodejs)
|
||||
function(add_nodejs_module NAME)
|
||||
message("Registering module ${NAME}")
|
||||
_add_nodejs_module(${NAME} ${ARGN})
|
||||
|
||||
if(MSVC)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /std:c++17")
|
||||
else()
|
||||
target_compile_features(${NAME} PUBLIC cxx_std_17)
|
||||
endif()
|
||||
|
||||
set_target_properties(${NAME}
|
||||
PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${NODE_LIB_DIRECTORY}/"
|
||||
@ -108,6 +114,7 @@ if (MSVC)
|
||||
)
|
||||
foreach(CompilerFlag ${CompilerFlags})
|
||||
string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
|
||||
string(REGEX REPLACE "/O\\S+($| )" "/02" ${CompilerFlag} "${${CompilerFlag}}")
|
||||
endforeach()
|
||||
#add_compile_options("/MTd")
|
||||
add_compile_options("/EHsc") #We require exception handling
|
||||
|
@ -620,7 +620,6 @@ function(add_nodejs_module NAME)
|
||||
CXX_VISIBILITY_PRESET hidden
|
||||
POSITION_INDEPENDENT_CODE TRUE
|
||||
CMAKE_CXX_STANDARD_REQUIRED TRUE
|
||||
CXX_STANDARD 11
|
||||
)
|
||||
|
||||
# Handle link flag cases properly
|
||||
|
@ -3,15 +3,15 @@ set(MODULE_NAME "teaclient_ppt")
|
||||
set(SOURCE_FILES src/KeyboardHook.cpp)
|
||||
if (MSVC)
|
||||
set(SOURCE_FILES ${SOURCE_FILES} src/Win32KeyboardHook.cpp src/Win32KeyboardHookLL.cpp src/Win32KeyboardRawInput.cpp)
|
||||
add_definitions(-DUSING_UV_SHARED)
|
||||
else()
|
||||
add_definitions(-DHAVE_X11)
|
||||
set(SOURCE_FILES ${SOURCE_FILES} src/X11KeyboardHook.cpp)
|
||||
endif()
|
||||
|
||||
add_nodejs_module(${MODULE_NAME} binding.cc ${SOURCE_FILES})
|
||||
if (WIN32)
|
||||
target_compile_definitions(${MODULE_NAME} PUBLIC /O2)
|
||||
target_compile_definitions(${MODULE_NAME} PRIVATE -DUSING_UV_SHARED)
|
||||
else()
|
||||
target_compile_definitions(${MODULE_NAME} PRIVATE -DHAVE_X11)
|
||||
endif()
|
||||
|
||||
add_executable(Hook-Test ${SOURCE_FILES} test/HookTest.cpp)
|
||||
|
@ -173,9 +173,6 @@ endif()
|
||||
add_definitions(-DNO_OPEN_SSL)
|
||||
target_link_libraries(${MODULE_NAME} ${REQUIRED_LIBRARIES})
|
||||
target_compile_definitions(${MODULE_NAME} PUBLIC -DNODEJS_API)
|
||||
if (WIN32)
|
||||
target_compile_definitions(${MODULE_NAME} PUBLIC /O2)
|
||||
endif()
|
||||
|
||||
add_executable(Audio-Test ${SOURCE_FILES} test/audio/main.cpp)
|
||||
target_link_libraries(Audio-Test ${REQUIRED_LIBRARIES})
|
||||
|
3
native/serverconnection/exports/exports.d.ts
vendored
3
native/serverconnection/exports/exports.d.ts
vendored
@ -234,9 +234,10 @@ declare module "tc-native/connection" {
|
||||
callback_started: () => any;
|
||||
}
|
||||
|
||||
export type DeviceSetResult = "success" | "invalid-device";
|
||||
export interface AudioRecorder {
|
||||
get_device() : string;
|
||||
set_device(device_id: string, callback: () => void); /* Recorder needs to be started afterwards */
|
||||
set_device(device_id: string, callback: (result: DeviceSetResult) => void); /* Recorder needs to be started afterwards */
|
||||
|
||||
start(callback: (result: boolean | string) => void);
|
||||
started() : boolean;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Created by WolverinDEV on 09/08/2020.
|
||||
//
|
||||
|
||||
#include <cmath>
|
||||
#include "AudioGain.h"
|
||||
#include "../logger.h"
|
||||
|
||||
@ -18,7 +19,7 @@ bool tc::audio::apply_gain(void *vp_buffer, size_t channel_count, size_t sample_
|
||||
|
||||
if(value > 1.f) {
|
||||
log_debug(category::audio, tr("Audio gain apply clipped: {}"), (float) value);
|
||||
value = 1.f;
|
||||
value = isinf(value) ? 0 : 1.f;
|
||||
audio_clipped = true;
|
||||
}
|
||||
}
|
||||
|
@ -11,165 +11,19 @@ using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::audio;
|
||||
|
||||
#if false
|
||||
class AudioInputSource {
|
||||
public:
|
||||
constexpr static auto kChannelCount{2};
|
||||
constexpr static auto kSampleRate{48000};
|
||||
|
||||
explicit AudioInputSource(PaHostApiIndex index) : device_index{index} {}
|
||||
~AudioInputSource() = default;
|
||||
|
||||
/* its blocking! */
|
||||
void register_consumer(AudioInput* consumer) {
|
||||
std::lock_guard lock{this->registered_inputs_lock};
|
||||
if(find(this->registered_inputs.begin(), this->registered_inputs.end(), consumer) != this->registered_inputs.end())
|
||||
return;
|
||||
|
||||
this->registered_inputs.push_back(consumer);
|
||||
}
|
||||
|
||||
/* its blocking */
|
||||
void remove_consumer(AudioInput* consumer) {
|
||||
std::lock_guard lock{this->registered_inputs_lock};
|
||||
|
||||
auto index = find(this->registered_inputs.begin(), this->registered_inputs.end(), consumer);
|
||||
if(index == this->registered_inputs.end())
|
||||
return;
|
||||
|
||||
this->registered_inputs.erase(index);
|
||||
if(!this->registered_inputs.empty())
|
||||
return;
|
||||
}
|
||||
|
||||
/* this could take a bit longer! */
|
||||
bool begin_recording(std::string& error) {
|
||||
std::lock_guard lock{this->state_lock};
|
||||
if(this->state == RECORDING) return true;
|
||||
|
||||
if(this->state != STOPPED) {
|
||||
if(this->state == DELETED) {
|
||||
error = "stream has been deleted";
|
||||
return false;
|
||||
}
|
||||
error = "invalid state";
|
||||
return false;
|
||||
}
|
||||
|
||||
this->current_device = Pa_GetDeviceInfo(this->device_index);
|
||||
if(!this->current_device) {
|
||||
error = "failed to get device info";
|
||||
return false;
|
||||
}
|
||||
|
||||
PaStreamParameters parameters{};
|
||||
memset(¶meters, 0, sizeof(parameters));
|
||||
parameters.channelCount = (int) kChannelCount;
|
||||
parameters.device = this->device_index;
|
||||
parameters.sampleFormat = paFloat32;
|
||||
parameters.suggestedLatency = this->current_device->defaultLowOutputLatency;
|
||||
auto err = Pa_OpenStream(
|
||||
&this->input_stream,
|
||||
¶meters,
|
||||
nullptr,
|
||||
(double) kSampleRate,
|
||||
paFramesPerBufferUnspecified,
|
||||
paClipOff,
|
||||
&AudioInputSource::pa_audio_callback,
|
||||
this);
|
||||
|
||||
if(err != paNoError) {
|
||||
this->input_stream = nullptr;
|
||||
error = to_string(err) + "/" + Pa_GetErrorText(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
err = Pa_StartStream(this->input_stream);
|
||||
if(err != paNoError) {
|
||||
error = "recording failed " + to_string(err) + "/" + Pa_GetErrorText(err);
|
||||
err = Pa_CloseStream(this->input_stream);
|
||||
if(err != paNoError)
|
||||
log_critical(category::audio, tr("Failed to close opened pa stream. This will cause memory leaks. Error: {}/{}"), err, Pa_GetErrorText(err));
|
||||
return false;
|
||||
}
|
||||
this->state = RECORDING;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void stop_recording_if_possible() {
|
||||
std::lock_guard lock{this->state_lock};
|
||||
if(this->state != RECORDING) return;
|
||||
|
||||
{
|
||||
std::lock_guard client_lock{this->registered_inputs_lock};
|
||||
if(!this->registered_inputs.empty()) return;
|
||||
}
|
||||
this->state = STOPPED;
|
||||
|
||||
if(Pa_IsStreamActive(this->input_stream))
|
||||
Pa_AbortStream(this->input_stream);
|
||||
|
||||
auto error = Pa_CloseStream(this->input_stream);
|
||||
if(error != paNoError)
|
||||
log_error(category::audio, tr("Failed to close PA stream: {}"), error);
|
||||
this->input_stream = nullptr;
|
||||
}
|
||||
|
||||
const PaDeviceIndex device_index;
|
||||
private:
|
||||
static int pa_audio_callback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* _input_source) {
|
||||
if(!input) return 0; /* this should never happen */
|
||||
auto input_source = (AudioInputSource*) _input_source;
|
||||
|
||||
std::lock_guard lock{input_source->registered_inputs_lock};
|
||||
for(auto& client : input_source->registered_inputs)
|
||||
client->consume(input, frameCount, 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::mutex state_lock{};
|
||||
enum _state {
|
||||
STOPPED,
|
||||
RECORDING,
|
||||
DELETED
|
||||
} state{STOPPED};
|
||||
PaStream* input_stream{nullptr};
|
||||
const PaDeviceInfo* current_device = nullptr;
|
||||
|
||||
std::mutex registered_inputs_lock{};
|
||||
std::vector<AudioInput*> registered_inputs{};
|
||||
};
|
||||
std::mutex input_sources_lock{};
|
||||
static std::deque<std::shared_ptr<AudioInputSource>> input_sources{};
|
||||
|
||||
std::shared_ptr<AudioInputSource> get_input_source(PaDeviceIndex device_index, bool create = true) {
|
||||
std::lock_guard sources_lock{input_sources_lock};
|
||||
for(const auto& input : input_sources)
|
||||
if(input->device_index == device_index)
|
||||
return input;
|
||||
if(!create)
|
||||
return nullptr;
|
||||
|
||||
auto input = std::make_shared<AudioInputSource>(device_index);
|
||||
input_sources.push_back(std::make_shared<AudioInputSource>(device_index));
|
||||
return input;
|
||||
}
|
||||
#endif
|
||||
|
||||
AudioConsumer::AudioConsumer(tc::audio::AudioInput *handle, size_t channel_count, size_t sample_rate, size_t frame_size) :
|
||||
handle(handle),
|
||||
channel_count(channel_count),
|
||||
sample_rate(sample_rate) ,
|
||||
frame_size(frame_size) {
|
||||
if(this->frame_size > 0) {
|
||||
this->reframer = make_unique<Reframer>(channel_count, frame_size);
|
||||
this->reframer = std::make_unique<Reframer>(channel_count, frame_size);
|
||||
this->reframer->on_frame = [&](const void* buffer) { this->handle_framed_data(buffer, this->frame_size); };
|
||||
}
|
||||
}
|
||||
|
||||
void AudioConsumer::handle_framed_data(const void *buffer, size_t samples) {
|
||||
unique_lock read_callback_lock(this->on_read_lock);
|
||||
std::unique_lock read_callback_lock(this->on_read_lock);
|
||||
auto function = this->on_read; /* copy */
|
||||
read_callback_lock.unlock();
|
||||
if(!function)
|
||||
@ -189,7 +43,7 @@ AudioInput::AudioInput(size_t channels, size_t rate) : _channel_count(channels),
|
||||
AudioInput::~AudioInput() {
|
||||
this->close_device();
|
||||
{
|
||||
lock_guard lock(this->consumers_lock);
|
||||
std::lock_guard lock(this->consumers_lock);
|
||||
for(const auto& consumer : this->_consumers)
|
||||
consumer->handle = nullptr;
|
||||
}
|
||||
@ -197,7 +51,7 @@ AudioInput::~AudioInput() {
|
||||
}
|
||||
|
||||
void AudioInput::set_device(const std::shared_ptr<AudioDevice> &device) {
|
||||
lock_guard lock(this->input_source_lock);
|
||||
std::lock_guard lock(this->input_source_lock);
|
||||
if(device == this->input_device) return;
|
||||
|
||||
this->close_device();
|
||||
@ -205,7 +59,7 @@ void AudioInput::set_device(const std::shared_ptr<AudioDevice> &device) {
|
||||
}
|
||||
|
||||
void AudioInput::close_device() {
|
||||
lock_guard lock(this->input_source_lock);
|
||||
std::lock_guard lock(this->input_source_lock);
|
||||
if(this->input_recorder) {
|
||||
this->input_recorder->remove_consumer(this);
|
||||
this->input_recorder->stop_if_possible();
|
||||
@ -216,12 +70,15 @@ void AudioInput::close_device() {
|
||||
}
|
||||
|
||||
bool AudioInput::record(std::string& error) {
|
||||
lock_guard lock(this->input_source_lock);
|
||||
std::lock_guard lock(this->input_source_lock);
|
||||
if(!this->input_device) {
|
||||
error = "no device";
|
||||
return false;
|
||||
}
|
||||
if(this->input_recorder) return true;
|
||||
|
||||
if(this->input_recorder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this->input_recorder = this->input_device->record();
|
||||
if(!this->input_recorder) {
|
||||
@ -255,9 +112,9 @@ void AudioInput::stop() {
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioConsumer> AudioInput::create_consumer(size_t frame_length) {
|
||||
auto result = shared_ptr<AudioConsumer>(new AudioConsumer(this, this->_channel_count, this->_sample_rate, frame_length));
|
||||
auto result = std::shared_ptr<AudioConsumer>(new AudioConsumer(this, this->_channel_count, this->_sample_rate, frame_length));
|
||||
{
|
||||
lock_guard lock(this->consumers_lock);
|
||||
std::lock_guard lock(this->consumers_lock);
|
||||
this->_consumers.push_back(result);
|
||||
}
|
||||
return result;
|
||||
@ -265,7 +122,7 @@ std::shared_ptr<AudioConsumer> AudioInput::create_consumer(size_t frame_length)
|
||||
|
||||
void AudioInput::delete_consumer(const std::shared_ptr<AudioConsumer> &source) {
|
||||
{
|
||||
lock_guard lock(this->consumers_lock);
|
||||
std::lock_guard lock(this->consumers_lock);
|
||||
auto it = find(this->_consumers.begin(), this->_consumers.end(), source);
|
||||
if(it != this->_consumers.end())
|
||||
this->_consumers.erase(it);
|
||||
@ -314,13 +171,13 @@ void AudioInput::consume(const void *input, size_t frameCount, size_t channels)
|
||||
audio::apply_gain(this->resample_buffer, this->_channel_count, frameCount, this->_volume);
|
||||
}
|
||||
|
||||
auto begin = chrono::system_clock::now();
|
||||
auto begin = std::chrono::system_clock::now();
|
||||
for(const auto& consumer : this->consumers())
|
||||
consumer->process_data(input, frameCount);
|
||||
|
||||
auto end = chrono::system_clock::now();
|
||||
auto ms = chrono::duration_cast<chrono::milliseconds>(end - begin).count();
|
||||
auto end = std::chrono::system_clock::now();
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count();
|
||||
if(ms > 5) {
|
||||
log_warn(category::audio, tr("Processing of audio input needed {}ms. This could be an issue!"), chrono::duration_cast<chrono::milliseconds>(end - begin).count());
|
||||
log_warn(category::audio, tr("Processing of audio input needed {}ms. This could be an issue!"), std::chrono::duration_cast<chrono::milliseconds>(end - begin).count());
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ namespace tc::audio {
|
||||
class AudioInput : public AudioDeviceRecord::Consumer {
|
||||
friend class ::AudioInputSource;
|
||||
public:
|
||||
AudioInput(size_t /* channels */, size_t /* rate */);
|
||||
AudioInput(size_t /* channels */, size_t /* sample rate */);
|
||||
virtual ~AudioInput();
|
||||
|
||||
void set_device(const std::shared_ptr<AudioDevice>& /* device */);
|
||||
@ -58,10 +58,10 @@ namespace tc::audio {
|
||||
std::shared_ptr<AudioConsumer> create_consumer(size_t /* frame size */);
|
||||
void delete_consumer(const std::shared_ptr<AudioConsumer>& /* source */);
|
||||
|
||||
inline size_t channel_count() { return this->_channel_count; }
|
||||
inline size_t sample_rate() { return this->_sample_rate; }
|
||||
[[nodiscard]] inline size_t channel_count() const { return this->_channel_count; }
|
||||
[[nodiscard]] inline size_t sample_rate() const { return this->_sample_rate; }
|
||||
|
||||
inline float volume() { return this->_volume; }
|
||||
[[nodiscard]] inline float volume() const { return this->_volume; }
|
||||
inline void set_volume(float value) { this->_volume = value; }
|
||||
private:
|
||||
void consume(const void *, size_t, size_t) override;
|
||||
@ -71,15 +71,15 @@ namespace tc::audio {
|
||||
|
||||
std::mutex consumers_lock;
|
||||
std::deque<std::shared_ptr<AudioConsumer>> _consumers;
|
||||
std::unique_ptr<AudioResampler> _resampler{nullptr};
|
||||
|
||||
std::recursive_mutex input_source_lock;
|
||||
|
||||
std::unique_ptr<AudioResampler> _resampler{nullptr};
|
||||
std::shared_ptr<AudioDevice> input_device{};
|
||||
|
||||
void* resample_buffer{nullptr};
|
||||
size_t resample_buffer_size{0};
|
||||
|
||||
float _volume = 1.f;
|
||||
float _volume{1.f};
|
||||
|
||||
std::shared_ptr<AudioDeviceRecord> input_recorder{};
|
||||
};
|
||||
|
@ -149,23 +149,28 @@ NAN_METHOD(AudioRecorderWrapper::_set_device) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto device = is_null_device ? nullptr : audio::find_device_by_id(*Nan::Utf8String(info[0]), true);
|
||||
if(!device && !is_null_device) {
|
||||
Nan::ThrowError("invalid device id");
|
||||
return;
|
||||
}
|
||||
|
||||
unique_ptr<Nan::Persistent<v8::Function>> _callback = make_unique<Nan::Persistent<v8::Function>>(info[1].As<v8::Function>());
|
||||
unique_ptr<Nan::Persistent<v8::Object>> _recorder = make_unique<Nan::Persistent<v8::Object>>(info.Holder());
|
||||
|
||||
auto _async_callback = Nan::async_callback([call = std::move(_callback), recorder = move(_recorder)] {
|
||||
auto call_callback = [call = std::move(_callback), recorder = move(_recorder)](const std::string& status) {
|
||||
Nan::HandleScope scope;
|
||||
auto callback_function = call->Get(Nan::GetCurrentContext()->GetIsolate());
|
||||
|
||||
(void) callback_function->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||
v8::Local<v8::Value> args[1];
|
||||
args[0] = Nan::LocalStringUTF8(status);
|
||||
(void) callback_function->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, args);
|
||||
|
||||
recorder->Reset();
|
||||
call->Reset();
|
||||
};
|
||||
|
||||
auto device = is_null_device ? nullptr : audio::find_device_by_id(*Nan::Utf8String(info[0]), true);
|
||||
if(!device && !is_null_device) {
|
||||
call_callback("invalid-device");
|
||||
return;
|
||||
}
|
||||
|
||||
auto _async_callback = Nan::async_callback([callback = std::move(call_callback)] {
|
||||
callback("success");
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
std::thread([_async_callback, input, device]{
|
||||
@ -189,10 +194,11 @@ NAN_METHOD(AudioRecorderWrapper::_start) {
|
||||
std::string error{};
|
||||
|
||||
v8::Local<v8::Value> argv[1];
|
||||
if(input->record(error))
|
||||
if(input->record(error)) {
|
||||
argv[0] = Nan::New<v8::Boolean>(true);
|
||||
else
|
||||
} else {
|
||||
argv[0] = Nan::LocalString(error);
|
||||
}
|
||||
(void) info[0].As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,7 @@
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace tc::audio {
|
||||
class AudioInput;
|
||||
|
||||
namespace recorder {
|
||||
@ -61,4 +60,3 @@ namespace tc {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ namespace tc {
|
||||
friend inline std::shared_ptr<_Tp> std::static_pointer_cast(const std::shared_ptr<_Up>& __r) noexcept;
|
||||
friend class VoiceConnection;
|
||||
public:
|
||||
VoiceSender(VoiceConnection*);
|
||||
explicit VoiceSender(VoiceConnection*);
|
||||
virtual ~VoiceSender();
|
||||
|
||||
codec::value get_codec() { return this->_current_codec; }
|
||||
|
@ -37,17 +37,17 @@ NAN_MODULE_INIT(VoiceConnectionWrap::Init) {
|
||||
Nan::SetPrototypeMethod(klass, "decoding_supported", VoiceConnectionWrap::_decoding_supported);
|
||||
Nan::SetPrototypeMethod(klass, "encoding_supported", VoiceConnectionWrap::_encoding_supported);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "register_client", VoiceConnectionWrap::_register_client);
|
||||
Nan::SetPrototypeMethod(klass, "available_clients", VoiceConnectionWrap::_available_clients);
|
||||
Nan::SetPrototypeMethod(klass, "unregister_client", VoiceConnectionWrap::_unregister_client);
|
||||
Nan::SetPrototypeMethod(klass, "register_client", VoiceConnectionWrap::register_client);
|
||||
Nan::SetPrototypeMethod(klass, "available_clients", VoiceConnectionWrap::available_clients);
|
||||
Nan::SetPrototypeMethod(klass, "unregister_client", VoiceConnectionWrap::unregister_client);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "audio_source", VoiceConnectionWrap::_audio_source);
|
||||
Nan::SetPrototypeMethod(klass, "set_audio_source", VoiceConnectionWrap::_set_audio_source);
|
||||
Nan::SetPrototypeMethod(klass, "audio_source", VoiceConnectionWrap::audio_source);
|
||||
Nan::SetPrototypeMethod(klass, "set_audio_source", VoiceConnectionWrap::set_audio_source);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_encoder_codec", VoiceConnectionWrap::_get_encoder_codec);
|
||||
Nan::SetPrototypeMethod(klass, "set_encoder_codec", VoiceConnectionWrap::_set_encoder_codec);
|
||||
Nan::SetPrototypeMethod(klass, "get_encoder_codec", VoiceConnectionWrap::get_encoder_codec);
|
||||
Nan::SetPrototypeMethod(klass, "set_encoder_codec", VoiceConnectionWrap::set_encoder_codec);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "enable_voice_send", VoiceConnectionWrap::_enable_voice_send);
|
||||
Nan::SetPrototypeMethod(klass, "enable_voice_send", VoiceConnectionWrap::enable_voice_send);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
@ -81,17 +81,15 @@ NAN_METHOD(VoiceConnectionWrap::_decoding_supported) {
|
||||
info.GetReturnValue().Set(codec >= 4 && codec <= 5); /* ignore SPEX currently :/ */
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_register_client) {
|
||||
return ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->register_client(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::register_client) {
|
||||
auto connection = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
auto id = info[0]->Uint32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
auto handle = this->handle.lock();
|
||||
auto handle = connection->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
@ -106,14 +104,10 @@ NAN_METHOD(VoiceConnectionWrap::register_client) {
|
||||
info.GetReturnValue().Set(client->js_handle());
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_available_clients) {
|
||||
return ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->available_clients(info);
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::available_clients) {
|
||||
auto handle = this->handle.lock();
|
||||
auto connection = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
|
||||
auto handle = connection->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
@ -128,20 +122,20 @@ NAN_METHOD(VoiceConnectionWrap::available_clients) {
|
||||
info.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_unregister_client) {
|
||||
return ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->unregister_client(info);
|
||||
}
|
||||
NAN_METHOD(VoiceConnectionWrap::unregister_client) {
|
||||
auto connection = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
auto id = info[0]->Uint32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
auto handle = this->handle.lock();
|
||||
auto handle = connection->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
auto client = handle->find_client(id);
|
||||
if(!client) {
|
||||
Nan::ThrowError("missing client");
|
||||
@ -152,16 +146,14 @@ NAN_METHOD(VoiceConnectionWrap::unregister_client) {
|
||||
handle->delete_client(client);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_audio_source) {
|
||||
NAN_METHOD(VoiceConnectionWrap::audio_source) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
info.GetReturnValue().Set(client->_voice_recoder_handle.Get(info.GetIsolate()));
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_set_audio_source) {
|
||||
ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->set_audio_source(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::set_audio_source) {
|
||||
auto connection = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
@ -173,36 +165,38 @@ NAN_METHOD(VoiceConnectionWrap::set_audio_source) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this->handle.lock()) {
|
||||
auto handle = connection->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
this->release_recorder();
|
||||
connection->release_recorder();
|
||||
if(!info[0]->IsNullOrUndefined()) {
|
||||
this->_voice_recoder_ptr = ObjectWrap::Unwrap<audio::recorder::AudioConsumerWrapper>(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
this->_voice_recoder_handle.Reset(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
connection->_voice_recoder_ptr = ObjectWrap::Unwrap<audio::recorder::AudioConsumerWrapper>(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
connection->_voice_recoder_handle.Reset(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
|
||||
auto native_consumer = this->_voice_recoder_ptr->native_consumer();
|
||||
auto native_consumer = connection->_voice_recoder_ptr->native_consumer();
|
||||
|
||||
weak_ptr weak_handle = this->handle;
|
||||
weak_ptr weak_handle = handle;
|
||||
auto sample_rate = native_consumer->sample_rate;
|
||||
auto channels = native_consumer->channel_count;
|
||||
|
||||
lock_guard read_lock(this->_voice_recoder_ptr->native_read_callback_lock);
|
||||
this->_voice_recoder_ptr->native_read_callback = [weak_handle, sample_rate, channels](const void* buffer, size_t length) {
|
||||
lock_guard read_lock(connection->_voice_recoder_ptr->native_read_callback_lock);
|
||||
connection->_voice_recoder_ptr->native_read_callback = [weak_handle, sample_rate, channels](const void* buffer, size_t length) {
|
||||
auto handle = weak_handle.lock();
|
||||
if(!handle) {
|
||||
log_warn(category::audio, tr("Missing voice connection handle. Dropping input!"));
|
||||
return;
|
||||
}
|
||||
|
||||
shared_ptr<VoiceSender> sender = handle->voice_sender();
|
||||
auto sender = handle->voice_sender();
|
||||
if(sender) {
|
||||
if(length > 0 && buffer)
|
||||
if(length > 0 && buffer) {
|
||||
sender->send_data(buffer, length, sample_rate, channels);
|
||||
else
|
||||
} else {
|
||||
sender->send_stop();
|
||||
}
|
||||
} else {
|
||||
log_warn(category::audio, tr("Missing voice connection audio sender. Dropping input!"));
|
||||
return;
|
||||
@ -211,9 +205,9 @@ NAN_METHOD(VoiceConnectionWrap::set_audio_source) {
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_get_encoder_codec) {
|
||||
auto _this = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = _this->handle.lock();
|
||||
NAN_METHOD(VoiceConnectionWrap::get_encoder_codec) {
|
||||
auto connection = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = connection->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
@ -222,9 +216,9 @@ NAN_METHOD(VoiceConnectionWrap::_get_encoder_codec) {
|
||||
info.GetReturnValue().Set(handle->get_encoder_codec());
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_set_encoder_codec) {
|
||||
auto _this = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = _this->handle.lock();
|
||||
NAN_METHOD(VoiceConnectionWrap::set_encoder_codec) {
|
||||
auto connection = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = connection->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
@ -239,7 +233,7 @@ NAN_METHOD(VoiceConnectionWrap::_set_encoder_codec) {
|
||||
handle->set_encoder_codec((uint8_t) info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_enable_voice_send) {
|
||||
NAN_METHOD(VoiceConnectionWrap::enable_voice_send) {
|
||||
auto _this = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = _this->handle.lock();
|
||||
if(!handle) {
|
||||
|
@ -29,7 +29,7 @@ namespace tc {
|
||||
}
|
||||
|
||||
explicit VoiceConnectionWrap(const std::shared_ptr<VoiceConnection>&);
|
||||
virtual ~VoiceConnectionWrap();
|
||||
~VoiceConnectionWrap() override;
|
||||
|
||||
void do_wrap(const v8::Local<v8::Object>&);
|
||||
private:
|
||||
@ -37,20 +37,16 @@ namespace tc {
|
||||
static NAN_METHOD(_encoding_supported);
|
||||
static NAN_METHOD(_decoding_supported);
|
||||
|
||||
static NAN_METHOD(_register_client);
|
||||
NAN_METHOD(register_client);
|
||||
static NAN_METHOD(_available_clients);
|
||||
NAN_METHOD(available_clients);
|
||||
static NAN_METHOD(_unregister_client);
|
||||
NAN_METHOD(unregister_client);
|
||||
static NAN_METHOD(register_client);
|
||||
static NAN_METHOD(available_clients);
|
||||
static NAN_METHOD(unregister_client);
|
||||
|
||||
static NAN_METHOD(_audio_source);
|
||||
static NAN_METHOD(_set_audio_source);
|
||||
NAN_METHOD(set_audio_source);
|
||||
static NAN_METHOD(audio_source);
|
||||
static NAN_METHOD(set_audio_source);
|
||||
|
||||
static NAN_METHOD(_get_encoder_codec);
|
||||
static NAN_METHOD(_set_encoder_codec);
|
||||
static NAN_METHOD(_enable_voice_send);
|
||||
static NAN_METHOD(get_encoder_codec);
|
||||
static NAN_METHOD(set_encoder_codec);
|
||||
static NAN_METHOD(enable_voice_send);
|
||||
|
||||
void release_recorder();
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TeaClient",
|
||||
"version": "1.4.9",
|
||||
"version": "1.4.10",
|
||||
"description": "",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
Loading…
Reference in New Issue
Block a user