modified: .gitignore

modified:   src/Logger.ts
	modified:   src/VMR/Bus.ts
	modified:   src/VMR/Control.ts
	new file:   src/VMR/Device.ts
	modified:   src/VMR/Remote.ts
	modified:   src/VMR/Strip.ts
	modified:   src/decorators/minVersion.ts
	modified:   src/decorators/validation.ts
	modified:   src/main.ts
	new file:   src/utils/Error.ts
	modified:   src/utils/getEnvVariable.ts
	modified:   src/variables.ts
This commit is contained in:
Maksym 2024-07-30 02:40:52 +02:00
parent fd2f5df26b
commit 5b2df1c3f6
13 changed files with 258 additions and 84 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ node_modules
.vscode .vscode
logs logs
VoicemeeterRemote.h VoicemeeterRemote.h
.hintrc

View File

@ -2,25 +2,34 @@ import path from "path";
import { DEBUG } from "./variables"; import { DEBUG } from "./variables";
import fs from "fs"; import fs from "fs";
export type LogOptions = Omit<Parameters<Logger["writeEntry"]>["0"], "type">;
export class Logger { export class Logger {
public currentLogFile: string; public currentLogFile: string;
public tag?: string; public tag?: string;
private get pathToLogFile(): string { public divider: string = "\n";
return path.join(__dirname, "../log", this.currentLogFile); private get pathToLogFolder(): string {
return path.join(__dirname, "../logs");
}
private get pathToCurrentLog(): string {
return path.join(__dirname, "../logs", this.currentLogFile);
} }
constructor(options?: { tag?: string }) { constructor(options?: { tag?: string; divider?: string }) {
this.currentLogFile = this.createNewLogFile(); this.currentLogFile = this.createNewLogFile();
if (options !== undefined) { if (options === undefined) return;
this.tag = options.tag; if (options.tag !== undefined) this.tag = options.tag;
} if (options.divider !== undefined) this.divider = options.divider;
} }
private createNewLogFile() { private createNewLogFile() {
const date = new Date().toISOString().split("T"); const date = new Date().toISOString().split("T");
date[1] = date[1].split(".")[0].replaceAll(":", "-"); date[1] = date[1].split(".")[0].replaceAll(":", "-");
const fileName = `${date}.log`; const fileName = `${date.join("-")}.log`;
fs.writeFileSync(this.pathToLogFile, ""); const newLogPath = path.join(this.pathToLogFolder, fileName);
if (!fs.existsSync(newLogPath)) fs.writeFileSync(newLogPath, "");
const latestLogPath = path.join(this.pathToLogFolder, "./latest.log");
if (fs.existsSync(latestLogPath)) fs.writeFileSync(latestLogPath, "");
return fileName; return fileName;
} }
private toString(object: any) { private toString(object: any) {
@ -32,11 +41,14 @@ export class Logger {
else if (typeof object === "string") return object; else if (typeof object === "string") return object;
return "Cannot transfrom object to string"; return "Cannot transfrom object to string";
} }
private writeEntryToFile( private appendTextToLog(content: string) {
fs.appendFileSync(this.pathToCurrentLog, content);
fs.appendFileSync(path.join(this.pathToLogFolder, "./latest.log"), content);
}
private writeEntry(
options: { options: {
tag?: string; tag?: string;
type: "log" | "error" | "warn" | "info"; type: "log" | "error" | "warn" | "info";
divider?: string;
}, },
...args: any ...args: any
) { ) {
@ -50,29 +62,38 @@ export class Logger {
dateStyle: "short", dateStyle: "short",
timeStyle: "medium", timeStyle: "medium",
}); });
const divider = options.divider === undefined ? "\n\t" : options.divider;
let string = `[${dateString}] `; let string = `[${dateString}] `;
if (this.tag !== undefined) string += `[LTAG: ${this.tag}`; const tags = [
if (options.tag !== undefined) string += ` TAG: ${options.tag}] `; ["LTAG", this.tag],
else string += "] "; ["TAG", options.tag],
].filter((i) => i[1] !== undefined);
if (tags.length > 0) {
string += "[";
for (const [tagName, tag] of tags) {
string += `${tagName}: ${tag}`;
}
string += "] ";
}
string += `[${options.type.toUpperCase()}] `; string += `[${options.type.toUpperCase()}] `;
string += strings.join(divider); string += strings.join(this.divider + "\t");
fs.appendFileSync(this.pathToLogFile, string); string += this.divider;
this.appendTextToLog(string);
} }
public log(...args: any) { public log(options?: LogOptions, ...args: any) {
if (!DEBUG) console.log(...args); if (!DEBUG) console.log(...args);
this.writeEntryToFile({ type: "log" }, ...args); this.writeEntry({ type: "log", ...options }, ...args);
} }
public error(...args: any) { public error(options?: LogOptions, ...args: any) {
if (!DEBUG) console.error(...args); if (!DEBUG) console.error(...args);
this.writeEntryToFile({ type: "error" }, ...args); this.writeEntry({ type: "error", ...options }, ...args);
} }
public warn(...args: any) { public warn(options?: LogOptions, ...args: any) {
if (!DEBUG) console.warn(...args); if (!DEBUG) console.warn(...args);
this.writeEntryToFile({ type: "warn" }, ...args); this.writeEntry({ type: "warn", ...options }, ...args);
} }
public info(...args: any) { public info(options?: LogOptions, ...args: any) {
if (!DEBUG) console.info(...args); if (!DEBUG) console.info(...args);
this.writeEntryToFile({ type: "info" }, ...args); this.writeEntry({ type: "info", ...options }, ...args);
} }
} }
export const logger = new Logger({});

View File

@ -0,0 +1,58 @@
import { VMObject } from "./VMObject";
import { minType } from "../decorators/minVersion";
import { validation } from "../decorators/validation";
import { Control } from "./Control";
export class Strip extends VMObject {
@validation()
public get Mono(): boolean {
return this.getParamValueBoolean(this.buildParameterString("Mono"))[1];
}
public set Mono(v: boolean) {
this.setParamValueBoolean(this.buildParameterString("Mono"), v);
}
@validation()
public get Mute(): boolean {
return this.getParamValueBoolean(this.buildParameterString("Mute"))[1];
}
public set Mute(v: boolean) {
this.setParamValueBoolean(this.buildParameterString("Mute"), v);
}
@validation()
public get EQEnabled(): boolean {
return this.getParamValueBoolean(this.buildParameterString("EQ", "on"))[1];
}
public set EQEnabled(v: boolean) {
this.setParamValueBoolean(this.buildParameterString("EQ", "on"), v);
}
@validation()
public get EQVariant(): boolean {
return this.getParamValueBoolean(this.buildParameterString("EQ", "AB"))[1];
}
public set EQVariant(v: boolean) {
this.setParamValueBoolean(this.buildParameterString("EQ", "AB"), v);
}
@validation()
public get Gain(): number {
return this.getParamValueNumber(this.buildParameterString("Gain"))[1];
}
public set Gain(v: number) {
this.setParamValueNumber(this.buildParameterString("Gain"), v);
}
constructor(
control: Control,
public id: number
) {
// control.validation();
super(control, "Bus", id);
}
initialize(): void {
console.log("Nothing to initialize");
}
}

View File

@ -1,5 +1,8 @@
import { Remote } from "./Remote"; import { Remote } from "./Remote";
import { validation } from "./decorators/validation"; import { validation } from "../decorators/validation";
import { Device } from "./Device";
import { throwError } from "../utils/Error";
import { logger } from "../Logger";
export enum LoginReturnType { export enum LoginReturnType {
"OK" = 0, "OK" = 0,
@ -16,15 +19,45 @@ export enum VoicemeeterType {
export class Control extends Remote { export class Control extends Remote {
private static buses = { private static buses = {
Potato: { total: 8, virtual: 3, physical: 5 }, Potato: { virtual: 3, physical: 5 },
Banana: { total: 5, virtual: 2, physical: 3 }, Banana: { virtual: 2, physical: 3 },
Basic: { total: 2, virtual: 1, physical: 1 }, Basic: { virtual: 1, physical: 1 },
}; };
private static strips = { private static strips = {
Potato: { total: 8, virtual: 3, physical: 5 }, Potato: { virtual: 3, physical: 5 },
Banana: { total: 8, virtual: 3, physical: 5 }, Banana: { virtual: 2, physical: 3 },
Basic: { total: 8, virtual: 3, physical: 5 }, Basic: { virtual: 1, physical: 2 },
}; };
private static totalBuses = (() => {
const totalBuses = {} as (typeof Control)["totalBuses"];
for (const voicemeeterVersion in Control.buses) {
if (
Object.prototype.hasOwnProperty.call(Control.buses, voicemeeterVersion)
) {
const element =
Control.buses[voicemeeterVersion as keyof (typeof Control)["buses"]];
totalBuses[voicemeeterVersion as keyof (typeof Control)["buses"]] =
element.physical + element.virtual;
}
}
return totalBuses;
})() as Record<keyof (typeof Control)["buses"], number>;
private static totalStrips = (() => {
const totalStrips = {} as (typeof Control)["totalStrips"];
for (const voicemeeterVersion in Control.strips) {
if (
Object.prototype.hasOwnProperty.call(Control.strips, voicemeeterVersion)
) {
const element =
Control.strips[
voicemeeterVersion as keyof (typeof Control)["strips"]
];
totalStrips[voicemeeterVersion as keyof (typeof Control)["strips"]] =
element.physical + element.virtual;
}
}
return totalStrips;
})() as Record<keyof (typeof Control)["strips"], number>;
public loggedIn: boolean = false; public loggedIn: boolean = false;
public parametersUpdated: boolean = false; public parametersUpdated: boolean = false;
@ -52,20 +85,55 @@ export class Control extends Remote {
@validation() @validation()
public get availableBuses() { public get availableBuses() {
return Control.buses[ return {
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType total:
]; Control.totalBuses[
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType
],
...Control.buses[
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType
],
};
} }
@validation() @validation()
public get availableStrips() { public get availableStrips() {
return Control.strips[ return {
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType total:
]; Control.totalStrips[
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType
],
...Control.strips[
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType
],
};
} }
public minVersion(minVersion: number) { @validation()
if (minVersion > this.voicemeeterType) throw new Error("Test"); public get availableInputDevices(): Device[] {
const numberOfDevices: number = this.run("VBVMR_Input_GetDeviceNumber");
const devices: Device[] = [];
for (let i = 0; i < numberOfDevices; i++) {
let type, deviceName, hardwareId;
const result = this.run(
"VBVMR_Input_GetDeviceDescA",
i,
type,
deviceName,
hardwareId
);
logger.log({}, type, deviceName, hardwareId);
if (result !== 0) throwError("Error happened on VM side");
}
return devices;
}
@validation()
public get availableOutputDevices(): Device[] {
const numberOfDevices: number = this.run("VBVMR_Output_GetDeviceNumber");
const devices: Device[] = [];
for (let i = 0; i < numberOfDevices; i++) {}
return devices;
} }
public login() { public login() {
@ -74,7 +142,7 @@ export class Control extends Remote {
this.interval = setInterval(() => { this.interval = setInterval(() => {
const result = this.run("VBVMR_IsParametersDirty"); const result = this.run("VBVMR_IsParametersDirty");
if (result !== 0 && result !== 1) if (result !== 0 && result !== 1)
throw new Error( throwError(
`An error occurred while checking for updated parameters. DLL result code: ${result}` `An error occurred while checking for updated parameters. DLL result code: ${result}`
); );
const isUpdated = result === 1 ? true : false; const isUpdated = result === 1 ? true : false;
@ -89,7 +157,7 @@ export class Control extends Remote {
createTimer(); createTimer();
} }
const result = this.run("VBVMR_Login") as LoginReturnType; const result = this.run("VBVMR_Login") as LoginReturnType;
if (result !== 0) throw new Error(`DLL result code: ${result}`); if (result !== 0) throwError(`DLL result code: ${result}`);
this.loggedIn = true; this.loggedIn = true;
return result; return result;
@ -102,7 +170,7 @@ export class Control extends Remote {
this.interval = undefined; this.interval = undefined;
} }
const result = this.run("VBVMR_Logout") as number; const result = this.run("VBVMR_Logout") as number;
if (result !== 0) throw new Error(`DLL result code: ${result}`); if (result !== 0) throwError(`DLL result code: ${result}`);
this.loggedIn = false; this.loggedIn = false;
return result; return result;
} }

9
src/VMR/Device.ts Normal file
View File

@ -0,0 +1,9 @@
export type DeviceDriver = "ASIO" | "MME" | "WDM" | "KS";
export class Device {
constructor(
public driver: DeviceDriver,
public isInput: boolean,
public name: string
) {}
}

View File

@ -1,6 +1,7 @@
import { IKoffiLib, KoffiFunction, load } from "koffi"; import { IKoffiLib, KoffiFunction, load } from "koffi";
import { Parser } from "../utils/getFuncsNamesFromVRHFile"; import { Parser } from "../utils/getFuncsNamesFromVRHFile";
import path from "path"; import path from "path";
import { throwError } from "../utils/Error";
export type RemoteFuncs = (typeof Remote.allowedFuncs)[number]; export type RemoteFuncs = (typeof Remote.allowedFuncs)[number];
@ -17,7 +18,9 @@ export class Remote {
"VBVMR_GetParameterStringA", "VBVMR_GetParameterStringA",
"VBVMR_Output_GetDeviceNumber", "VBVMR_Output_GetDeviceNumber",
"VBVMR_Input_GetDeviceNumber", "VBVMR_Input_GetDeviceNumber",
"VBVMR_GetVoicemeeterVersion" "VBVMR_GetVoicemeeterVersion",
"VBVMR_Input_GetDeviceDescA",
"VBVMR_Output_GetDeviceDescA"
] as const; ] as const;
public dllFuncs = {} as Partial< public dllFuncs = {} as Partial<
@ -35,7 +38,7 @@ export class Remote {
for (const func of Remote.allowedFuncs) { for (const func of Remote.allowedFuncs) {
const funcHeader = results.find((r) => r.name === func); const funcHeader = results.find((r) => r.name === func);
if (funcHeader === undefined) if (funcHeader === undefined)
throw new Error(`Allowed function not found in header file. Caused by function: ${func}`); return throwError(`Allowed function not found in header file. Caused by function: ${func}`);
this.dllFuncs[func] = this.dll.func(funcHeader.rawString); this.dllFuncs[func] = this.dll.func(funcHeader.rawString);
} }
} }
@ -45,9 +48,9 @@ export class Remote {
...args: any ...args: any
) { ) {
if (!Remote.allowedFuncs.includes(command)) if (!Remote.allowedFuncs.includes(command))
throw new Error("Used function isn't listed in allowed"); throwError("Used function isn't listed in allowed");
const func = this.dllFuncs[command]; const func = this.dllFuncs[command];
if(func === undefined) throw new Error("Executed function wasn't initialized"); if(func === undefined)return throwError("Executed function wasn't initialized");
return func(...args) return func(...args)
} }
} }

View File

@ -1,6 +1,6 @@
import { VMObject } from "./VMObject"; import { VMObject } from "./VMObject";
import { minType } from "./decorators/minVersion"; import { minType } from "../decorators/minVersion";
import { validation } from "./decorators/validation"; import { validation } from "../decorators/validation";
import { Control } from "./Control"; import { Control } from "./Control";
export type OutputBuses = Record<"A" | "B", Record<number, boolean>>; export type OutputBuses = Record<"A" | "B", Record<number, boolean>>;
@ -8,9 +8,7 @@ export type OutputBuses = Record<"A" | "B", Record<number, boolean>>;
export class Strip extends VMObject { export class Strip extends VMObject {
@validation() @validation()
public get Mono(): boolean { public get Mono(): boolean {
return this.getParamValueBoolean( return this.getParamValueBoolean(this.buildParameterString("Mono"))[1];
this.buildParameterString("Mono")
)[1];
} }
public set Mono(v: boolean) { public set Mono(v: boolean) {
this.setParamValueBoolean(this.buildParameterString("Mono"), v); this.setParamValueBoolean(this.buildParameterString("Mono"), v);
@ -18,9 +16,7 @@ export class Strip extends VMObject {
@validation() @validation()
public get Mute(): boolean { public get Mute(): boolean {
return this.getParamValueBoolean( return this.getParamValueBoolean(this.buildParameterString("Mute"))[1];
this.buildParameterString("Mute")
)[1];
} }
public set Mute(v: boolean) { public set Mute(v: boolean) {
this.setParamValueBoolean(this.buildParameterString("Mute"), v); this.setParamValueBoolean(this.buildParameterString("Mute"), v);
@ -28,11 +24,9 @@ export class Strip extends VMObject {
@validation() @validation()
public get Solo(): boolean { public get Solo(): boolean {
return this.getParamValueBoolean( return this.getParamValueBoolean(this.buildParameterString("Solo"))[1];
this.buildParameterString("Solo")
)[1];
} }
public set Solor(v: boolean) { public set Solo(v: boolean) {
this.setParamValueBoolean(this.buildParameterString("Solo"), v); this.setParamValueBoolean(this.buildParameterString("Solo"), v);
} }
@ -57,9 +51,7 @@ export class Strip extends VMObject {
const buses: number[] = []; const buses: number[] = [];
for (let i = 0; i < this.control.availableBuses.total; i++) { for (let i = 0; i < this.control.availableBuses.total; i++) {
buses.push( buses.push(
this.getParamValueNumber( this.getParamValueNumber(this.buildParameterString("GainLayer", i))[1]
this.buildParameterString("GainLayer", i)
)[1]
); );
} }
return buses; return buses;
@ -68,9 +60,7 @@ export class Strip extends VMObject {
let script = ""; let script = "";
for (let i = 0; i < changedBuses.length; i++) { for (let i = 0; i < changedBuses.length; i++) {
const gainForBus = changedBuses[i]; const gainForBus = changedBuses[i];
script += `${this.buildParameterString( script += `${this.buildParameterString("GainLayer", i)} = ${gainForBus};`;
"GainLayer", i,
)} = ${gainForBus};`;
} }
this.setParameters(script); this.setParameters(script);
} }
@ -113,7 +103,10 @@ export class Strip extends VMObject {
this.setParameters(script); this.setParameters(script);
} }
constructor(control: Control, public id: number) { constructor(
control: Control,
public id: number
) {
// control.validation(); // control.validation();
super(control, "Strip", id); super(control, "Strip", id);
} }

View File

@ -1,5 +1,6 @@
import { validate } from "./validation"; import { validate } from "./validation";
import { Control, VoicemeeterType } from "../Control"; import { Control, VoicemeeterType } from "../VMR/Control";
import { throwError } from "../utils/Error";
export function minType(minType: VoicemeeterType) { export function minType(minType: VoicemeeterType) {
return (object: any, name: string, descriptor: PropertyDescriptor) => { return (object: any, name: string, descriptor: PropertyDescriptor) => {
@ -10,7 +11,7 @@ export function minType(minType: VoicemeeterType) {
descriptor[key] = function (this: Control, ...args: any[]) { descriptor[key] = function (this: Control, ...args: any[]) {
validate.bind(this)(); validate.bind(this)();
if (this.voicemeeterType < minType) if (this.voicemeeterType < minType)
throw new Error( return throwError(
`Used property/method with name: ${name} is not supported by this type of Voicemeeter. Your type: ${ `Used property/method with name: ${name} is not supported by this type of Voicemeeter. Your type: ${
VoicemeeterType[this.voicemeeterType] VoicemeeterType[this.voicemeeterType]
} Minimal required type is ${VoicemeeterType[minType]}` } Minimal required type is ${VoicemeeterType[minType]}`

View File

@ -1,5 +1,6 @@
import { Control } from "../Control"; import { Control } from "../VMR/Control";
import { Strip } from "../Strip"; import { Strip } from "../VMR/Strip";
import { throwError } from "../utils/Error";
type validateThisType = Control | Strip; type validateThisType = Control | Strip;
@ -7,7 +8,7 @@ export function validate(this: validateThisType) {
let isValid = true; let isValid = true;
if (this instanceof Control && !this.loggedIn) isValid = false; if (this instanceof Control && !this.loggedIn) isValid = false;
else if (this instanceof Strip && !this.control.loggedIn) isValid = false; else if (this instanceof Strip && !this.control.loggedIn) isValid = false;
if (!isValid) throw new Error("Communication pipe isn't open"); if (!isValid) return throwError("Communication pipe isn't open");
} }
export function validation() { export function validation() {

View File

@ -1,14 +1,17 @@
import { Control, VoicemeeterType } from "./VMR/Control"; import { Control, VoicemeeterType } from "./VMR/Control";
import { Logger } from "./Logger"; import { logger, Logger } from "./Logger";
import { VOICEMEETER_REMOTE_DLL } from "./variables";
import { throwError } from "./utils/Error";
let control: Control; if (VOICEMEETER_REMOTE_DLL === undefined)
throwError("The path for VM Remote dll is not specified");
let control: Control = new Control(VOICEMEETER_REMOTE_DLL!);
let interval: NodeJS.Timeout; let interval: NodeJS.Timeout;
const logger = new Logger({});
function exit() { function exit(eventName: (typeof exitEvents)[0]) {
clearInterval(interval); clearInterval(interval);
control.logout(); if (control !== undefined && control.loggedIn) control.logout();
logger.log("Logout"); logger.log({ tag: `Signal: ${eventName.name}` }, "Logout");
process.exit(); process.exit();
} }
@ -19,12 +22,12 @@ const exitEvents = [
{ name: "SIGUSR1" }, { name: "SIGUSR1" },
{ name: "SIGUSR2" }, { name: "SIGUSR2" },
{ name: "beforeExit" }, { name: "beforeExit" },
{ name: "exit" },
{ name: "uncaughtException" }, { name: "uncaughtException" },
{ name: "unhandledRejection" }, { name: "unhandledRejection" },
] as { name: NodeJS.Signals }[]; ] as { name: NodeJS.Signals }[];
for (const event of exitEvents) { for (const event of exitEvents) {
process.on(event.name, exit.bind(null)); process.on(event.name, () => exit(event));
} }
logger.log({}, control.availableInputDevices);

8
src/utils/Error.ts Normal file
View File

@ -0,0 +1,8 @@
import { logger, Logger } from "../Logger";
import { DEBUG } from "../variables";
export function throwError(message: string) {
logger.error({ tag: "CRIT" }, message);
if (DEBUG) throw new Error(message);
process.exit(-1);
}

View File

@ -1,7 +1,13 @@
export function getEnvVariable( import { logger } from "../Logger";
name: string,
type?: "string" | "boolean" | "number" export type EnvironmentVariableType = "string" | "boolean" | "number";
) {
function getEnvVariable(name: string, type: "string"): string | undefined;
function getEnvVariable(name: string, type: "boolean"): boolean | undefined;
function getEnvVariable(name: string, type: "number"): number | undefined;
function getEnvVariable(name: string, type: undefined): any | undefined;
function getEnvVariable(name: string, type?: EnvironmentVariableType) {
const variable = process.env[name]; const variable = process.env[name];
if (process.env[name] === undefined) return undefined; if (process.env[name] === undefined) return undefined;
if (type === undefined) return variable; if (type === undefined) return variable;
@ -9,3 +15,5 @@ export function getEnvVariable(
else if (type === "string") return variable; else if (type === "string") return variable;
else if (type === "number") return new Number(variable).valueOf(); else if (type === "number") return new Number(variable).valueOf();
} }
export { getEnvVariable };

View File

@ -1,6 +1,6 @@
import { getEnvVariable } from "./utils/getEnvVariable"; import { getEnvVariable } from "./utils/getEnvVariable";
export const DEBUG = getEnvVariable("DEBUG", "boolean"); export const DEBUG = getEnvVariable("NODE_ENV", "string") !== "production";
export const VOICEMEETER_REMOTE_DLL = getEnvVariable( export const VOICEMEETER_REMOTE_DLL = getEnvVariable(
"VOICEMEETER_REMOTE_DLL", "VOICEMEETER_REMOTE_DLL",
"string" "string"