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
logs
VoicemeeterRemote.h
.hintrc

View File

@ -2,25 +2,34 @@ import path from "path";
import { DEBUG } from "./variables";
import fs from "fs";
export type LogOptions = Omit<Parameters<Logger["writeEntry"]>["0"], "type">;
export class Logger {
public currentLogFile: string;
public tag?: string;
private get pathToLogFile(): string {
return path.join(__dirname, "../log", this.currentLogFile);
public divider: string = "\n";
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();
if (options !== undefined) {
this.tag = options.tag;
}
if (options === undefined) return;
if (options.tag !== undefined) this.tag = options.tag;
if (options.divider !== undefined) this.divider = options.divider;
}
private createNewLogFile() {
const date = new Date().toISOString().split("T");
date[1] = date[1].split(".")[0].replaceAll(":", "-");
const fileName = `${date}.log`;
fs.writeFileSync(this.pathToLogFile, "");
const fileName = `${date.join("-")}.log`;
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;
}
private toString(object: any) {
@ -32,11 +41,14 @@ export class Logger {
else if (typeof object === "string") return object;
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: {
tag?: string;
type: "log" | "error" | "warn" | "info";
divider?: string;
},
...args: any
) {
@ -50,29 +62,38 @@ export class Logger {
dateStyle: "short",
timeStyle: "medium",
});
const divider = options.divider === undefined ? "\n\t" : options.divider;
let string = `[${dateString}] `;
if (this.tag !== undefined) string += `[LTAG: ${this.tag}`;
if (options.tag !== undefined) string += ` TAG: ${options.tag}] `;
else string += "] ";
const tags = [
["LTAG", this.tag],
["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 += strings.join(divider);
fs.appendFileSync(this.pathToLogFile, string);
string += strings.join(this.divider + "\t");
string += this.divider;
this.appendTextToLog(string);
}
public log(...args: any) {
public log(options?: LogOptions, ...args: any) {
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);
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);
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);
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 { 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 {
"OK" = 0,
@ -16,15 +19,45 @@ export enum VoicemeeterType {
export class Control extends Remote {
private static buses = {
Potato: { total: 8, virtual: 3, physical: 5 },
Banana: { total: 5, virtual: 2, physical: 3 },
Basic: { total: 2, virtual: 1, physical: 1 },
Potato: { virtual: 3, physical: 5 },
Banana: { virtual: 2, physical: 3 },
Basic: { virtual: 1, physical: 1 },
};
private static strips = {
Potato: { total: 8, virtual: 3, physical: 5 },
Banana: { total: 8, virtual: 3, physical: 5 },
Basic: { total: 8, virtual: 3, physical: 5 },
Potato: { virtual: 3, physical: 5 },
Banana: { virtual: 2, physical: 3 },
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 parametersUpdated: boolean = false;
@ -52,20 +85,55 @@ export class Control extends Remote {
@validation()
public get availableBuses() {
return Control.buses[
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType
];
return {
total:
Control.totalBuses[
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType
],
...Control.buses[
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType
],
};
}
@validation()
public get availableStrips() {
return Control.strips[
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType
];
return {
total:
Control.totalStrips[
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType
],
...Control.strips[
VoicemeeterType[this.voicemeeterType] as keyof typeof VoicemeeterType
],
};
}
public minVersion(minVersion: number) {
if (minVersion > this.voicemeeterType) throw new Error("Test");
@validation()
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() {
@ -74,7 +142,7 @@ export class Control extends Remote {
this.interval = setInterval(() => {
const result = this.run("VBVMR_IsParametersDirty");
if (result !== 0 && result !== 1)
throw new Error(
throwError(
`An error occurred while checking for updated parameters. DLL result code: ${result}`
);
const isUpdated = result === 1 ? true : false;
@ -89,7 +157,7 @@ export class Control extends Remote {
createTimer();
}
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;
return result;
@ -102,7 +170,7 @@ export class Control extends Remote {
this.interval = undefined;
}
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;
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 { Parser } from "../utils/getFuncsNamesFromVRHFile";
import path from "path";
import { throwError } from "../utils/Error";
export type RemoteFuncs = (typeof Remote.allowedFuncs)[number];
@ -17,7 +18,9 @@ export class Remote {
"VBVMR_GetParameterStringA",
"VBVMR_Output_GetDeviceNumber",
"VBVMR_Input_GetDeviceNumber",
"VBVMR_GetVoicemeeterVersion"
"VBVMR_GetVoicemeeterVersion",
"VBVMR_Input_GetDeviceDescA",
"VBVMR_Output_GetDeviceDescA"
] as const;
public dllFuncs = {} as Partial<
@ -35,7 +38,7 @@ export class Remote {
for (const func of Remote.allowedFuncs) {
const funcHeader = results.find((r) => r.name === func);
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);
}
}
@ -45,9 +48,9 @@ export class Remote {
...args: any
) {
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];
if(func === undefined) throw new Error("Executed function wasn't initialized");
if(func === undefined)return throwError("Executed function wasn't initialized");
return func(...args)
}
}

View File

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

View File

@ -1,5 +1,6 @@
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) {
return (object: any, name: string, descriptor: PropertyDescriptor) => {
@ -10,7 +11,7 @@ export function minType(minType: VoicemeeterType) {
descriptor[key] = function (this: Control, ...args: any[]) {
validate.bind(this)();
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: ${
VoicemeeterType[this.voicemeeterType]
} Minimal required type is ${VoicemeeterType[minType]}`

View File

@ -1,5 +1,6 @@
import { Control } from "../Control";
import { Strip } from "../Strip";
import { Control } from "../VMR/Control";
import { Strip } from "../VMR/Strip";
import { throwError } from "../utils/Error";
type validateThisType = Control | Strip;
@ -7,7 +8,7 @@ export function validate(this: validateThisType) {
let isValid = true;
if (this instanceof Control && !this.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() {

View File

@ -1,14 +1,17 @@
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;
const logger = new Logger({});
function exit() {
function exit(eventName: (typeof exitEvents)[0]) {
clearInterval(interval);
control.logout();
logger.log("Logout");
if (control !== undefined && control.loggedIn) control.logout();
logger.log({ tag: `Signal: ${eventName.name}` }, "Logout");
process.exit();
}
@ -19,12 +22,12 @@ const exitEvents = [
{ name: "SIGUSR1" },
{ name: "SIGUSR2" },
{ name: "beforeExit" },
{ name: "exit" },
{ name: "uncaughtException" },
{ name: "unhandledRejection" },
] as { name: NodeJS.Signals }[];
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(
name: string,
type?: "string" | "boolean" | "number"
) {
import { logger } from "../Logger";
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];
if (process.env[name] === undefined) return undefined;
if (type === undefined) return variable;
@ -9,3 +15,5 @@ export function getEnvVariable(
else if (type === "string") return variable;
else if (type === "number") return new Number(variable).valueOf();
}
export { getEnvVariable };

View File

@ -1,6 +1,6 @@
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(
"VOICEMEETER_REMOTE_DLL",
"string"