voicemeeter-connector
Version:
A Connector to use the Voicemeeter API
446 lines (408 loc) • 17.5 kB
text/typescript
/* eslint-disable no-control-regex */
import koffi from "koffi";
import { Device, VMLibrary, VoiceMeeterTypes } from "../types/voicemeeter-types";
import { BusProperties, MacroButtonModes, StripProperties } from "./constants";
import DLLHandler from "./dll-handler";
/**
* @ignore
*/
let libVM: VMLibrary;
/**
* @ignore
*/
let instance: Voicemeeter;
export default class Voicemeeter {
/**
* Initializes the voice meeter dll connection.
* This call is neccessary to use the api. It returns a promise with a VoiceMeeter instance
*/
static init = async (): Promise<Voicemeeter> => {
const dllPath = await DLLHandler.getDLLPath();
return new Promise((resolve: (instance: Voicemeeter) => any) => {
if (!instance) {
instance = new Voicemeeter();
}
const lib = koffi.load(`${dllPath}/VoicemeeterRemote64.dll`);
libVM = {
VBVMR_Login: lib.func("long __stdcall VBVMR_Login(void)"),
VBVMR_Logout: lib.func("long __stdcall VBVMR_Logout(void)"),
VBVMR_RunVoicemeeter: lib.func("long __stdcall VBVMR_RunVoicemeeter(long mode)"),
VBVMR_IsParametersDirty: lib.func("long __stdcall VBVMR_IsParametersDirty(void)"),
VBVMR_GetLevel: lib.func("long __stdcall VBVMR_GetLevel(long type, long channel, _Out_ float* value)"),
VBVMR_GetParameterFloat: lib.func("long __stdcall VBVMR_GetParameterFloat(const char* param, _Out_ float* value)"),
VBVMR_GetParameterStringA: lib.func("long __stdcall VBVMR_GetParameterStringA(const char* param, _Out_ char* value)"),
VBVMR_SetParameters: lib.func("long __stdcall VBVMR_SetParameters(const char* param)"),
VBVMR_Output_GetDeviceNumber: lib.func("long __stdcall VBVMR_Output_GetDeviceNumber(void)"),
VBVMR_Output_GetDeviceDescA: lib.func(
"long __stdcall VBVMR_Output_GetDeviceDescA(long index, _Out_ long* type, _Out_ char* name, _Out_ char* hardwareId)"
),
VBVMR_Input_GetDeviceNumber: lib.func("long __stdcall VBVMR_Input_GetDeviceNumber(void)"),
VBVMR_Input_GetDeviceDescA: lib.func(
"long __stdcall VBVMR_Input_GetDeviceDescA(long index, long* type, char* name, char* hardwareId)"
),
VBVMR_GetVoicemeeterType: lib.func("long __stdcall VBVMR_GetVoicemeeterType(_Out_ long* type)"),
VBVMR_GetVoicemeeterVersion: lib.func("long __stdcall VBVMR_GetVoicemeeterVersion(_Out_ long* version)"),
VBVMR_MacroButton_IsDirty: lib.func("long __stdcall VBVMR_MacroButton_IsDirty(void)"),
VBVMR_MacroButton_GetStatus: lib.func(
"long __stdcall VBVMR_MacroButton_GetStatus(long nuLogicalButton, _Out_ float* pValue, long bitmode)"
),
VBVMR_MacroButton_SetStatus: lib.func(
"long __stdcall VBVMR_MacroButton_SetStatus(long nuLogicalButton, float fValue, long bitmode)"
),
};
instance.isInitialised = true;
resolve(instance);
});
};
private isInitialised = false;
private isConnected = false;
private outputDevices: Device[] = [];
private inputDevices: Device[] = [];
private version = "";
private type: VoiceMeeterTypes;
private eventPool = [] as Array<() => void>;
private stringParameters = ["Label", "FadeTo", "FadeBy", "AppGain", "AppMute", "name", "ip"];
private timerInterval: NodeJS.Timeout;
/**
* Starts a connection to VoiceMeeter
*/
public connect = (): { success: boolean; message: string; code: number } | never => {
if (!this.isInitialised) {
throw new Error("Await the initialisation before connect");
}
if (this.isConnected) {
// If already connected, still return success status instead of throwing
return { success: true, message: "Already connected.", code: 0 };
}
const loginResult = libVM.VBVMR_Login();
switch (loginResult) {
case 0: {
// Complete success, Voicemeeter is running
this.isConnected = true;
this.type = this.getVoicemeeterType();
this.version = this.getVoicemeeterVersion();
this.timerInterval = setInterval(this.checkPropertyChange, 10);
return { success: true, message: "Successfully connected to VoiceMeeter (VM Running).", code: 0 };
}
case 1: {
// Connected successfully but Voicemeeter application is not running
// Pipe is established, can wait for VM to start
this.isConnected = true;
this.type = undefined;
this.version = "Unknown";
this.timerInterval = setInterval(this.checkPropertyChange, 10);
return { success: true, message: "Connected to VoiceMeeter API (VM App Not Running).", code: 1 };
}
case -1: {
// Failed to get client (unexpected error) - throw error
this.isConnected = false;
throw new Error("VoiceMeeter connection failed: Unable to get client (Unexpected error -1).");
}
case -2: {
// Unexpected login (logout should have been executed first) - throw error
this.isConnected = false;
throw new Error("VoiceMeeter connection failed: Unexpected login (Expected logout first -2).");
}
default: {
// Unknown return value - throw error
this.isConnected = false;
throw new Error(`VoiceMeeter connection failed with unknown error code: ${loginResult}.`);
}
}
};
/**
* Getter $outputDevices
* @return {Device[] }
*/
public get $outputDevices(): Device[] {
return this.outputDevices;
}
/**
* Getter $inputDevices
* @return {Device[] }
*/
public get $inputDevices(): Device[] {
return this.inputDevices;
}
/**
* Getter $version
* @return {string }
*/
public get $version(): string {
return this.version;
}
/**
* Getter $type
* @return {VoiceMeeterTypes}
*/
public get $type(): VoiceMeeterTypes {
return this.type;
}
/**
* Terminates the connection to VoiceMeeter
*/
public disconnect = () => {
if (!this.isConnected) {
throw new Error("Not connected ");
}
try {
if (libVM.VBVMR_Logout() === 0) {
clearInterval(this.timerInterval);
this.isConnected = false;
return;
}
throw new Error("Disconnect failed");
} catch {
throw new Error("Disconnect failed");
}
};
/**
* Updates all input and ouput devices
*/
public updateDeviceList = () => {
if (!this.isConnected) {
throw new Error("Not connected ");
}
this.outputDevices = [];
this.inputDevices = [];
const outputDeviceNumber = libVM.VBVMR_Output_GetDeviceNumber();
for (let i = 0; i < outputDeviceNumber; i++) {
const hardwareIdPtr = Buffer.alloc(256);
const namePtr = Buffer.alloc(256);
const typePtr = [0];
libVM.VBVMR_Output_GetDeviceDescA(i, typePtr, namePtr, hardwareIdPtr);
this.outputDevices.push({
name: namePtr.toString().replaceAll(/\u0000+$/g, ""),
hardwareId: hardwareIdPtr.toString().replaceAll(/\u0000+$/g, ""),
type: typePtr[0],
});
}
const inputDeviceNumber = libVM.VBVMR_Input_GetDeviceNumber();
for (let i = 0; i < inputDeviceNumber; i++) {
const hardwareIdPtr = Buffer.alloc(256);
const namePtr = Buffer.alloc(256);
const typePtr = [0];
libVM.VBVMR_Input_GetDeviceDescA(i, typePtr, namePtr, hardwareIdPtr);
this.inputDevices.push({
name: namePtr.toString().replaceAll(/\u0000+$/g, ""),
hardwareId: hardwareIdPtr.toString().replaceAll(/\u0000+$/g, ""),
type: typePtr[0],
});
}
};
/**
* Returns wheter a parameter has been changed
*/
public isParametersDirty = () => {
return libVM.VBVMR_IsParametersDirty();
};
/**
* Gets a bus parameter.
* @param {number} index Index of the bus
* @param {BusProperties} property Property which should be get
*/
public getBusParameter = (index: number, property: BusProperties) => {
return this.getParameter("Bus", index, property);
};
/**
* Gets a strip parameter
* @param {number} index Index of the strip
* @param {StripProperties} property Property which should be get
*/
public getStripParameter = (index: number, property: StripProperties) => {
return this.getParameter("Strip", index, property);
};
/**
* Sets a parameter of a strip.
* @param {number} index Strip number
* @param {StripProperties} property Propertyname which should be changed
* @param {any} value Property value
*/
public setStripParameter = (index: number, property: StripProperties, value: any) => {
return this.setParameter("Strip", index, property, value);
};
/**
* Sets a parameter of a bus.
* @param {number} index Bus number
* @param {StripProperties} property Propertyname which should be changed
* @param {any} value Property value
*/
public setBusParameter = (index: number, property: BusProperties, value: any) => {
return this.setParameter("Bus", index, property, value);
};
/**
* @param {()=>any} fn Function which should be called if something changes
*/
public attachChangeEvent = (fn: () => any) => {
this.eventPool.push(fn);
};
/**
* @param parameterName Name of the parameter that should be get
* @returns {any} Parameter value
*/
public getOption = (parameterName: string) => {
if (!this.isConnected) {
throw new Error("Not correct connected ");
}
// Some parameters return string values and require some post-processing, this checks for those parameters
if (this.stringParameters.some((str) => parameterName.includes(str))) {
const strPtr = Buffer.alloc(512);
libVM.VBVMR_GetParameterStringA(parameterName, strPtr);
return [...String.fromCharCode.apply(null, strPtr)]
.filter((e: string) => {
return e !== "\0";
})
.join("");
}
const valuePtr = [0];
libVM.VBVMR_GetParameterFloat(parameterName, valuePtr);
return valuePtr[0];
};
/**
* Sets an option.
* @param {string} option Option to set
*/
public setOption = (option: string) => {
const script = Buffer.alloc(option.length + 1);
script.fill(0).write(option);
libVM.VBVMR_SetParameters(script);
return new Promise((resolve) => {
setTimeout(resolve, 200);
});
};
/**
* Checks if any macro button has unsaved changes
*/
public isMacroButtonDirty = () => {
return libVM.VBVMR_MacroButton_IsDirty();
};
/**
* Gets the status of a specific macro button
* @param {number} buttonIndex - The index of the macro button
* @param {number} [bitmode=0] - Bit mode parameter (optional, defaults to MacroButtonModes.DEFAULT)
* @returns {number} The current status value of the macro button
* @throws {Error} Throws an error if failed to get the button status
*/
public getMacroButtonStatus = (buttonIndex: number, bitmode: number = MacroButtonModes.DEFAULT): number => {
const valuePtr = [0];
const result = libVM.VBVMR_MacroButton_GetStatus(buttonIndex, valuePtr, bitmode);
if (result === 0) {
return valuePtr[0];
}
throw new Error(`Failed to get macro button ${buttonIndex} status`);
};
/**
* Sets the status of a specific macro button
* @param {number} buttonIndex - The index of the macro button
* @param {number} value - The status value to set
* @param {number} [bitmode=0] - Bit mode parameter (optional, defaults to MacroButtonModes.DEFAULT)
* @throws {Error} Throws an error if failed to set the button status
*/
public setMacroButtonStatus = (buttonIndex: number, value: number, bitmode: number = MacroButtonModes.DEFAULT): void => {
const result = libVM.VBVMR_MacroButton_SetStatus(buttonIndex, value, bitmode);
if (result !== 0) {
throw new Error(`Failed to set macro button ${buttonIndex} status`);
}
};
/**
* Checks whether properties has been changed and calls all event listeners
*/
private checkPropertyChange = () => {
let hasChanges = false;
if (this.isParametersDirty() === 1) {
hasChanges = true;
}
if (this.isMacroButtonDirty() === 1) {
hasChanges = true;
}
if (hasChanges) {
for (const eventListener of this.eventPool) {
eventListener();
}
}
};
/**
* Gets installed voicemeeter type.
* Means Voicemeeter(normal,banana,potato)
*/
private getVoicemeeterType = (): VoiceMeeterTypes => {
const typePtr = [0];
if (libVM.VBVMR_GetVoicemeeterType(typePtr) !== 0) {
throw new Error("running failed");
}
switch (typePtr[0]) {
case 1: {
// Voicemeeter
return "voicemeeter";
}
case 2: {
// Voicemeeter Banana
return "voicemeeterBanana";
}
case 3: {
// Voicemeeter Potato
return "voicemeeterPotato";
}
default: {
throw new Error("Voicemeeter seems not to be installed");
}
}
};
/**
* Returns the installed voicemeeter version
*/
private getVoicemeeterVersion = () => {
const versionPtr = [0];
if (libVM.VBVMR_GetVoicemeeterVersion(versionPtr) !== 0) {
throw new Error("running failed");
}
// For info on this see: https://github.com/mirror/equalizerapo/blob/53d885f7f1a097b457e17a5206b7d60f647877a8/VoicemeeterClient/VoicemeeterRemote.h#L122-L135
const version =
`${(versionPtr[0] & 0xff_00_00_00) >> 24}` +
`.${(versionPtr[0] & 0x00_ff_00_00) >> 16}` +
`.${(versionPtr[0] & 0x00_00_ff_00) >> 8}` +
`.${versionPtr[0] & 0x00_00_00_ff}`;
return version;
};
/**
* Gets a parameter of voicemeeter
* @param {'Strip'|'Bus'} selector Strip or Bus
* @param {number} index Number of strip or bus
* @param {StripProperties|BusProperties} property Property which should be read
*/
private getParameter = (selector: "Strip" | "Bus", index: number, property: StripProperties | BusProperties) => {
const parameterName = `${selector}[${index}].${property}`;
return this.getOption(parameterName);
};
/**
* Sets a parameter of a bus or Strip
* @param {'Strip'|'Bus'} selector
* @param {number} index Number of strip or bus
* @param {StripProperties|BusProperties} property Propertyname which should be changed
* @param {any} value Property value
*/
private setParameter = (
selector: "Strip" | "Bus",
index: number,
property: StripProperties | BusProperties,
value: any
): Promise<any> => {
if (!this.isConnected) {
throw new Error("Not connected ");
}
const scriptString = `${selector}[${index}].${property}=${value};`;
return this.setOption(scriptString);
};
/**
* Gets realtime audio level see the VoicemeeterRemote API: [VoicemeeterRemote.h GetLevel](https://github.com/mirror/equalizerapo/blob/7aece1b788fce5aa11873f3842a0d01f7c78454b/VoicemeeterClient/VoicemeeterRemote.h#L284),
* for more details about the parameters
* @param {0|1|2|3} type 0 = pre fader input levels. 1 = post fader input levels. 2= post Mute input levels. 3= output levels
* @param channel audio channel zero based index
* @returns {float} Current audio level
*/
public getLevel = (type: 0 | 1 | 2 | 3, channel: number) => {
const levelPtr = [0];
libVM.VBVMR_GetLevel(type, channel, levelPtr);
return levelPtr[0];
};
}