Adding rnnoise suppression

This commit is contained in:
WolverinDEV 2020-10-01 10:56:22 +02:00
parent 4a49d36ece
commit 7087514df1
38 changed files with 727 additions and 553 deletions

2
github

@ -1 +1 @@
Subproject commit 4fa1ab237cd12b53de46fe82d31c942513c619bd Subproject commit 9c3cc6d05838a03a5827836b300f8bc8e71b26d2

View File

@ -122,5 +122,5 @@ function deploy_client() {
#install_npm #install_npm
#compile_scripts #compile_scripts
#compile_native #compile_native
#package_client package_client
deploy_client #deploy_client

View File

@ -2,7 +2,7 @@ import * as electron from "electron";
import * as crash_handler from "./modules/crash_handler"; import * as crash_handler from "./modules/crash_handler";
import * as child_process from "child_process"; import * as child_process from "child_process";
import {app} from "electron"; import {app} from "electron";
import * as Sentry from "@sentry/electron"; //import * as Sentry from "@sentry/electron";
/* /*
Sentry.init({ Sentry.init({

View File

@ -0,0 +1,47 @@
import {app} from "electron";
import * as crash_handler from "../crash_handler";
import * as loader from "./ui-loader/graphical";
let appReferences = 0;
/**
* Normally the app closes when all windows have been closed.
* If you're holding an app reference, it will not terminate when all windows have been closed.
*/
export function referenceApp() {
appReferences++;
}
export function dereferenceApp() {
appReferences--;
testAppState();
}
function testAppState() {
if(appReferences > 0) { return; }
console.log("All windows have been closed, closing app.");
app.quit();
}
function initializeAppListeners() {
app.on('quit', () => {
console.debug("Shutting down app.");
crash_handler.finalize_handler();
loader.ui.cleanup();
console.log("App has been finalized.");
});
app.on('window-all-closed', () => {
console.log("All windows have been closed. Manual app reference count: %d", appReferences);
testAppState();
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
});
}
initializeAppListeners();

View File

@ -1,14 +1,16 @@
import * as main_window from "./main_window"; import { app } from "electron";
import { mainWindow } from "./main-window";
export function handle_second_instance_call(argv: string[], work_dir: string) { export function handleSecondInstanceCall(argv: string[], _workingDirectory: string) {
const original_args = argv.slice(1).filter(e => !e.startsWith("--original-process-start-time=") && e != "--allow-file-access-from-files"); const original_args = argv.slice(1).filter(e => !e.startsWith("--original-process-start-time=") && e != "--allow-file-access-from-files");
console.log("Second instance: %o", original_args); console.log("Second instance: %o", original_args);
if(!main_window.main_window) { if(!mainWindow) {
console.warn("Ignoring second instance call because we haven't yet started"); console.warn("Ignoring second instance call because we haven't yet started");
return; return;
} }
main_window.main_window.focus();
mainWindow.focus();
execute_connect_urls(original_args); execute_connect_urls(original_args);
} }
@ -16,6 +18,15 @@ export function execute_connect_urls(argv: string[]) {
const connect_urls = argv.filter(e => e.startsWith("teaclient://")); const connect_urls = argv.filter(e => e.startsWith("teaclient://"));
for(const url of connect_urls) { for(const url of connect_urls) {
console.log("Received connect url: %s", url); console.log("Received connect url: %s", url);
main_window.main_window.webContents.send('connect', url); mainWindow.webContents.send('connect', url);
} }
}
export function initializeSingleInstance() : boolean {
if(!app.requestSingleInstanceLock()) {
return false;
}
app.on('second-instance', (event, argv, workingDirectory) => handleSecondInstanceCall(argv, workingDirectory));
return true;
} }

View File

@ -16,18 +16,18 @@ import {parse_version, Version} from "../../shared/version";
import Timer = NodeJS.Timer; import Timer = NodeJS.Timer;
import MessageBoxOptions = Electron.MessageBoxOptions; import MessageBoxOptions = Electron.MessageBoxOptions;
import {Headers} from "tar-stream"; import {Headers} from "tar-stream";
import {Arguments, process_args} from "../../shared/process-arguments"; import {Arguments, processArguments} from "../../shared/process-arguments";
import * as electron from "electron"; import * as electron from "electron";
import {PassThrough} from "stream"; import {PassThrough} from "stream";
import ErrnoException = NodeJS.ErrnoException; import ErrnoException = NodeJS.ErrnoException;
import {reference_app} from "../main_window";
import * as url from "url"; import * as url from "url";
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window"; import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
import {referenceApp} from "../AppInstance";
const is_debug = false; const is_debug = false;
export function server_url() : string { export function server_url() : string {
const default_path = is_debug ? "http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/client-api/environment/" : "http://clientapi.teaspeak.de/"; const default_path = is_debug ? "http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/client-api/environment/" : "http://clientapi.teaspeak.de/";
return process_args.has_value(...Arguments.SERVER_URL) ? process_args.value(...Arguments.SERVER_URL) : default_path; return processArguments.has_value(...Arguments.SERVER_URL) ? processArguments.value(...Arguments.SERVER_URL) : default_path;
} }
export interface UpdateVersion { export interface UpdateVersion {
@ -647,8 +647,8 @@ export async function execute_update(update_file: string, restart_callback: (cal
} }
export async function current_version() : Promise<Version> { export async function current_version() : Promise<Version> {
if(process_args.has_value(Arguments.UPDATER_LOCAL_VERSION)) if(processArguments.has_value(Arguments.UPDATER_LOCAL_VERSION))
return parse_version(process_args.value(Arguments.UPDATER_LOCAL_VERSION)); return parse_version(processArguments.value(Arguments.UPDATER_LOCAL_VERSION));
let parent_path = app.getAppPath(); let parent_path = app.getAppPath();
if(parent_path.endsWith(".asar")) { if(parent_path.endsWith(".asar")) {
@ -679,7 +679,7 @@ export let update_restart_pending = false;
export async function execute_graphical(channel: string, ask_install: boolean) : Promise<Boolean> { export async function execute_graphical(channel: string, ask_install: boolean) : Promise<Boolean> {
const electron = require('electron'); const electron = require('electron');
const ui_debug = process_args.has_flag(Arguments.UPDATER_UI_DEBUG); const ui_debug = processArguments.has_flag(Arguments.UPDATER_UI_DEBUG);
const window = new electron.BrowserWindow({ const window = new electron.BrowserWindow({
show: false, show: false,
width: ui_debug ? 1200 : 600, width: ui_debug ? 1200 : 600,
@ -736,7 +736,7 @@ export async function execute_graphical(channel: string, ask_install: boolean) :
set_text("Loading data"); set_text("Loading data");
let version: UpdateVersion; let version: UpdateVersion;
try { try {
version = await minawait(newest_version(process_args.has_flag(Arguments.UPDATER_ENFORCE) ? undefined : current_vers, channel), 3000); version = await minawait(newest_version(processArguments.has_flag(Arguments.UPDATER_ENFORCE) ? undefined : current_vers, channel), 3000);
} catch (error) { } catch (error) {
set_error("Failed to get newest information:<br>" + error); set_error("Failed to get newest information:<br>" + error);
await await_exit(); await await_exit();
@ -815,7 +815,7 @@ export async function execute_graphical(channel: string, ask_install: boolean) :
try { try {
await execute_update(update_path, callback => { await execute_update(update_path, callback => {
reference_app(); /* we'll never delete this reference, but we'll call app.quit() manually */ referenceApp(); /* we'll never delete this reference, but we'll call app.quit() manually */
update_restart_pending = true; update_restart_pending = true;
window.close(); window.close();
callback(); callback();
@ -873,5 +873,5 @@ export function stop_auto_update_check() {
} }
export async function selected_channel() : Promise<string> { export async function selected_channel() : Promise<string> {
return process_args.has_value(Arguments.UPDATER_CHANNEL) ? process_args.value(Arguments.UPDATER_CHANNEL) : "release"; return processArguments.has_value(Arguments.UPDATER_CHANNEL) ? processArguments.value(Arguments.UPDATER_CHANNEL) : "release";
} }

View File

@ -1,35 +1,32 @@
import {BrowserWindow, Menu, MenuItem, MessageBoxOptions, app, dialog} from "electron"; import {BrowserWindow, app, dialog} from "electron";
import * as path from "path"; import * as path from "path";
let app_references = 0;
export function reference_app() {
app_references++;
}
export function unreference_app() {
app_references--;
test_app_should_exit();
}
export let is_debug: boolean; export let is_debug: boolean;
export let allow_dev_tools: boolean; export let allow_dev_tools: boolean;
import {Arguments, parse_arguments, process_args} from "../shared/process-arguments"; import {Arguments, processArguments} from "../../shared/process-arguments";
import * as updater from "./app-updater"; import * as updater from "./../app-updater";
import * as loader from "./ui-loader"; import * as loader from "./../ui-loader";
import * as crash_handler from "../crash_handler";
import * as url from "url"; import * as url from "url";
import {loadWindowBounds, startTrackWindowBounds} from "../shared/window"; import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
import {referenceApp, dereferenceApp} from "../AppInstance";
// Keep a global reference of the window object, if you don't, the window will // 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. // be closed automatically when the JavaScript object is garbage collected.
export let main_window: BrowserWindow = null; export let mainWindow: BrowserWindow = null;
function spawnMainWindow(rendererEntryPoint: string) {
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
console.log("Allowing untrusted certificate for %o", url);
event.preventDefault();
callback(true);
});
function spawn_main_window(entry_point: string) {
// Create the browser window. // Create the browser window.
console.log("Spawning main window"); console.log("Spawning main window");
reference_app(); /* main browser window references the app */
main_window = new BrowserWindow({ referenceApp(); /* main browser window references the app */
mainWindow = new BrowserWindow({
width: 800, width: 800,
height: 600, height: 600,
@ -40,38 +37,42 @@ function spawn_main_window(entry_point: string) {
webPreferences: { webPreferences: {
webSecurity: false, webSecurity: false,
nodeIntegrationInWorker: true, nodeIntegrationInWorker: true,
nodeIntegration: true nodeIntegration: true,
preload: path.join(__dirname, "preload.js")
}, },
icon: path.join(__dirname, "..", "..", "resources", "logo.ico") icon: path.join(__dirname, "..", "..", "resources", "logo.ico"),
}); });
main_window.webContents.on('devtools-closed', event => { mainWindow.webContents.on('devtools-closed', () => {
console.log("Dev tools destroyed!"); console.log("Dev tools destroyed!");
}); });
main_window.on('closed', () => { mainWindow.on('closed', () => {
app.releaseSingleInstanceLock(); app.releaseSingleInstanceLock();
require("./url-preview").close(); require("../url-preview").close();
main_window = null; mainWindow = null;
unreference_app(); dereferenceApp();
}); });
main_window.loadURL(url.pathToFileURL(loader.ui.preloading_page(entry_point)).toString()); mainWindow.loadURL(url.pathToFileURL(loader.ui.preloading_page(rendererEntryPoint)).toString()).catch(error => {
console.error("Failed to load UI entry point: %o", error);
handleUILoadingError("UI entry point failed to load");
});
main_window.once('ready-to-show', () => { mainWindow.once('ready-to-show', () => {
main_window.show(); mainWindow.show();
loadWindowBounds('main-window', main_window).then(() => { loadWindowBounds('main-window', mainWindow).then(() => {
startTrackWindowBounds('main-window', main_window); startTrackWindowBounds('main-window', mainWindow);
main_window.focus(); mainWindow.focus();
loader.ui.cleanup(); loader.ui.cleanup();
if(allow_dev_tools && !main_window.webContents.isDevToolsOpened()) if(allow_dev_tools && !mainWindow.webContents.isDevToolsOpened())
main_window.webContents.openDevTools(); mainWindow.webContents.openDevTools();
}); });
}); });
main_window.webContents.on('new-window', (event, url_str, frameName, disposition, options, additionalFeatures) => { mainWindow.webContents.on('new-window', (event, url_str, frameName, disposition, options, additionalFeatures) => {
if(frameName.startsWith("__modal_external__")) { if(frameName.startsWith("__modal_external__")) {
return; return;
} }
@ -101,79 +102,44 @@ function spawn_main_window(entry_point: string) {
} }
}); });
main_window.webContents.on('crashed', event => { mainWindow.webContents.on('crashed', () => {
console.error("UI thread crashed! Closing app!"); console.error("UI thread crashed! Closing app!");
if(!process_args.has_flag(Arguments.DEBUG)) if(!processArguments.has_flag(Arguments.DEBUG)) {
main_window.close(); mainWindow.close();
}
}); });
} }
function handle_ui_load_error(message: string) { function handleUILoadingError(message: string) {
referenceApp();
console.log("Caught loading error: %s", message); console.log("Caught loading error: %s", message);
//"A critical error happened while loading TeaClient!", "A critical error happened while loading TeaClient!<br>" + message if(mainWindow) {
reference_app(); mainWindow.close();
mainWindow = undefined;
}
dialog.showMessageBox({ dialog.showMessageBox({
type: "error", type: "error",
buttons: ["exit"], buttons: ["exit"],
title: "A critical error happened while loading TeaClient!", title: "A critical error happened while loading TeaClient!",
message: (message || "no error").toString() message: (message || "no error").toString()
}).then(unreference_app); }).then(dereferenceApp);
loader.ui.cancel(); loader.ui.cancel();
} }
function test_app_should_exit() {
if(app_references > 0) return;
console.log("All windows have been closed, closing app.");
app.quit();
}
function init_listener() {
app.on('quit', () => {
console.debug("Shutting down app.");
crash_handler.finalize_handler();
loader.ui.cleanup();
console.log("App has been finalized.");
});
app.on('window-all-closed', () => {
console.log("All windows have been closed. App reference count: %d", app_references);
test_app_should_exit();
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (main_window === null) {
//spawn_loading_screen();
//createWindow()
}
});
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
console.log("Allowing untrusted certificate for %o", url);
event.preventDefault();
callback(true);
});
}
export function execute() { export function execute() {
console.log("Main app executed!"); console.log("Main app executed!");
parse_arguments(); is_debug = processArguments.has_flag(...Arguments.DEBUG);
is_debug = process_args.has_flag(...Arguments.DEBUG); allow_dev_tools = processArguments.has_flag(...Arguments.DEV_TOOLS);
allow_dev_tools = process_args.has_flag(...Arguments.DEV_TOOLS);
if(is_debug) { if(is_debug) {
console.log("Enabled debug!"); console.log("Enabled debug!");
console.log("Arguments: %o", process_args); console.log("Arguments: %o", processArguments);
} }
Menu.setApplicationMenu(null);
init_listener();
console.log("Setting up render backend"); console.log("Setting up render backend");
require("./render-backend"); require("../render-backend");
console.log("Spawn loading screen"); console.log("Spawn loading screen");
loader.ui.execute_loader().then(async (entry_point: string) => { loader.ui.execute_loader().then(async (entry_point: string) => {
@ -193,14 +159,14 @@ export function execute() {
return entry_point; return entry_point;
}).then((entry_point: string) => { }).then((entry_point: string) => {
reference_app(); /* because we've no windows when we close the loader UI */ referenceApp(); /* because we've no windows when we close the loader UI */
loader.ui.cleanup(); /* close the window */ loader.ui.cleanup(); /* close the window */
if(entry_point) //has not been canceled if(entry_point) //has not been canceled
spawn_main_window(entry_point); spawnMainWindow(entry_point);
else { else {
handle_ui_load_error("Missing UI entry point"); handleUILoadingError("Missing UI entry point");
} }
unreference_app(); dereferenceApp();
}).catch(handle_ui_load_error); }).catch(handleUILoadingError);
} }

View File

@ -0,0 +1,12 @@
/* preloaded script, init hook will be called before the loader will be executed */
declare global {
interface Window {
__native_client_init_hook: () => void;
__native_client_init_shared: (webpackRequire: any) => void;
}
}
window.__native_client_init_hook = () => require("../../renderer/index");
window.__native_client_init_shared = webpackRequire => window["shared-require"] = webpackRequire;
export = {};

View File

@ -1,26 +1,29 @@
// Quit when all windows are closed.
import * as electron from "electron"; import * as electron from "electron";
import * as app_updater from "./app-updater"; import * as app_updater from "./app-updater";
import * as instance_handler from "./instance_handler";
import { app } from "electron"; import {app, Menu} from "electron";
import MessageBoxOptions = electron.MessageBoxOptions; import MessageBoxOptions = electron.MessageBoxOptions;
import {process_args, parse_arguments, Arguments} from "../shared/process-arguments"; import {processArguments, parseProcessArguments, Arguments} from "../shared/process-arguments";
import {open as open_changelog} from "./app-updater/changelog"; import {open as open_changelog} from "./app-updater/changelog";
import * as crash_handler from "../crash_handler"; import * as crash_handler from "../crash_handler";
import {initializeSingleInstance} from "./MultiInstanceHandler";
async function execute_app() { import "./AppInstance";
if(process_args.has_value("update-execute")) {
console.log("Executing update " + process_args.value("update-execute")); async function handleAppReady() {
await app_updater.execute_update(process_args.value("update-execute"), callback => { Menu.setApplicationMenu(null);
if(processArguments.has_value("update-execute")) {
console.log("Executing update " + processArguments.value("update-execute"));
await app_updater.execute_update(processArguments.value("update-execute"), callback => {
console.log("Update preconfig successful. Extracting update. (The client should start automatically)"); console.log("Update preconfig successful. Extracting update. (The client should start automatically)");
app.quit(); app.quit();
setImmediate(callback); setImmediate(callback);
}); });
return; return;
} else if(process_args.has_value("update-failed-new") || process_args.has_value("update-succeed-new")) { } else if(processArguments.has_value("update-failed-new") || processArguments.has_value("update-succeed-new")) {
const success = process_args.has_value("update-succeed-new"); const success = processArguments.has_value("update-succeed-new");
let data: { let data: {
parse_success: boolean; parse_success: boolean;
log_file?: string; log_file?: string;
@ -30,7 +33,7 @@ async function execute_app() {
parse_success: false parse_success: false
}; };
try { try {
let encoded_data = Buffer.from(process_args.value("update-failed-new") || process_args.value("update-succeed-new"), "base64").toString(); let encoded_data = Buffer.from(processArguments.value("update-failed-new") || processArguments.value("update-succeed-new"), "base64").toString();
for(const part of encoded_data.split(";")) { for(const part of encoded_data.split(";")) {
const index = part.indexOf(':'); const index = part.indexOf(':');
if(index == -1) if(index == -1)
@ -45,9 +48,9 @@ async function execute_app() {
} }
console.log("Update success: %o. Update data: %o", success, data); console.log("Update success: %o. Update data: %o", success, data);
let title = ""; let title;
let type = ""; let type;
let message = ""; let message;
const buttons: ({ const buttons: ({
key: string, key: string,
@ -129,22 +132,22 @@ async function execute_app() {
/* register client a teaclient:// handler */ /* register client a teaclient:// handler */
if(app.getAppPath().endsWith(".asar")) { if(app.getAppPath().endsWith(".asar")) {
if(!app.setAsDefaultProtocolClient("teaclient", app.getPath("exe"))) if(!app.setAsDefaultProtocolClient("teaclient", app.getPath("exe"))) {
console.error("Failed to setup default teaclient protocol handler"); console.error("Failed to setup default teaclient protocol handler");
}
} }
try { try {
{ const version = await app_updater.current_version();
const version = await app_updater.current_version(); global["app_version_client"] = version.toString();
global["app_version_client"] = version.toString();
} const main = require("./main-window");
const main = require("./main_window");
main.execute(); main.execute();
app_updater.start_auto_update_check(); app_updater.start_auto_update_check();
} catch (error) { } catch (error) {
console.dir(error); console.error(error);
const result = electron.dialog.showMessageBox({ await electron.dialog.showMessageBox({
type: "error", type: "error",
message: "Failed to execute app main!\n" + error, message: "Failed to execute app main!\n" + error,
title: "Main execution failed!", title: "Main execution failed!",
@ -155,26 +158,26 @@ async function execute_app() {
} }
function main() { function main() {
//setTimeout(() => process.crash(), 1000); if('allowRendererProcessReuse' in app) {
if('allowRendererProcessReuse' in app)
app.allowRendererProcessReuse = false; app.allowRendererProcessReuse = false;
}
parse_arguments(); parseProcessArguments();
if(process_args.has_value(Arguments.DISABLE_HARDWARE_ACCELERATION)) if(processArguments.has_value(Arguments.DISABLE_HARDWARE_ACCELERATION)) {
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
}
if(process_args.has_value(Arguments.DUMMY_CRASH_MAIN)) if(processArguments.has_value(Arguments.DUMMY_CRASH_MAIN)) {
crash_handler.handler.crash(); crash_handler.handler.crash();
}
if(!process_args.has_value(Arguments.DEBUG) && !process_args.has_value(Arguments.NO_SINGLE_INSTANCE)) { if(!processArguments.has_value(Arguments.DEBUG) && !processArguments.has_value(Arguments.NO_SINGLE_INSTANCE)) {
if(!app.requestSingleInstanceLock()) { if(!initializeSingleInstance()) {
console.log("Another instance is already running. Closing this instance"); console.log("Another instance is already running. Closing this instance");
app.exit(0); app.exit(0);
} }
app.on('second-instance', (event, argv, workingDirectory) => instance_handler.handle_second_instance_call(argv, workingDirectory));
} }
app.on('ready', execute_app);
app.on('ready', handleAppReady);
} }
export const execute = main; export const execute = main;

View File

@ -3,7 +3,7 @@ import {ExternalModal, kIPCChannelExternalModal} from "../../shared/ipc/External
import {ProxiedClass} from "../../shared/proxy/Definitions"; import {ProxiedClass} from "../../shared/proxy/Definitions";
import {BrowserWindow, dialog} from "electron"; import {BrowserWindow, dialog} from "electron";
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window"; import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
import {Arguments, process_args} from "../../shared/process-arguments"; import {Arguments, processArguments} from "../../shared/process-arguments";
import {open_preview} from "../url-preview"; import {open_preview} from "../url-preview";
import * as path from "path"; import * as path from "path";
@ -33,6 +33,7 @@ class ProxyImplementation extends ProxiedClass<ExternalModal> implements Externa
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
preload: path.join(__dirname, "..", "..", "renderer-manifest", "preload.js")
}, },
icon: path.join(__dirname, "..", "..", "resources", "logo.ico"), icon: path.join(__dirname, "..", "..", "resources", "logo.ico"),
minWidth: 600, minWidth: 600,
@ -77,7 +78,7 @@ class ProxyImplementation extends ProxiedClass<ExternalModal> implements Externa
} }
}); });
if(process_args.has_flag(Arguments.DEV_TOOLS)) if(processArguments.has_flag(Arguments.DEV_TOOLS))
this.windowInstance.webContents.openDevTools(); this.windowInstance.webContents.openDevTools();
try { try {

View File

@ -6,10 +6,8 @@ import BrowserWindow = electron.BrowserWindow;
import {open as open_changelog} from "../app-updater/changelog"; import {open as open_changelog} from "../app-updater/changelog";
import * as updater from "../app-updater"; import * as updater from "../app-updater";
import {execute_connect_urls} from "../instance_handler"; import {execute_connect_urls} from "../MultiInstanceHandler";
import {process_args} from "../../shared/process-arguments"; import {processArguments} from "../../shared/process-arguments";
import {open_preview} from "../url-preview";
import {dialog} from "electron";
import "./ExternalModal"; import "./ExternalModal";
@ -17,7 +15,7 @@ ipcMain.on('basic-action', (event, action, ...args: any[]) => {
const window = BrowserWindow.fromWebContents(event.sender); const window = BrowserWindow.fromWebContents(event.sender);
if(action == "parse-connect-arguments") { if(action == "parse-connect-arguments") {
execute_connect_urls(process_args["_"] || []); execute_connect_urls(processArguments["_"] || []);
} else if(action === "open-changelog") { } else if(action === "open-changelog") {
open_changelog(); open_changelog();
} else if(action === "check-native-update") { } else if(action === "check-native-update") {

View File

@ -2,7 +2,7 @@ import * as electron from "electron";
import * as path from "path"; import * as path from "path";
import {screen} from "electron"; import {screen} from "electron";
import {Arguments, process_args} from "../../shared/process-arguments"; import {Arguments, processArguments} from "../../shared/process-arguments";
import * as loader from "./loader"; import * as loader from "./loader";
import * as updater from "../app-updater"; import * as updater from "../app-updater";
import * as url from "url"; import * as url from "url";
@ -60,7 +60,7 @@ export namespace ui {
console.error("Received error from loader after it had been closed... Error: %o", error); console.error("Received error from loader after it had been closed... Error: %o", error);
}; };
}; };
if(!process_args.has_flag(...Arguments.DISABLE_ANIMATION)) if(!processArguments.has_flag(...Arguments.DISABLE_ANIMATION))
setTimeout(resolved, 250); setTimeout(resolved, 250);
else else
setImmediate(resolved); setImmediate(resolved);
@ -133,7 +133,7 @@ export namespace ui {
startTrackWindowBounds('ui-load-window', gui); startTrackWindowBounds('ui-load-window', gui);
const call_loader = () => load_files().catch(reject); const call_loader = () => load_files().catch(reject);
if(!process_args.has_flag(...Arguments.DISABLE_ANIMATION)) if(!processArguments.has_flag(...Arguments.DISABLE_ANIMATION))
setTimeout(call_loader, 1000); setTimeout(call_loader, 1000);
else else
setImmediate(call_loader); setImmediate(call_loader);

View File

@ -1,4 +1,4 @@
import {is_debug} from "../main_window"; import {is_debug} from "../main-window";
import * as moment from "moment"; import * as moment from "moment";
import * as request from "request"; import * as request from "request";
import * as querystring from "querystring"; import * as querystring from "querystring";
@ -8,7 +8,7 @@ const UUID = require('pure-uuid');
import * as path from "path"; import * as path from "path";
import * as zlib from "zlib"; import * as zlib from "zlib";
import * as tar from "tar-stream"; import * as tar from "tar-stream";
import {Arguments, process_args} from "../../shared/process-arguments"; import {Arguments, processArguments} from "../../shared/process-arguments";
import {parse_version} from "../../shared/version"; import {parse_version} from "../../shared/version";
import * as electron from "electron"; import * as electron from "electron";
@ -27,7 +27,7 @@ const remote_url: RemoteURL = () => {
if(remote_url.cached) if(remote_url.cached)
return remote_url.cached; return remote_url.cached;
const default_path = is_debug ? "http://localhost/home/TeaSpeak/Web-Client/client-api/environment/" : "https://clientapi.teaspeak.de/"; const default_path = is_debug ? "http://localhost/home/TeaSpeak/Web-Client/client-api/environment/" : "https://clientapi.teaspeak.de/";
return remote_url.cached = (process_args.has_value(...Arguments.SERVER_URL) ? process_args.value(...Arguments.SERVER_URL) : default_path); return remote_url.cached = (processArguments.has_value(...Arguments.SERVER_URL) ? processArguments.value(...Arguments.SERVER_URL) : default_path);
}; };
export interface VersionedFile { export interface VersionedFile {
@ -82,7 +82,7 @@ function get_raw_app_files() : Promise<VersionedFile[]> {
} }
if(response.statusCode != 200) { setImmediate(reject, "invalid status code " + response.statusCode + " for " + url); return; } if(response.statusCode != 200) { setImmediate(reject, "invalid status code " + response.statusCode + " for " + url); return; }
if(parseInt(response.headers["info-version"] as string) != 1 && !process_args.has_flag(Arguments.UPDATER_UI_IGNORE_VERSION)) { setImmediate(reject, "Invalid response version (" + response.headers["info-version"] + "). Update your app manually!"); return; } if(parseInt(response.headers["info-version"] as string) != 1 && !processArguments.has_flag(Arguments.UPDATER_UI_IGNORE_VERSION)) { setImmediate(reject, "Invalid response version (" + response.headers["info-version"] + "). Update your app manually!"); return; }
if(!body) { if(!body) {
setImmediate(reject, "invalid body. (Missing)"); setImmediate(reject, "invalid body. (Missing)");
return; return;
@ -461,7 +461,7 @@ async function load_cached_or_remote_ui_pack(channel: string, stats_update: (mes
const required_version = parse_version(e.pack_info.min_client_version); const required_version = parse_version(e.pack_info.min_client_version);
return client_version.in_dev() || client_version.newer_than(required_version) || client_version.equals(required_version); return client_version.in_dev() || client_version.newer_than(required_version) || client_version.equals(required_version);
}); });
if(process_args.has_flag(Arguments.UPDATER_UI_NO_CACHE)) { if(processArguments.has_flag(Arguments.UPDATER_UI_NO_CACHE)) {
console.log("Ignoring local UI cache"); console.log("Ignoring local UI cache");
available_versions = []; available_versions = [];
} }
@ -574,7 +574,7 @@ enum UILoaderMethod {
} }
export async function load_files(channel: string, stats_update: (message: string, index: number) => any) : Promise<String> { export async function load_files(channel: string, stats_update: (message: string, index: number) => any) : Promise<String> {
let enforced_loading_method = parseInt(process_args.has_value(Arguments.UPDATER_UI_LOAD_TYPE) ? process_args.value(Arguments.UPDATER_UI_LOAD_TYPE) : "-1") as UILoaderMethod; let enforced_loading_method = parseInt(processArguments.has_value(Arguments.UPDATER_UI_LOAD_TYPE) ? processArguments.value(Arguments.UPDATER_UI_LOAD_TYPE) : "-1") as UILoaderMethod;
if(typeof UILoaderMethod[enforced_loading_method] !== "undefined") { if(typeof UILoaderMethod[enforced_loading_method] !== "undefined") {
switch (enforced_loading_method) { switch (enforced_loading_method) {

View File

@ -1,19 +1,19 @@
/* --------------- bootstrap --------------- */ /* --------------- bootstrap --------------- */
import * as RequireProxy from "../renderer/RequireProxy"; import * as RequireProxy from "../renderer/RequireProxy";
import * as path from "path"; import * as path from "path";
RequireProxy.initialize(path.join(__dirname, "backend-impl")); RequireProxy.initialize(path.join(__dirname, "backend-impl"), "modal-external");
/* --------------- entry point --------------- */ /* --------------- entry point --------------- */
import * as loader from "tc-loader"; import * as loader from "tc-loader";
import {Stage} from "tc-loader"; import {Stage} from "tc-loader";
import {Arguments, process_args} from "../shared/process-arguments"; import {Arguments, processArguments} from "../shared/process-arguments";
import {remote} from "electron"; import {remote} from "electron";
export function initialize(manifestTarget: string) { export function initialize() {
console.log("Initializing native client for manifest target %s", manifestTarget); console.log("Initializing native client");
const _impl = message => { const _impl = message => {
if(!process_args.has_flag(Arguments.DEBUG)) { if(!processArguments.has_flag(Arguments.DEBUG)) {
console.error("Displaying critical error: %o", message); console.error("Displaying critical error: %o", message);
message = message.replace(/<br>/i, "\n"); message = message.replace(/<br>/i, "\n");
@ -33,10 +33,11 @@ export function initialize(manifestTarget: string) {
} }
}; };
if(window.impl_display_critical_error) if(window.impl_display_critical_error) {
window.impl_display_critical_error = _impl; window.impl_display_critical_error = _impl;
else } else {
window.displayCriticalError = _impl; window.displayCriticalError = _impl;
}
loader.register_task(loader.Stage.JAVASCRIPT, { loader.register_task(loader.Stage.JAVASCRIPT, {
name: "teaclient jquery", name: "teaclient jquery",
@ -51,10 +52,11 @@ export function initialize(manifestTarget: string) {
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "handler initialize", name: "handler initialize",
priority: 100, priority: 80,
function: async () => { function: async () => {
await import("../renderer/Logger"); await import("../renderer/Logger");
await import("../renderer/PersistentLocalStorage"); await import("../renderer/PersistentLocalStorage");
await import("../renderer/ContextMenu");
} }
}) })
} }

View File

@ -0,0 +1,12 @@
/* preloaded script, init hook will be called before the loader will be executed */
declare global {
interface Window {
__native_client_init_hook: () => void;
__native_client_init_shared: (webpackRequire: any) => void;
}
}
window.__native_client_init_hook = () => require("./index").initialize();
window.__native_client_init_shared = webpackRequire => window["shared-require"] = webpackRequire;
export = {};

View File

@ -0,0 +1,64 @@
import {ContextMenuEntry, ContextMenuFactory, setGlobalContextMenuFactory} from "tc-shared/ui/ContextMenu";
import * as electron from "electron";
import {MenuItemConstructorOptions} from "electron";
import {clientIconClassToImage} from "./IconHelper";
const {Menu} = electron.remote;
let currentMenu: electron.Menu;
function mapMenuEntry(entry: ContextMenuEntry) : MenuItemConstructorOptions {
switch (entry.type) {
case "normal":
return {
type: "normal",
label: typeof entry.label === "string" ? entry.label : entry.label.text,
enabled: entry.enabled,
visible: entry.visible,
click: entry.click,
icon: typeof entry.icon === "string" ? clientIconClassToImage(entry.icon) : undefined,
id: entry.uniqueId,
submenu: entry.subMenu ? entry.subMenu.map(mapMenuEntry).filter(e => !!e) : undefined
};
case "checkbox":
return {
type: "normal",
label: typeof entry.label === "string" ? entry.label : entry.label.text,
enabled: entry.enabled,
visible: entry.visible,
click: entry.click,
id: entry.uniqueId,
checked: entry.checked
};
case "separator":
return {
type: "separator"
};
default:
return undefined;
}
}
setGlobalContextMenuFactory(new class implements ContextMenuFactory {
closeContextMenu() {
currentMenu?.closePopup();
currentMenu = undefined;
}
spawnContextMenu(position: { pageX: number; pageY: number }, entries: ContextMenuEntry[], callbackClose?: () => void) {
this.closeContextMenu();
currentMenu = Menu.buildFromTemplate(entries.map(mapMenuEntry).filter(e => !!e));
currentMenu.popup({
callback: () => {
callbackClose();
currentMenu = undefined;
},
x: position.pageX,
y: position.pageY,
window: electron.remote.BrowserWindow.getFocusedWindow()
});
}
});

View File

@ -29,7 +29,7 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "native icon sprite loader", name: "native icon sprite loader",
function: async () => { function: async () => {
const image = new Image(); const image = new Image();
image.src = kClientSpriteUrl; image.src = loader.config.baseUrl + kClientSpriteUrl;
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
image.onload = resolve; image.onload = resolve;
image.onerror = () => reject("failed to load client icon sprite"); image.onerror = () => reject("failed to load client icon sprite");

View File

@ -1,7 +1,7 @@
import {clientIconClassToImage} from "./IconHelper"; import {clientIconClassToImage} from "./IconHelper";
import * as electron from "electron"; import * as electron from "electron";
import * as mbar from "tc-shared/ui/frames/MenuBar"; import * as mbar from "tc-shared/ui/frames/MenuBar";
import {Arguments, process_args} from "../shared/process-arguments"; import {Arguments, processArguments} from "../shared/process-arguments";
import ipcRenderer = electron.ipcRenderer; import ipcRenderer = electron.ipcRenderer;
import {LocalIcon} from "tc-shared/file/Icons"; import {LocalIcon} from "tc-shared/file/Icons";
@ -239,7 +239,7 @@ mbar.native_actions = {
call_basic_action("reload-window") call_basic_action("reload-window")
}, },
show_dev_tools() { return process_args.has_flag(Arguments.DEV_TOOLS); } show_dev_tools() { return processArguments.has_flag(Arguments.DEV_TOOLS); }
}; };

View File

@ -49,8 +49,10 @@ namespace proxied_load {
} }
let backend_root: string; let backend_root: string;
export function initialize(backend_root_: string) { let app_module: string;
export function initialize(backend_root_: string, app_module_: string) {
backend_root = backend_root_; backend_root = backend_root_;
app_module = app_module_;
proxied_load.original_load = Module._load; proxied_load.original_load = Module._load;
Module._load = proxied_load; Module._load = proxied_load;
@ -95,13 +97,17 @@ overrides.push({
}); });
function resolveModuleMapping(context: string, resource: string) { function resolveModuleMapping(context: string, resource: string) {
if(context.endsWith("/")) if(context.endsWith("/")) {
context = context.substring(0, context.length - 1); context = context.substring(0, context.length - 1);
}
const loader = require("tc-loader"); const loader = require("tc-loader");
const mapping = loader.module_mapping().find(e => e.application === "client-app"); //FIXME: Variable name! const mapping = loader.module_mapping().find(e => e.application === app_module); //FIXME: Variable name!
if(!mapping) throw "missing mapping"; if(!mapping) {
debugger;
throw "missing ui module mapping";
}
const entries = mapping.modules.filter(e => e.context === context); const entries = mapping.modules.filter(e => e.context === context);
if(!entries.length) { if(!entries.length) {

View File

@ -1,6 +1,6 @@
import {Settings, settings} from "tc-shared/settings"; import {Settings, settings} from "tc-shared/settings";
import {tr} from "tc-shared/i18n/localize"; import {tr} from "tc-shared/i18n/localize";
import {Arguments, process_args} from "../shared/process-arguments"; import {Arguments, processArguments} from "../shared/process-arguments";
import {remote} from "electron"; import {remote} from "electron";
import {server_connections} from "tc-shared/ConnectionManager"; import {server_connections} from "tc-shared/ConnectionManager";
@ -36,7 +36,7 @@ window.onbeforeunload = event => {
} }
}; };
if(process_args.has_flag(Arguments.DEBUG)) { if(processArguments.has_flag(Arguments.DEBUG)) {
do_exit(false); do_exit(false);
return; return;
} }

View File

@ -18,10 +18,12 @@ import {LogCategory, logWarn} from "tc-shared/log";
import NativeFilterMode = audio.record.FilterMode; import NativeFilterMode = audio.record.FilterMode;
export class NativeInput implements AbstractInput { export class NativeInput implements AbstractInput {
static readonly instances = [] as NativeInput[];
readonly events: Registry<InputEvents>; readonly events: Registry<InputEvents>;
private nativeHandle: audio.record.AudioRecorder; readonly nativeHandle: audio.record.AudioRecorder;
private nativeConsumer: audio.record.AudioConsumer; readonly nativeConsumer: audio.record.AudioConsumer;
private state: InputState; private state: InputState;
private deviceId: string | undefined; private deviceId: string | undefined;
@ -35,6 +37,9 @@ export class NativeInput implements AbstractInput {
this.nativeHandle = audio.record.create_recorder(); this.nativeHandle = audio.record.create_recorder();
this.nativeConsumer = this.nativeHandle.create_consumer(); this.nativeConsumer = this.nativeHandle.create_consumer();
this.nativeConsumer.toggle_rnnoise(true);
(window as any).consumer = this.nativeConsumer; /* FIXME! */
this.nativeConsumer.callback_ended = () => { this.nativeConsumer.callback_ended = () => {
this.filtered = true; this.filtered = true;
this.events.fire("notify_voice_end"); this.events.fire("notify_voice_end");
@ -45,6 +50,14 @@ export class NativeInput implements AbstractInput {
}; };
this.state = InputState.PAUSED; this.state = InputState.PAUSED;
NativeInput.instances.push(this);
}
destroy() {
const index = NativeInput.instances.indexOf(this);
if(index !== -1) {
NativeInput.instances.splice(index, 1);
}
} }
async start(): Promise<InputStartResult> { async start(): Promise<InputStartResult> {
@ -214,29 +227,33 @@ export class NativeInput implements AbstractInput {
} }
export class NativeLevelMeter implements LevelMeter { export class NativeLevelMeter implements LevelMeter {
readonly _device: IDevice; static readonly instances: NativeLevelMeter[] = [];
readonly targetDevice: IDevice;
private _callback: (num: number) => any; public nativeRecorder: audio.record.AudioRecorder;
private _recorder: audio.record.AudioRecorder; public nativeConsumer: audio.record.AudioConsumer;
private _consumer: audio.record.AudioConsumer;
private _filter: audio.record.ThresholdConsumeFilter; private callback: (num: number) => any;
private nativeFilter: audio.record.ThresholdConsumeFilter;
constructor(device: IDevice) { constructor(device: IDevice) {
this._device = device; this.targetDevice = device;
} }
async initialize() { async initialize() {
try { try {
this._recorder = audio.record.create_recorder(); this.nativeRecorder = audio.record.create_recorder();
this._consumer = this._recorder.create_consumer(); this.nativeConsumer = this.nativeRecorder.create_consumer();
this._filter = this._consumer.create_filter_threshold(.5); this.nativeConsumer.toggle_rnnoise(true); /* FIXME! */
this._filter.set_attack_smooth(.75);
this._filter.set_release_smooth(.75);
await new Promise(resolve => this._recorder.set_device(this._device.deviceId, resolve)); this.nativeFilter = this.nativeConsumer.create_filter_threshold(.5);
this.nativeFilter.set_attack_smooth(.75);
this.nativeFilter.set_release_smooth(.75);
await new Promise(resolve => this.nativeRecorder.set_device(this.targetDevice.deviceId, resolve));
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
this._recorder.start(flag => { this.nativeRecorder.start(flag => {
if (typeof flag === "boolean" && flag) if (typeof flag === "boolean" && flag)
resolve(); resolve();
else else
@ -246,40 +263,46 @@ export class NativeLevelMeter implements LevelMeter {
} catch (error) { } catch (error) {
if (typeof (error) === "string") if (typeof (error) === "string")
throw error; throw error;
console.warn(tr("Failed to initialize levelmeter for device %o: %o"), this._device, error); console.warn(tr("Failed to initialize levelmeter for device %o: %o"), this.targetDevice, error);
throw "initialize failed (lookup console)"; throw "initialize failed (lookup console)";
} }
/* references this variable, needs a destory() call, else memory leak */ /* references this variable, needs a destory() call, else memory leak */
this._filter.set_analyze_filter(value => { this.nativeFilter.set_analyze_filter(value => {
(this._callback || (() => { if(this.callback) this.callback(value);
}))(value);
}); });
NativeLevelMeter.instances.push(this);
} }
destroy() { destroy() {
if (this._filter) { const index = NativeLevelMeter.instances.indexOf(this);
this._filter.set_analyze_filter(undefined); if(index !== -1) {
this._consumer.unregister_filter(this._filter); NativeLevelMeter.instances.splice(index, 1);
} }
if (this._consumer) { if (this.nativeFilter) {
this._recorder.delete_consumer(this._consumer); this.nativeFilter.set_analyze_filter(undefined);
this.nativeConsumer.unregister_filter(this.nativeFilter);
} }
if(this._recorder) { if (this.nativeConsumer) {
this._recorder.stop(); this.nativeRecorder.delete_consumer(this.nativeConsumer);
} }
this._recorder = undefined;
this._consumer = undefined; if(this.nativeRecorder) {
this._filter = undefined; this.nativeRecorder.stop();
}
this.nativeRecorder = undefined;
this.nativeConsumer = undefined;
this.nativeFilter = undefined;
} }
getDevice(): IDevice { getDevice(): IDevice {
return this._device; return this.targetDevice;
} }
setObserver(callback: (value: number) => any) { setObserver(callback: (value: number) => any) {
this._callback = callback; this.callback = callback;
} }
} }

View File

@ -12,7 +12,7 @@ import {NativeInput} from "../audio/AudioRecorder";
import {ConnectionState} from "tc-shared/ConnectionHandler"; import {ConnectionState} from "tc-shared/ConnectionHandler";
import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer"; import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer";
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import {LogCategory, logInfo, logWarn} from "tc-shared/log"; import {LogCategory, logDebug, logInfo, logWarn} from "tc-shared/log";
import {tr} from "tc-shared/i18n/localize"; import {tr} from "tc-shared/i18n/localize";
export class NativeVoiceConnectionWrapper extends AbstractVoiceConnection { export class NativeVoiceConnectionWrapper extends AbstractVoiceConnection {
@ -83,11 +83,15 @@ export class NativeVoiceConnectionWrapper extends AbstractVoiceConnection {
} }
if(this.currentRecorder) { if(this.currentRecorder) {
this.currentRecorder.callback_unmount = undefined;
this.native.set_audio_source(undefined);
this.handleVoiceEndEvent();
await this.currentRecorder.unmount(); await this.currentRecorder.unmount();
this.currentRecorder = undefined;
} }
this.handleVoiceEndEvent(); await recorder?.unmount();
this.currentRecorder = recorder; this.currentRecorder = recorder;
try { try {
@ -97,14 +101,10 @@ export class NativeVoiceConnectionWrapper extends AbstractVoiceConnection {
throw "Recorder input must be an instance of NativeInput!"; throw "Recorder input must be an instance of NativeInput!";
} }
await recorder.unmount();
recorder.current_handler = this.connection.client; recorder.current_handler = this.connection.client;
recorder.callback_unmount = () => { recorder.callback_unmount = () => {
this.currentRecorder = undefined; logDebug(LogCategory.VOICE, tr("Lost voice recorder..."));
this.native.set_audio_source(undefined); this.acquireVoiceRecorder(undefined);
this.handleVoiceEndEvent();
}; };
recorder.callback_start = this.handleVoiceStartEvent.bind(this); recorder.callback_start = this.handleVoiceStartEvent.bind(this);
@ -116,7 +116,7 @@ export class NativeVoiceConnectionWrapper extends AbstractVoiceConnection {
this.currentRecorder = undefined; this.currentRecorder = undefined;
throw error; throw error;
} }
this.events.fire("notify_recorder_changed", {}) this.events.fire("notify_recorder_changed", {});
} }
voiceRecorder(): RecorderProfile { voiceRecorder(): RecorderProfile {
@ -259,6 +259,8 @@ class NativeVoiceClientWrapper implements VoiceClient {
break; break;
} }
} }
this.resetLatencySettings();
} }
destroy() { destroy() {

View File

@ -1,128 +0,0 @@
import {clientIconClassToImage} from "./IconHelper";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import * as electron from "electron";
const remote = electron.remote;
const {Menu, MenuItem} = remote;
class ElectronContextMenu implements contextmenu.ContextMenuProvider {
private _close_listeners: (() => any)[] = [];
private _current_menu: electron.Menu;
private _div: JQuery;
despawn_context_menu() {
if(!this._current_menu)
return;
this._current_menu.closePopup();
this._current_menu = undefined;
for(const listener of this._close_listeners) {
if(listener) {
try {
listener();
} catch (e) {
console.error("Failed to call context menu close listener: %o", e);
}
}
}
this._close_listeners = [];
}
finalize() {
if(this._div) this._div.detach();
this._div = undefined;
}
initialize() {
}
private _entry_id = 0;
private build_menu(entry: contextmenu.MenuEntry) : electron.MenuItem {
if(entry.type == contextmenu.MenuEntryType.CLOSE) {
this._close_listeners.push(entry.callback);
return undefined;
}
const click_callback = () => {
if(entry.callback)
entry.callback();
this.despawn_context_menu();
};
const _id = "entry_" + (this._entry_id++);
if(entry.type == contextmenu.MenuEntryType.ENTRY) {
return new MenuItem({
id: _id,
label: (typeof entry.name === "function" ? (entry.name as (() => string))() : entry.name) as string,
type: "normal",
click: click_callback,
icon: clientIconClassToImage(entry.icon_class),
visible: entry.visible,
enabled: !entry.disabled
});
} else if(entry.type == contextmenu.MenuEntryType.HR) {
if(typeof(entry.visible) === "boolean" && !entry.visible)
return undefined;
return new MenuItem({
id: _id,
type: "separator",
label: '',
click: click_callback
})
} else if(entry.type == contextmenu.MenuEntryType.CHECKBOX) {
return new MenuItem({
id: _id,
label: (typeof entry.name === "function" ? (entry.name as (() => string))() : entry.name) as string,
type: "checkbox",
checked: !!entry.checkbox_checked,
click: click_callback,
icon: clientIconClassToImage(entry.icon_class),
visible: entry.visible,
enabled: !entry.disabled
});
} else if (entry.type == contextmenu.MenuEntryType.SUB_MENU) {
const sub_menu = new Menu();
for(const e of entry.sub_menu) {
const build = this.build_menu(e);
if(!build)
continue;
sub_menu.append(build);
}
return new MenuItem({
id: _id,
label: (typeof entry.name === "function" ? (entry.name as (() => string))() : entry.name) as string,
type: "submenu",
submenu: sub_menu,
click: click_callback,
icon: clientIconClassToImage(entry.icon_class),
visible: entry.visible,
enabled: !entry.disabled
});
}
return undefined;
}
spawn_context_menu(x: number, y: number, ...entries: contextmenu.MenuEntry[]) {
this.despawn_context_menu();
this._current_menu = new Menu();
for(const entry of entries) {
const build = this.build_menu(entry);
if(!build)
continue;
this._current_menu.append(build);
}
this._current_menu.popup({
window: remote.getCurrentWindow(),
x: x,
y: y,
callback: () => this.despawn_context_menu()
});
}
html_format_enabled() { return false; }
}
contextmenu.set_provider(new ElectronContextMenu());

View File

@ -17,4 +17,13 @@ setRecorderBackend(new class implements AudioRecorderBacked {
getDeviceList(): DeviceList { getDeviceList(): DeviceList {
return inputDeviceList; return inputDeviceList;
} }
isRnNoiseSupported(): boolean {
return true;
}
toggleRnNoise(target: boolean) {
NativeLevelMeter.instances.forEach(input => input.nativeConsumer.toggle_rnnoise(target));
NativeInput.instances.forEach(input => input.nativeConsumer.toggle_rnnoise(target));
}
}); });

View File

@ -1,13 +1,13 @@
/* --------------- bootstrap --------------- */ /* --------------- bootstrap --------------- */
import * as RequireProxy from "./RequireProxy"; import * as RequireProxy from "./RequireProxy";
import * as crash_handler from "../crash_handler"; import * as crash_handler from "../crash_handler";
import {Arguments, parse_arguments, process_args} from "../shared/process-arguments"; import {Arguments, parseProcessArguments, processArguments} from "../shared/process-arguments";
import * as path from "path"; import * as path from "path";
import * as Sentry from "@sentry/electron";
import * as electron from "electron"; import * as electron from "electron";
import {remote} from "electron"; import {remote} from "electron";
/* /*
import * as Sentry from "@sentry/electron";
Sentry.init({ Sentry.init({
dsn: "https://72b9f40ce6894b179154e7558f1aeb87@o437344.ingest.sentry.io/5399791", dsn: "https://72b9f40ce6894b179154e7558f1aeb87@o437344.ingest.sentry.io/5399791",
appName: "TeaSpeak - Client", appName: "TeaSpeak - Client",
@ -16,13 +16,13 @@ Sentry.init({
*/ */
/* first of all setup crash handler */ /* first of all setup crash handler */
parse_arguments(); parseProcessArguments();
if(!process_args.has_flag(Arguments.NO_CRASH_RENDERER)) { if(!processArguments.has_flag(Arguments.NO_CRASH_RENDERER)) {
const is_electron_run = process.argv[0].endsWith("electron") || process.argv[0].endsWith("electron.exe"); const is_electron_run = process.argv[0].endsWith("electron") || process.argv[0].endsWith("electron.exe");
crash_handler.initialize_handler("renderer", is_electron_run); crash_handler.initialize_handler("renderer", is_electron_run);
} }
RequireProxy.initialize(path.join(__dirname, "backend-impl")); RequireProxy.initialize(path.join(__dirname, "backend-impl"), "client-app");
/* --------------- main initialize --------------- */ /* --------------- main initialize --------------- */
import * as loader from "tc-loader"; import * as loader from "tc-loader";
@ -73,7 +73,7 @@ loader.register_task(loader.Stage.INITIALIZING, {
name: "teaclient initialize error", name: "teaclient initialize error",
function: async () => { function: async () => {
const _impl = message => { const _impl = message => {
if(!process_args.has_flag(Arguments.DEBUG)) { if(!processArguments.has_flag(Arguments.DEBUG)) {
console.error("Displaying critical error: %o", message); console.error("Displaying critical error: %o", message);
message = message.replace(/<br>/i, "\n"); message = message.replace(/<br>/i, "\n");
@ -92,10 +92,11 @@ loader.register_task(loader.Stage.INITIALIZING, {
} }
}; };
if(window.impl_display_critical_error) if(window.impl_display_critical_error) {
window.impl_display_critical_error = _impl; window.impl_display_critical_error = _impl;
else } else {
window.displayCriticalError = _impl; window.displayCriticalError = _impl;
}
}, },
priority: 100 priority: 100
}); });
@ -103,14 +104,14 @@ loader.register_task(loader.Stage.INITIALIZING, {
loader.register_task(loader.Stage.INITIALIZING, { loader.register_task(loader.Stage.INITIALIZING, {
name: "teaclient initialize arguments", name: "teaclient initialize arguments",
function: async () => { function: async () => {
if(process_args.has_value(Arguments.DUMMY_CRASH_RENDERER)) if(processArguments.has_value(Arguments.DUMMY_CRASH_RENDERER))
crash_handler.handler.crash(); crash_handler.handler.crash();
/* loader url setup */ /* loader url setup */
{ {
const baseUrl = process_args.value(Arguments.SERVER_URL); const baseUrl = processArguments.value(Arguments.SERVER_URL);
console.error(process_args.value(Arguments.UPDATER_UI_LOAD_TYPE)); console.error(processArguments.value(Arguments.UPDATER_UI_LOAD_TYPE));
if(typeof baseUrl === "string" && parseFloat((process_args.value(Arguments.UPDATER_UI_LOAD_TYPE)?.toString() || "").trim()) === 3) { if(typeof baseUrl === "string" && parseFloat((processArguments.value(Arguments.UPDATER_UI_LOAD_TYPE)?.toString() || "").trim()) === 3) {
loader.config.baseUrl = baseUrl; loader.config.baseUrl = baseUrl;
} }
} }
@ -121,7 +122,7 @@ loader.register_task(loader.Stage.INITIALIZING, {
loader.register_task(loader.Stage.INITIALIZING, { loader.register_task(loader.Stage.INITIALIZING, {
name: 'gdb-waiter', name: 'gdb-waiter',
function: async () => { function: async () => {
if(process_args.has_flag(Arguments.DEV_TOOLS_GDB)) { if(processArguments.has_flag(Arguments.DEV_TOOLS_GDB)) {
console.log("Process ID: %d", process.pid); console.log("Process ID: %d", process.pid);
await new Promise(resolve => { await new Promise(resolve => {
console.log("Waiting for continue!"); console.log("Waiting for continue!");
@ -154,7 +155,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
try { try {
await import("./version"); await import("./version");
await import("./MenuBarHandler"); await import("./MenuBarHandler");
await import("./context-menu"); await import("./ContextMenu");
await import("./SingleInstanceHandler"); await import("./SingleInstanceHandler");
await import("./IconHelper"); await import("./IconHelper");
await import("./connection/FileTransfer"); await import("./connection/FileTransfer");
@ -174,6 +175,4 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
remote.getCurrentWindow().on('focus', () => remote.getCurrentWindow().flashFrame(false)); remote.getCurrentWindow().on('focus', () => remote.getCurrentWindow().flashFrame(false));
}, },
priority: 60 priority: 60
}); });
export async function initialize() { }

View File

@ -29,12 +29,12 @@ export interface Window {
process_args: Arguments; process_args: Arguments;
} }
export const process_args: Arguments = {} as Arguments; export const processArguments: Arguments = {} as Arguments;
export function parse_arguments() { export function parseProcessArguments() {
if(!process || !process.type || process.type === 'renderer') { if(!process || !process.type || process.type === 'renderer') {
Object.assign(process_args, electron.remote.getGlobal("process_arguments")); Object.assign(processArguments, electron.remote.getGlobal("process_arguments"));
(window as any).process_args = process_args; (window as any).process_args = processArguments;
} else { } else {
const is_electron_run = process.argv[0].endsWith("electron") || process.argv[0].endsWith("electron.exe"); const is_electron_run = process.argv[0].endsWith("electron") || process.argv[0].endsWith("electron.exe");
@ -46,15 +46,15 @@ export function parse_arguments() {
}) as Arguments; }) as Arguments;
args.has_flag = (...keys) => { args.has_flag = (...keys) => {
for(const key of [].concat(...Array.of(...keys).map(e => Array.isArray(e) ? Array.of(...e) : [e]))) for(const key of [].concat(...Array.of(...keys).map(e => Array.isArray(e) ? Array.of(...e) : [e])))
if(typeof process_args[key as any as string] === "boolean") if(typeof processArguments[key as any as string] === "boolean")
return process_args[key as any as string]; return processArguments[key as any as string];
return false; return false;
}; };
args.value = (...keys) => { args.value = (...keys) => {
for(const key of [].concat(...Array.of(...keys).map(e => Array.isArray(e) ? Array.of(...e) : [e]))) for(const key of [].concat(...Array.of(...keys).map(e => Array.isArray(e) ? Array.of(...e) : [e])))
if(typeof process_args[key] !== "undefined") if(typeof processArguments[key] !== "undefined")
return process_args[key]; return processArguments[key];
return undefined; return undefined;
}; };
@ -78,11 +78,11 @@ export function parse_arguments() {
} }
} }
console.log("Parsed CMD arguments %o as %o", process.argv, args); console.log("Parsed CMD arguments %o as %o", process.argv, args);
Object.assign(process_args, args); Object.assign(processArguments, args);
Object.assign(global["process_arguments"] = {}, args); Object.assign(global["process_arguments"] = {}, args);
} }
if(process_args.has_flag("help", "h")) { if(processArguments.has_flag("help", "h")) {
console.log("TeaClient command line help page"); console.log("TeaClient command line help page");
console.log(" -h --help => Displays this page"); console.log(" -h --help => Displays this page");
console.log(" -d --debug => Enabled the application debug"); console.log(" -d --debug => Enabled the application debug");

View File

@ -109,11 +109,14 @@ include_directories(${TeaSpeak_SharedLib_INCLUDE_DIR})
find_package(StringVariable REQUIRED) find_package(StringVariable REQUIRED)
include_directories(${StringVariable_INCLUDE_DIR}) include_directories(${StringVariable_INCLUDE_DIR})
find_package(Ed25519 REQUIRED) find_package(ed25519 REQUIRED)
include_directories(${ed25519_INCLUDE_DIR}) include_directories(${ed25519_INCLUDE_DIR})
find_package(ThreadPool REQUIRED) find_package(ThreadPool REQUIRED)
include_directories(${ThreadPool_INCLUDE_DIR}) include_directories(${ThreadPool_INCLUDE_DIR})
find_package(rnnoise REQUIRED)
if (WIN32) if (WIN32)
add_compile_options(/NODEFAULTLIB:ThreadPoolStatic) add_compile_options(/NODEFAULTLIB:ThreadPoolStatic)
add_definitions(-DWINDOWS) #Required by ThreadPool add_definitions(-DWINDOWS) #Required by ThreadPool
@ -121,13 +124,13 @@ if (WIN32)
add_definitions(-D_SILENCE_CXX17_OLD_ALLOCATOR_MEMBERS_DEPRECATION_WARNING) # For the FMT library add_definitions(-D_SILENCE_CXX17_OLD_ALLOCATOR_MEMBERS_DEPRECATION_WARNING) # For the FMT library
endif () endif ()
find_package(Soxr REQUIRED) find_package(soxr REQUIRED)
include_directories(${soxr_INCLUDE_DIR}) include_directories(${soxr_INCLUDE_DIR})
find_package(fvad REQUIRED) find_package(fvad REQUIRED)
include_directories(${fvad_INCLUDE_DIR}) include_directories(${fvad_INCLUDE_DIR})
find_package(Opus REQUIRED) find_package(opus REQUIRED)
include_directories(${opus_INCLUDE_DIR}) include_directories(${opus_INCLUDE_DIR})
find_package(spdlog REQUIRED) find_package(spdlog REQUIRED)
@ -148,6 +151,7 @@ set(REQUIRED_LIBRARIES
${opus_LIBRARIES_STATIC} ${opus_LIBRARIES_STATIC}
${ed25519_LIBRARIES_STATIC} ${ed25519_LIBRARIES_STATIC}
rnnoise
spdlog::spdlog_header_only spdlog::spdlog_header_only
Nan::Helpers Nan::Helpers

View File

@ -223,9 +223,9 @@ declare module "tc-native/connection" {
} }
export interface AudioConsumer { export interface AudioConsumer {
sample_rate: number; readonly sampleRate: number;
channels: number; readonly channelCount: number;
frame_size: number; readonly frameSize: number;
/* TODO add some kind of order to may improve CPU performance (Some filters are more intense then others) */ /* TODO add some kind of order to may improve CPU performance (Some filters are more intense then others) */
get_filters() : ConsumeFilter[]; get_filters() : ConsumeFilter[];
@ -238,6 +238,9 @@ declare module "tc-native/connection" {
set_filter_mode(mode: FilterMode); set_filter_mode(mode: FilterMode);
get_filter_mode() : FilterMode; get_filter_mode() : FilterMode;
toggle_rnnoise(enabled: boolean);
rnnoise_enabled() : boolean;
callback_data: (buffer: Float32Array) => any; callback_data: (buffer: Float32Array) => any;
callback_ended: () => any; callback_ended: () => any;
callback_started: () => any; callback_started: () => any;

View File

@ -33,10 +33,11 @@ void AudioConsumer::handle_framed_data(const void *buffer, size_t samples) {
} }
void AudioConsumer::process_data(const void *buffer, size_t samples) { void AudioConsumer::process_data(const void *buffer, size_t samples) {
if(this->reframer) if(this->reframer) {
this->reframer->process(buffer, samples); this->reframer->process(buffer, samples);
else } else {
this->handle_framed_data(buffer, samples); this->handle_framed_data(buffer, samples);
}
} }
AudioInput::AudioInput(size_t channels, size_t rate) : _channel_count(channels), _sample_rate(rate) {} AudioInput::AudioInput(size_t channels, size_t rate) : _channel_count(channels), _sample_rate(rate) {}
@ -51,7 +52,7 @@ AudioInput::~AudioInput() {
} }
void AudioInput::set_device(const std::shared_ptr<AudioDevice> &device) { void AudioInput::set_device(const std::shared_ptr<AudioDevice> &device) {
std::lock_guard lock(this->input_source_lock); std::lock_guard lock{this->input_source_lock};
if(device == this->input_device) return; if(device == this->input_device) return;
this->close_device(); this->close_device();
@ -59,7 +60,7 @@ void AudioInput::set_device(const std::shared_ptr<AudioDevice> &device) {
} }
void AudioInput::close_device() { void AudioInput::close_device() {
std::lock_guard lock(this->input_source_lock); std::lock_guard lock{this->input_source_lock};
if(this->input_recorder) { if(this->input_recorder) {
this->input_recorder->remove_consumer(this); this->input_recorder->remove_consumer(this);
this->input_recorder->stop_if_possible(); this->input_recorder->stop_if_possible();
@ -70,7 +71,7 @@ void AudioInput::close_device() {
} }
bool AudioInput::record(std::string& error) { bool AudioInput::record(std::string& error) {
std::lock_guard lock(this->input_source_lock); std::lock_guard lock{this->input_source_lock};
if(!this->input_device) { if(!this->input_device) {
error = "no device"; error = "no device";
return false; return false;

View File

@ -1,3 +1,4 @@
#include <rnnoise.h>
#include "AudioConsumer.h" #include "AudioConsumer.h"
#include "AudioRecorder.h" #include "AudioRecorder.h"
#include "AudioFilter.h" #include "AudioFilter.h"
@ -12,6 +13,10 @@ using namespace std;
using namespace tc::audio; using namespace tc::audio;
using namespace tc::audio::recorder; using namespace tc::audio::recorder;
inline v8::PropertyAttribute operator|(const v8::PropertyAttribute& a, const v8::PropertyAttribute& b) {
return (v8::PropertyAttribute) ((unsigned) a | (unsigned) b);
}
NAN_MODULE_INIT(AudioConsumerWrapper::Init) { NAN_MODULE_INIT(AudioConsumerWrapper::Init) {
auto klass = Nan::New<v8::FunctionTemplate>(AudioConsumerWrapper::NewInstance); auto klass = Nan::New<v8::FunctionTemplate>(AudioConsumerWrapper::NewInstance);
klass->SetClassName(Nan::New("AudioConsumer").ToLocalChecked()); klass->SetClassName(Nan::New("AudioConsumer").ToLocalChecked());
@ -27,6 +32,9 @@ NAN_MODULE_INIT(AudioConsumerWrapper::Init) {
Nan::SetPrototypeMethod(klass, "get_filter_mode", AudioConsumerWrapper::_get_filter_mode); Nan::SetPrototypeMethod(klass, "get_filter_mode", AudioConsumerWrapper::_get_filter_mode);
Nan::SetPrototypeMethod(klass, "set_filter_mode", AudioConsumerWrapper::_set_filter_mode); Nan::SetPrototypeMethod(klass, "set_filter_mode", AudioConsumerWrapper::_set_filter_mode);
Nan::SetPrototypeMethod(klass, "rnnoise_enabled", AudioConsumerWrapper::rnnoise_enabled);
Nan::SetPrototypeMethod(klass, "toggle_rnnoise", AudioConsumerWrapper::toggle_rnnoise);
constructor_template().Reset(klass); constructor_template().Reset(klass);
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked()); constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
} }
@ -39,7 +47,7 @@ NAN_METHOD(AudioConsumerWrapper::NewInstance) {
AudioConsumerWrapper::AudioConsumerWrapper(AudioRecorderWrapper* h, const std::shared_ptr<tc::audio::AudioConsumer> &handle) : _handle(handle), _recorder(h) { AudioConsumerWrapper::AudioConsumerWrapper(AudioRecorderWrapper* h, const std::shared_ptr<tc::audio::AudioConsumer> &handle) : _handle(handle), _recorder(h) {
log_allocate("AudioConsumerWrapper", this); log_allocate("AudioConsumerWrapper", this);
{ {
lock_guard read_lock(handle->on_read_lock); lock_guard read_lock{handle->on_read_lock};
handle->on_read = [&](const void* buffer, size_t length){ this->process_data(buffer, length); }; handle->on_read = [&](const void* buffer, size_t length){ this->process_data(buffer, length); };
} }
@ -51,16 +59,32 @@ AudioConsumerWrapper::AudioConsumerWrapper(AudioRecorderWrapper* h, const std::s
AudioConsumerWrapper::~AudioConsumerWrapper() { AudioConsumerWrapper::~AudioConsumerWrapper() {
log_free("AudioConsumerWrapper", this); log_free("AudioConsumerWrapper", this);
lock_guard lock(this->execute_lock); lock_guard lock{this->execute_mutex};
this->unbind(); this->unbind();
if(this->_handle->handle) { if(this->_handle->handle) {
this->_handle->handle->delete_consumer(this->_handle); this->_handle->handle->delete_consumer(this->_handle);
this->_handle = nullptr; this->_handle = nullptr;
} }
for(auto& instance : this->rnnoise_processor) {
if(!instance) { continue; }
rnnoise_destroy((DenoiseState*) instance);
instance = nullptr;
}
for(auto index{0}; index < kInternalFrameBufferCount; index++) {
if(!this->internal_frame_buffer[index]) { continue; }
free(this->internal_frame_buffer[index]);
this->internal_frame_buffer[index] = nullptr;
this->internal_frame_buffer_size[index] = 0;
}
#ifdef DO_DEADLOCK_REF #ifdef DO_DEADLOCK_REF
if(this->_recorder) if(this->_recorder) {
this->_recorder->js_unref(); this->_recorder->js_unref();
}
#endif #endif
} }
@ -119,20 +143,88 @@ void AudioConsumerWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
(void) callback_function.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr); (void) callback_function.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
}); });
Nan::Set(this->handle(), Nan::New<v8::String>("frame_size").ToLocalChecked(), Nan::New<v8::Number>((uint32_t) this->_handle->frame_size)); Nan::DefineOwnProperty(this->handle(), Nan::New<v8::String>("frameSize").ToLocalChecked(), Nan::New<v8::Number>((uint32_t) this->_handle->frame_size), v8::ReadOnly | v8::DontDelete);
Nan::Set(this->handle(), Nan::New<v8::String>("sample_rate").ToLocalChecked(), Nan::New<v8::Number>((uint32_t) this->_handle->sample_rate)); Nan::DefineOwnProperty(this->handle(), Nan::New<v8::String>("sampleRate").ToLocalChecked(), Nan::New<v8::Number>((uint32_t) this->_handle->sample_rate), v8::ReadOnly | v8::DontDelete);
Nan::Set(this->handle(), Nan::New<v8::String>("channels").ToLocalChecked(), Nan::New<v8::Number>((uint32_t) this->_handle->channel_count)); Nan::DefineOwnProperty(this->handle(), Nan::New<v8::String>("channelCount").ToLocalChecked(), Nan::New<v8::Number>((uint32_t) this->_handle->channel_count), v8::ReadOnly | v8::DontDelete);
} }
void AudioConsumerWrapper::unbind() { void AudioConsumerWrapper::unbind() {
if(this->_handle) { if(this->_handle) {
lock_guard lock(this->_handle->on_read_lock); lock_guard lock{this->_handle->on_read_lock};
this->_handle->on_read = nullptr; this->_handle->on_read = nullptr;
} }
} }
static const float kRnNoiseScale = -INT16_MIN;
void AudioConsumerWrapper::process_data(const void *buffer, size_t samples) { void AudioConsumerWrapper::process_data(const void *buffer, size_t samples) {
lock_guard lock(this->execute_lock); if(samples != 960) {
logger::error(logger::category::audio, tr("Received audio frame with invalid sample count (Expected 960, Received {})"), samples);
return;
}
lock_guard lock{this->execute_mutex};
if(this->filter_mode_ == FilterMode::BLOCK) { return; }
/* apply input modifiers */
if(this->rnnoise) {
/* TODO: don't call reserve_internal_buffer every time and assume the buffers are initialized */
/* TODO: Maybe find out if the microphone is some kind of pseudo stero so we can handle it as mono? */
if(this->_handle->channel_count > 1) {
auto channel_count = this->_handle->channel_count;
this->reserve_internal_buffer(0, samples * channel_count * sizeof(float));
this->reserve_internal_buffer(1, samples * channel_count * sizeof(float));
for(size_t channel{0}; channel < channel_count; channel++) {
auto target_buffer = (float*) this->internal_frame_buffer[1];
auto source_buffer = (const float*) buffer + channel;
for(size_t index{0}; index < samples; index++) {
*target_buffer = *source_buffer * kRnNoiseScale;
source_buffer += channel_count;
target_buffer++;
}
/* rnnoise uses a frame size of 480 */
this->initialize_rnnoise(channel);
rnnoise_process_frame((DenoiseState*) this->rnnoise_processor[channel], (float*) this->internal_frame_buffer[0] + channel * samples, (const float*) this->internal_frame_buffer[1]);
rnnoise_process_frame((DenoiseState*) this->rnnoise_processor[channel], (float*) this->internal_frame_buffer[0] + channel * samples + 480, (const float*) this->internal_frame_buffer[1] + 480);
}
const float* channel_buffer_ptr[kMaxChannelCount];
for(size_t channel{0}; channel < channel_count; channel++) {
channel_buffer_ptr[channel] = (const float*) this->internal_frame_buffer[0] + channel * samples;
}
/* now back again to interlanced */
auto target_buffer = (float*) this->internal_frame_buffer[1];
for(size_t index{0}; index < samples; index++) {
for(size_t channel{0}; channel < channel_count; channel++) {
*target_buffer = *(channel_buffer_ptr[channel]++) / kRnNoiseScale;
target_buffer++;
}
}
buffer = this->internal_frame_buffer[1];
} else {
/* rnnoise uses a frame size of 480 */
this->reserve_internal_buffer(0, samples * sizeof(float));
auto target_buffer = (float*) this->internal_frame_buffer[0];
for(size_t index{0}; index < samples; index++) {
target_buffer[index] = ((float*) buffer)[index] * kRnNoiseScale;
}
this->initialize_rnnoise(0);
rnnoise_process_frame((DenoiseState*) this->rnnoise_processor[0], target_buffer, target_buffer);
rnnoise_process_frame((DenoiseState*) this->rnnoise_processor[0], &target_buffer[480], &target_buffer[480]);
buffer = target_buffer;
for(size_t index{0}; index < samples; index++) {
target_buffer[index] /= kRnNoiseScale;
}
}
}
bool should_process{true}; bool should_process{true};
if(this->filter_mode_ == FilterMode::FILTER) { if(this->filter_mode_ == FilterMode::FILTER) {
@ -243,11 +335,26 @@ void AudioConsumerWrapper::delete_filter(const AudioFilterWrapper* filter) {
} }
{ {
lock_guard lock(this->execute_lock); /* ensure that the filter isn't used right now */ lock_guard lock(this->execute_mutex); /* ensure that the filter isn't used right now */
handle->_filter = nullptr; handle->_filter = nullptr;
} }
} }
void AudioConsumerWrapper::reserve_internal_buffer(int index, size_t target) {
assert(index < kInternalFrameBufferCount);
if(this->internal_frame_buffer_size[index] < target) {
if(this->internal_frame_buffer_size[index]) { ::free(this->internal_frame_buffer[index]); }
this->internal_frame_buffer[index] = malloc(target);
this->internal_frame_buffer_size[index] = target;
}
}
void AudioConsumerWrapper::initialize_rnnoise(int channel) {
if(!this->rnnoise_processor[channel]) {
this->rnnoise_processor[channel] = rnnoise_create(nullptr);
}
}
NAN_METHOD(AudioConsumerWrapper::_get_filters) { NAN_METHOD(AudioConsumerWrapper::_get_filters) {
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder()); auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
@ -352,4 +459,20 @@ NAN_METHOD(AudioConsumerWrapper::_set_filter_mode) {
auto value = info[0].As<v8::Number>()->ToInteger()->Value(); auto value = info[0].As<v8::Number>()->ToInteger()->Value();
handle->filter_mode_ = (FilterMode) value; handle->filter_mode_ = (FilterMode) value;
}
NAN_METHOD(AudioConsumerWrapper::rnnoise_enabled) {
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
info.GetReturnValue().Set(handle->rnnoise);
}
NAN_METHOD(AudioConsumerWrapper::toggle_rnnoise) {
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
if(info.Length() != 1 || !info[0]->IsBoolean()) {
Nan::ThrowError("invalid argument");
return;
}
handle->rnnoise = info[0]->BooleanValue();
} }

View File

@ -1,104 +1,117 @@
#pragma once #pragma once
#include <array>
#include <nan.h> #include <nan.h>
#include <mutex> #include <mutex>
#include <deque> #include <deque>
#include <include/NanEventCallback.h> #include <include/NanEventCallback.h>
namespace tc { namespace tc::audio {
namespace audio { class AudioInput;
class AudioInput; class AudioConsumer;
class AudioConsumer;
namespace filter { namespace filter {
class Filter; class Filter;
} }
namespace recorder { namespace recorder {
class AudioFilterWrapper; class AudioFilterWrapper;
class AudioRecorderWrapper; class AudioRecorderWrapper;
enum FilterMode { enum FilterMode {
BYPASS, BYPASS,
FILTER, FILTER,
BLOCK BLOCK
}; };
class AudioConsumerWrapper : public Nan::ObjectWrap { class AudioConsumerWrapper : public Nan::ObjectWrap {
friend class AudioRecorderWrapper; friend class AudioRecorderWrapper;
public: constexpr static auto kMaxChannelCount{2};
static NAN_MODULE_INIT(Init); public:
static NAN_METHOD(NewInstance); static NAN_MODULE_INIT(Init);
static inline Nan::Persistent<v8::Function> & constructor() { static NAN_METHOD(NewInstance);
static Nan::Persistent<v8::Function> my_constructor; static inline Nan::Persistent<v8::Function> & constructor() {
return my_constructor; static Nan::Persistent<v8::Function> my_constructor;
} return my_constructor;
}
static inline Nan::Persistent<v8::FunctionTemplate> & constructor_template() { static inline Nan::Persistent<v8::FunctionTemplate> & constructor_template() {
static Nan::Persistent<v8::FunctionTemplate> my_constructor_template; static Nan::Persistent<v8::FunctionTemplate> my_constructor_template;
return my_constructor_template; return my_constructor_template;
} }
AudioConsumerWrapper(AudioRecorderWrapper*, const std::shared_ptr<AudioConsumer>& /* handle */); AudioConsumerWrapper(AudioRecorderWrapper*, const std::shared_ptr<AudioConsumer>& /* handle */);
~AudioConsumerWrapper() override; ~AudioConsumerWrapper() override;
static NAN_METHOD(_get_filters); static NAN_METHOD(_get_filters);
static NAN_METHOD(_unregister_filter); static NAN_METHOD(_unregister_filter);
static NAN_METHOD(_create_filter_vad); static NAN_METHOD(_create_filter_vad);
static NAN_METHOD(_create_filter_threshold); static NAN_METHOD(_create_filter_threshold);
static NAN_METHOD(_create_filter_state); static NAN_METHOD(_create_filter_state);
static NAN_METHOD(_get_filter_mode); static NAN_METHOD(_get_filter_mode);
static NAN_METHOD(_set_filter_mode); static NAN_METHOD(_set_filter_mode);
std::shared_ptr<AudioFilterWrapper> create_filter(const std::string& /* name */, const std::shared_ptr<filter::Filter>& /* filter impl */); static NAN_METHOD(toggle_rnnoise);
void delete_filter(const AudioFilterWrapper*); static NAN_METHOD(rnnoise_enabled);
inline std::deque<std::shared_ptr<AudioFilterWrapper>> filters() { std::shared_ptr<AudioFilterWrapper> create_filter(const std::string& /* name */, const std::shared_ptr<filter::Filter>& /* filter impl */);
std::lock_guard lock(this->filter_mutex_); void delete_filter(const AudioFilterWrapper*);
return this->filter_;
}
inline FilterMode filter_mode() const { return this->filter_mode_; } inline std::deque<std::shared_ptr<AudioFilterWrapper>> filters() {
std::lock_guard lock(this->filter_mutex_);
return this->filter_;
}
inline std::shared_ptr<AudioConsumer> native_consumer() { return this->_handle; } inline FilterMode filter_mode() const { return this->filter_mode_; }
inline std::shared_ptr<AudioConsumer> native_consumer() { return this->_handle; }
std::mutex native_read_callback_lock; std::mutex native_read_callback_lock;
std::function<void(const void * /* buffer */, size_t /* samples */)> native_read_callback; std::function<void(const void * /* buffer */, size_t /* samples */)> native_read_callback;
private: private:
AudioRecorderWrapper* _recorder; AudioRecorderWrapper* _recorder;
std::mutex execute_lock; /* preprocessors */
std::shared_ptr<AudioConsumer> _handle; bool rnnoise{false};
std::array<void*, kMaxChannelCount> rnnoise_processor{nullptr};
std::mutex filter_mutex_; std::mutex execute_mutex;
std::deque<std::shared_ptr<AudioFilterWrapper>> filter_; std::shared_ptr<AudioConsumer> _handle;
FilterMode filter_mode_{FilterMode::FILTER};
bool last_consumed = false;
void do_wrap(const v8::Local<v8::Object>& /* object */); std::mutex filter_mutex_;
std::deque<std::shared_ptr<AudioFilterWrapper>> filter_;
FilterMode filter_mode_{FilterMode::FILTER};
bool last_consumed = false;
void unbind(); /* called with execute_lock locked */ constexpr static auto kInternalFrameBufferCount{2};
void process_data(const void* /* buffer */, size_t /* samples */); void* internal_frame_buffer[kInternalFrameBufferCount]{nullptr};
size_t internal_frame_buffer_size[kInternalFrameBufferCount]{0};
struct DataEntry { void do_wrap(const v8::Local<v8::Object>& /* object */);
void* buffer = nullptr;
size_t sample_count = 0;
~DataEntry() { void unbind(); /* called with execute_lock locked */
if(buffer) void process_data(const void* /* buffer */, size_t /* samples */);
free(buffer);
}
};
std::mutex _data_lock; void reserve_internal_buffer(int /* buffer */, size_t /* bytes */);
std::deque<std::unique_ptr<DataEntry>> _data_entries; void initialize_rnnoise(int /* channel */);
Nan::callback_t<> _call_data; struct DataEntry {
Nan::callback_t<> _call_ended; void* buffer = nullptr;
Nan::callback_t<> _call_started; size_t sample_count = 0;
};
} ~DataEntry() {
} if(buffer)
free(buffer);
}
};
std::mutex _data_lock;
std::deque<std::unique_ptr<DataEntry>> _data_entries;
Nan::callback_t<> _call_data;
Nan::callback_t<> _call_ended;
Nan::callback_t<> _call_started;
};
}
} }

View File

@ -3,67 +3,65 @@
#include <nan.h> #include <nan.h>
#include <include/NanEventCallback.h> #include <include/NanEventCallback.h>
namespace tc { namespace tc::audio {
namespace audio { namespace filter {
namespace filter { class Filter;
class Filter; }
}
namespace recorder { namespace recorder {
class AudioConsumerWrapper; class AudioConsumerWrapper;
class AudioFilterWrapper : public Nan::ObjectWrap { class AudioFilterWrapper : public Nan::ObjectWrap {
friend class AudioConsumerWrapper; friend class AudioConsumerWrapper;
public: public:
static NAN_MODULE_INIT(Init); static NAN_MODULE_INIT(Init);
static NAN_METHOD(NewInstance); static NAN_METHOD(NewInstance);
static inline Nan::Persistent<v8::Function> & constructor() { static inline Nan::Persistent<v8::Function> & constructor() {
static Nan::Persistent<v8::Function> my_constructor; static Nan::Persistent<v8::Function> my_constructor;
return my_constructor; return my_constructor;
} }
static inline Nan::Persistent<v8::FunctionTemplate> & constructor_template() { static inline Nan::Persistent<v8::FunctionTemplate> & constructor_template() {
static Nan::Persistent<v8::FunctionTemplate> my_constructor_template; static Nan::Persistent<v8::FunctionTemplate> my_constructor_template;
return my_constructor_template; return my_constructor_template;
} }
AudioFilterWrapper(const std::string& name, const std::shared_ptr<filter::Filter>& /* handle */); AudioFilterWrapper(const std::string& name, const std::shared_ptr<filter::Filter>& /* handle */);
~AudioFilterWrapper() override; ~AudioFilterWrapper() override;
static NAN_METHOD(_get_name); static NAN_METHOD(_get_name);
/* VAD and Threshold */ /* VAD and Threshold */
static NAN_METHOD(_get_margin_time); static NAN_METHOD(_get_margin_time);
static NAN_METHOD(_set_margin_time); static NAN_METHOD(_set_margin_time);
/* VAD relevant */ /* VAD relevant */
static NAN_METHOD(_get_level); static NAN_METHOD(_get_level);
/* threshold filter relevant */ /* threshold filter relevant */
static NAN_METHOD(_get_threshold); static NAN_METHOD(_get_threshold);
static NAN_METHOD(_set_threshold); static NAN_METHOD(_set_threshold);
static NAN_METHOD(_get_attack_smooth); static NAN_METHOD(_get_attack_smooth);
static NAN_METHOD(_set_attack_smooth); static NAN_METHOD(_set_attack_smooth);
static NAN_METHOD(_get_release_smooth); static NAN_METHOD(_get_release_smooth);
static NAN_METHOD(_set_release_smooth); static NAN_METHOD(_set_release_smooth);
static NAN_METHOD(_set_analyze_filter); static NAN_METHOD(_set_analyze_filter);
/* consume filter */ /* consume filter */
static NAN_METHOD(_is_consuming); static NAN_METHOD(_is_consuming);
static NAN_METHOD(_set_consuming); static NAN_METHOD(_set_consuming);
inline std::shared_ptr<filter::Filter> filter() { return this->_filter; } inline std::shared_ptr<filter::Filter> filter() { return this->_filter; }
private: private:
std::shared_ptr<filter::Filter> _filter; std::shared_ptr<filter::Filter> _filter;
std::string _name; std::string _name;
void do_wrap(const v8::Local<v8::Object>& /* object */); void do_wrap(const v8::Local<v8::Object>& /* object */);
Nan::callback_t<float> _call_analyzed; Nan::callback_t<float> _call_analyzed;
Nan::Persistent<v8::Function> _callback_analyzed; Nan::Persistent<v8::Function> _callback_analyzed;
}; };
} }
}
} }

View File

@ -19,7 +19,7 @@ NAN_METHOD(recorder::create_recorder) {
Nan::ThrowError(tr("audio hasn't been initialized yet")); Nan::ThrowError(tr("audio hasn't been initialized yet"));
return; return;
} }
auto input = make_shared<AudioInput>(2, 48000); auto input = std::make_shared<AudioInput>(2, 48000);
auto wrapper = new AudioRecorderWrapper(input); auto wrapper = new AudioRecorderWrapper(input);
auto js_object = Nan::NewInstance(Nan::New(AudioRecorderWrapper::constructor())).ToLocalChecked(); auto js_object = Nan::NewInstance(Nan::New(AudioRecorderWrapper::constructor())).ToLocalChecked();
wrapper->do_wrap(js_object); wrapper->do_wrap(js_object);
@ -55,24 +55,24 @@ NAN_METHOD(AudioRecorderWrapper::NewInstance) {
} }
AudioRecorderWrapper::AudioRecorderWrapper(std::shared_ptr<tc::audio::AudioInput> handle) : _input(std::move(handle)) { AudioRecorderWrapper::AudioRecorderWrapper(std::shared_ptr<tc::audio::AudioInput> handle) : input_(std::move(handle)) {
log_allocate("AudioRecorderWrapper", this); log_allocate("AudioRecorderWrapper", this);
} }
AudioRecorderWrapper::~AudioRecorderWrapper() { AudioRecorderWrapper::~AudioRecorderWrapper() {
if(this->_input) { if(this->input_) {
this->_input->stop(); this->input_->stop();
this->_input->close_device(); this->input_->close_device();
this->_input = nullptr; this->input_ = nullptr;
} }
{ {
lock_guard lock(this->_consumer_lock); lock_guard lock{this->consumer_mutex};
this->_consumers.clear(); this->consumer_.clear();
} }
log_free("AudioRecorderWrapper", this); log_free("AudioRecorderWrapper", this);
} }
std::shared_ptr<AudioConsumerWrapper> AudioRecorderWrapper::create_consumer() { std::shared_ptr<AudioConsumerWrapper> AudioRecorderWrapper::create_consumer() {
auto result = shared_ptr<AudioConsumerWrapper>(new AudioConsumerWrapper(this, this->_input->create_consumer(960)), [](AudioConsumerWrapper* ptr) { auto result = shared_ptr<AudioConsumerWrapper>(new AudioConsumerWrapper(this, this->input_->create_consumer(960)), [](AudioConsumerWrapper* ptr) {
assert(v8::Isolate::GetCurrent()); assert(v8::Isolate::GetCurrent());
ptr->Unref(); ptr->Unref();
}); });
@ -85,8 +85,8 @@ std::shared_ptr<AudioConsumerWrapper> AudioRecorderWrapper::create_consumer() {
} }
{ {
lock_guard lock(this->_consumer_lock); lock_guard lock(this->consumer_mutex);
this->_consumers.push_back(result); this->consumer_.push_back(result);
} }
return result; return result;
@ -95,8 +95,8 @@ std::shared_ptr<AudioConsumerWrapper> AudioRecorderWrapper::create_consumer() {
void AudioRecorderWrapper::delete_consumer(const AudioConsumerWrapper* consumer) { void AudioRecorderWrapper::delete_consumer(const AudioConsumerWrapper* consumer) {
shared_ptr<AudioConsumerWrapper> handle; /* need to keep the handle 'till everything has been finished */ shared_ptr<AudioConsumerWrapper> handle; /* need to keep the handle 'till everything has been finished */
{ {
lock_guard lock(this->_consumer_lock); lock_guard lock(this->consumer_mutex);
for(auto& c : this->_consumers) { for(auto& c : this->consumer_) {
if(&*c == consumer) { if(&*c == consumer) {
handle = c; handle = c;
break; break;
@ -106,16 +106,16 @@ void AudioRecorderWrapper::delete_consumer(const AudioConsumerWrapper* consumer)
return; return;
{ {
auto it = find(this->_consumers.begin(), this->_consumers.end(), handle); auto it = find(this->consumer_.begin(), this->consumer_.end(), handle);
if(it != this->_consumers.end()) if(it != this->consumer_.end())
this->_consumers.erase(it); this->consumer_.erase(it);
} }
} }
{ {
lock_guard lock(handle->execute_lock); /* if we delete the consumer while executing strange stuff could happen */ lock_guard lock(handle->execute_mutex); /* if we delete the consumer while executing strange stuff could happen */
handle->unbind(); handle->unbind();
this->_input->delete_consumer(handle->_handle); this->input_->delete_consumer(handle->_handle);
} }
} }
@ -125,7 +125,7 @@ void AudioRecorderWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
NAN_METHOD(AudioRecorderWrapper::_get_device) { NAN_METHOD(AudioRecorderWrapper::_get_device) {
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder()); auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
auto input = handle->_input; auto input = handle->input_;
auto device = input->current_device(); auto device = input->current_device();
if(device) if(device)
@ -136,7 +136,7 @@ NAN_METHOD(AudioRecorderWrapper::_get_device) {
NAN_METHOD(AudioRecorderWrapper::_set_device) { NAN_METHOD(AudioRecorderWrapper::_set_device) {
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder()); auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
auto input = handle->_input; auto input = handle->input_;
const auto is_null_device = info[0]->IsNullOrUndefined(); const auto is_null_device = info[0]->IsNullOrUndefined();
if(info.Length() != 2 || !(is_null_device || info[0]->IsString()) || !info[1]->IsFunction()) { if(info.Length() != 2 || !(is_null_device || info[0]->IsString()) || !info[1]->IsFunction()) {
@ -190,7 +190,7 @@ NAN_METHOD(AudioRecorderWrapper::_start) {
return; return;
} }
auto input = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder())->_input; auto input = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder())->input_;
std::string error{}; std::string error{};
v8::Local<v8::Value> argv[1]; v8::Local<v8::Value> argv[1];
@ -204,14 +204,14 @@ NAN_METHOD(AudioRecorderWrapper::_start) {
NAN_METHOD(AudioRecorderWrapper::_started) { NAN_METHOD(AudioRecorderWrapper::_started) {
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder()); auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
auto input = handle->_input; auto input = handle->input_;
info.GetReturnValue().Set(input->recording()); info.GetReturnValue().Set(input->recording());
} }
NAN_METHOD(AudioRecorderWrapper::_stop) { NAN_METHOD(AudioRecorderWrapper::_stop) {
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder()); auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
auto input = handle->_input; auto input = handle->input_;
input->stop(); input->stop();
} }
@ -265,10 +265,10 @@ NAN_METHOD(AudioRecorderWrapper::_set_volume) {
return; return;
} }
handle->_input->set_volume((float) info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0)); handle->input_->set_volume((float) info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
} }
NAN_METHOD(AudioRecorderWrapper::_get_volume) { NAN_METHOD(AudioRecorderWrapper::_get_volume) {
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder()); auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
info.GetReturnValue().Set(handle->_input->volume()); info.GetReturnValue().Set(handle->input_->volume());
} }

View File

@ -44,8 +44,8 @@ namespace tc::audio {
void delete_consumer(const AudioConsumerWrapper*); void delete_consumer(const AudioConsumerWrapper*);
inline std::deque<std::shared_ptr<AudioConsumerWrapper>> consumers() { inline std::deque<std::shared_ptr<AudioConsumerWrapper>> consumers() {
std::lock_guard lock(this->_consumer_lock); std::lock_guard lock{this->consumer_mutex};
return this->_consumers; return this->consumer_;
} }
void do_wrap(const v8::Local<v8::Object>& /* obj */); void do_wrap(const v8::Local<v8::Object>& /* obj */);
@ -53,10 +53,11 @@ namespace tc::audio {
inline void js_ref() { this->Ref(); } inline void js_ref() { this->Ref(); }
inline void js_unref() { this->Unref(); } inline void js_unref() { this->Unref(); }
private: private:
std::shared_ptr<AudioInput> _input; std::shared_ptr<AudioInput> input_;
std::mutex _consumer_lock; /* javascript consumer */
std::deque<std::shared_ptr<AudioConsumerWrapper>> _consumers; std::mutex consumer_mutex;
std::deque<std::shared_ptr<AudioConsumerWrapper>> consumer_;
}; };
} }
} }

View File

@ -42,8 +42,9 @@ bool VoiceSender::initialize_codec(std::string& error, connection::codec::value
data->converter->reset_encoder(); data->converter->reset_encoder();
} }
if(!data->resampler || data->resampler->input_rate() != rate) if(!data->resampler || data->resampler->input_rate() != rate) {
data->resampler = make_shared<AudioResampler>(rate, data->converter->sample_rate(), data->converter->channels()); data->resampler = make_shared<AudioResampler>(rate, data->converter->sample_rate(), data->converter->channels());
}
if(!data->resampler->valid()) { if(!data->resampler->valid()) {
error = "resampler is invalid"; error = "resampler is invalid";
@ -58,7 +59,7 @@ void VoiceSender::set_voice_send_enabled(bool flag) {
} }
void VoiceSender::send_data(const void *data, size_t samples, size_t rate, size_t channels) { void VoiceSender::send_data(const void *data, size_t samples, size_t rate, size_t channels) {
unique_lock lock(this->_execute_lock); unique_lock lock{this->_execute_lock};
if(!this->handle) { if(!this->handle) {
log_warn(category::voice_connection, tr("Dropping raw audio frame because of an invalid handle.")); log_warn(category::voice_connection, tr("Dropping raw audio frame because of an invalid handle."));
return; return;

View File

@ -182,7 +182,7 @@ NAN_METHOD(VoiceConnectionWrap::set_audio_source) {
auto sample_rate = native_consumer->sample_rate; auto sample_rate = native_consumer->sample_rate;
auto channels = native_consumer->channel_count; auto channels = native_consumer->channel_count;
lock_guard read_lock(connection->_voice_recoder_ptr->native_read_callback_lock); 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) { connection->_voice_recoder_ptr->native_read_callback = [weak_handle, sample_rate, channels](const void* buffer, size_t length) {
auto handle = weak_handle.lock(); auto handle = weak_handle.lock();
if(!handle) { if(!handle) {

View File

@ -151,6 +151,8 @@ const do_connect = (connection) => {
connection._voice_connection.register_client(7); connection._voice_connection.register_client(7);
}; };
do_connect(connection); do_connect(connection);
/*
let _connections = []; let _connections = [];
let i = 0; let i = 0;
let ii = setInterval(() => { let ii = setInterval(() => {
@ -160,6 +162,7 @@ let ii = setInterval(() => {
_connections.push(c); _connections.push(c);
do_connect(c); do_connect(c);
}, 500); }, 500);
*/
connection.callback_voice_data = (buffer, client_id, codec_id, flag_head, packet_id) => { connection.callback_voice_data = (buffer, client_id, codec_id, flag_head, packet_id) => {
console.log("Received voice of length %d from client %d in codec %d (Head: %o | ID: %d)", buffer.byteLength, client_id, codec_id, flag_head, packet_id); console.log("Received voice of length %d from client %d in codec %d (Head: %o | ID: %d)", buffer.byteLength, client_id, codec_id, flag_head, packet_id);

View File

@ -1,6 +1,6 @@
{ {
"name": "TeaClient", "name": "TeaClient",
"version": "1.4.11", "version": "1.4.12",
"description": "", "description": "",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {