/** * VMR.ahk - A wrapper for Voicemeeter's Remote API * - Version 2.0.0 * - Build timestamp 2024-03-09 19:51:57 UTC * - Repository: {@link https://github.com/SaifAqqad/VMR.ahk GitHub} * - Documentation: {@link https://saifaqqad.github.io/VMR.ahk VMR Docs} */ #Requires AutoHotkey >=2.0 class VMRUtils { static _MIN_PERCENTAGE := 0.001 static _MAX_PERCENTAGE := 1.0 /** * Converts a dB value to a percentage value. * * @param {Number} p_dB The dB value to convert. * __________ * @returns {Number} The percentage value. */ static DbToPercentage(p_dB) { local value := ((10 ** (p_dB / 20)) - VMRUtils._MIN_PERCENTAGE) / (VMRUtils._MAX_PERCENTAGE - VMRUtils._MIN_PERCENTAGE) return value < 0 ? 0 : Round(value * 100) } /** * Converts a percentage value to a dB value. * * @param {Number} p_percentage The percentage value to convert. * __________ * @returns {Number} The dB value. */ static PercentageToDb(p_percentage) { if (p_percentage < 0) p_percentage := 0 local value := 20 * Log(VMRUtils._MIN_PERCENTAGE + p_percentage / 100 * (VMRUtils._MAX_PERCENTAGE - VMRUtils._MIN_PERCENTAGE)) return Round(value, 2) } /** * Applies an upper and a lower bound on a passed value. * * @param {Number} p_value The value to apply the bounds on. * @param {Number} p_min The lower bound. * @param {Number} p_max The upper bound. * __________ * @returns {Number} The value with the bounds applied. */ static EnsureBetween(p_value, p_min, p_max) => Round(Max(p_min, Min(p_max, p_value)), 2) /** * Returns the index of the first occurrence of a value in an array, or -1 if it's not found. * * @param {Array} p_array The array to search in. * @param {Any} p_value The value to search for. * __________ * @returns {Number} The index of the first occurrence of the value in the array, or -1 if it's not found. */ static IndexOf(p_array, p_value) { local i, value if !(p_array is Array) throw Error("p_array: Expected an Array, got " Type(p_array)) for (i, value in p_array) { if (value = p_value) return i } return -1 } /** * Returns a string with the passed parameters joined using the passed seperator. * * @param {Array} p_params - The parameters to join. * @param {String} p_seperator - The seperator to use. * @param {Number} p_maxLength - The maximum length of each parameter. * __________ * @returns {String} The joined string. */ static Join(p_params, p_seperator, p_maxLength := 30) { local str := "" for (param in p_params) { str .= SubStr(VMRUtils.ToString(param), 1, p_maxLength) . p_seperator } return SubStr(str, 1, -StrLen(p_seperator)) } /** * Converts a value to a string. * * @param {Any} p_value The value to convert to a string. * _________ * @returns {String} The string representation of the passed value */ static ToString(p_value) { if (p_value is String) return p_value else if (p_value is Array) return "[" . VMRUtils.Join(p_value, ", ") . "]" else if (IsObject(p_value)) return p_value.ToString ? p_value.ToString() : Type(p_value) else return String(p_value) } } class VMRError extends Error { /**. * The return code of the Voicemeeter function that failed * @type {Number} */ ReturnCode := "" /** * The name of the function that threw the error * @type {String} */ What := "" /** * An error message * @type {String} */ Message := "" /** * Extra information about the error * @type {String} */ Extra := "" /** * @param {Any} p_errorValue - The error value * @param {String} p_funcName - The name of the function that threw the error * @param {Array} p_funcParams The parameters of the function that threw the error */ __New(p_errorValue, p_funcName, p_funcParams*) { this.What := p_funcName this.Extra := p_errorValue this.Message := "VMR failure in " p_funcName "(" VMRUtils.Join(p_funcParams, ", ") ")" if (p_errorValue is Error) { this.Extra := "Inner error message (" p_errorValue.Message ")" } else if (IsNumber(p_errorValue)) { this.ReturnCode := p_errorValue this.Extra := "VMR Return Code (" p_errorValue ")" } } } class VMRConsts { /** * Events fired by the {@link VMR|`VMR`} object. * Use {@link @VMR.On|`VMR.On`} to register event listeners. * * @event `ParametersChanged` - Called when bus/strip parameters change * @event `LevelsUpdated` - Called when the {@link @VMRAudioIO.Level|`Level`} arrays for bus/strips are updated * @event `DevicesUpdated` - Called when the list of available devices is updated * @event `MacroButtonsChanged` - Called when macro-buttons's states change * @event `MidiMessage` - Called when a midi message is received * - The `MidiMessage` callback will be passed an array with the hex-formatted bytes of the message */ static Events := { ParametersChanged: "ParametersChanged", LevelsUpdated: "LevelsUpdated", DevicesUpdated: "DevicesUpdated", MacroButtonsChanged: "MacroButtonsChanged", MidiMessage: "MidiMessage" } /** * Default names for Voicemeeter buses * @type {Array} */ static BUS_NAMES := [ ; Voicemeeter ["A", "B"], ; Voicemeeter Banana ["A1", "A2", "A3", "B1", "B2"], ; Voicemeeter Potato ["A1", "A2", "A3", "A4", "A5", "B1", "B2", "B3"] ] static STRIP_NAMES := [ ; Voicemeeter ["Input #1", "Input #2", "Virtual Input #1"], ; Voicemeeter Banana ["Input #1", "Input #2", "Input #3", "Virtual Input #1", "Virtual Input #2"], ; Voicemeeter Potato ["Input #1", "Input #2", "Input #3", "Input #4", "Input #5", "Virtual Input #1", "Virtual Input #2", "Virtual Input #3"] ] /** * Known string parameters for {@link VMRAudioIO|`VMRAudioIO`} * @type {Array} */ static IO_STRING_PARAMETERS := [ "Device", "Device.name", "Device.wdm", "Device.mme", "Device.ks", "Device.asio", "Label", "FadeTo", "FadeBy", "AppGain", "AppMute" ] /** * Known device drivers * @type {Array} */ static DEVICE_DRIVERS := ["wdm", "mme", "asio", "ks"] /** * Default device driver, used when setting a device without specifying a driver * @type {String} */ static DEFAULT_DEVICE_DRIVER := "wdm" static REGISTRY_KEY := Format("HKLM\Software{}\Microsoft\Windows\CurrentVersion\Uninstall\VB:Voicemeeter {17359A74-1236-5467}", A_Is64bitOS ? "\WOW6432Node" : "") static DLL_FILE := A_PtrSize == 8 ? "VoicemeeterRemote64.dll" : "VoicemeeterRemote.dll" static WM_DEVICE_CHANGE := 0x0219, WM_DEVICE_CHANGE_PARAM := 0x0007 static SYNC_TIMER_INTERVAL := 10, LEVELS_TIMER_INTERVAL := 30 static AUDIO_IO_GAIN_MIN := -60.0, AUDIO_IO_GAIN_MAX := 12.0 static AUDIO_IO_LIMIT_MIN := -40.0, AUDIO_IO_LIMIT_MAX := 12.0 } class VMRDevice { __New(name, driver, hwid) { this.Name := name this.Hwid := hwid if (IsNumber(driver)) { switch driver { case 3: driver := "wdm" case 4: driver := "ks" case 5: driver := "asio" default: driver := "mme" } } this.Driver := driver } ToString() { return this.name } } /** * A static wrapper class for the Voicemeeter Remote DLL. * * Must be initialized by calling {@link VBVMR.Init|`Init()`} before using any of its static methods. */ class VBVMR { static FUNC := { Login: 0, Logout: 0, SetParameterFloat: 0, SetParameterStringW: 0, GetParameterFloat: 0, GetParameterStringW: 0, GetVoicemeeterType: 0, GetVoicemeeterVersion: 0, GetLevel: 0, Output_GetDeviceNumber: 0, Output_GetDeviceDescW: 0, Input_GetDeviceNumber: 0, Input_GetDeviceDescW: 0, IsParametersDirty: 0, MacroButton_IsDirty: 0, MacroButton_GetStatus: 0, MacroButton_SetStatus: 0, GetMidiMessage: 0, SetParameters: 0, SetParametersW: 0 } static DLL := "", DLL_PATH := "" /** * Initializes the VBVMR class by loading the Voicemeeter Remote DLL and getting the addresses of all needed functions. * If the DLL is already loaded, it returns immediately. * @param {String} p_path - (Optional) The path to the Voicemeeter Remote DLL. If not specified, it will be looked up in the registry. * __________ * @throws {VMRError} - If the DLL is not found in the specified path or if voicemeeter is not installed. */ static Init(p_path := "") { if (VBVMR.DLL != "") return VBVMR.DLL_PATH := p_path ? p_path : VBVMR._GetDLLPath() local dllPath := VBVMR.DLL_PATH "\" VMRConsts.DLL_FILE if (!FileExist(dllPath)) throw VMRError("Voicemeeter is not installed in the path :`n" . dllPath, VBVMR.Init.Name, p_path) ; Load the voicemeeter DLL VBVMR.DLL := DllCall("LoadLibrary", "Str", dllPath, "Ptr") ; Get the addresses of all needed function for (fName in VBVMR.FUNC.OwnProps()) { VBVMR.FUNC.%fName% := DllCall("GetProcAddress", "Ptr", VBVMR.DLL, "AStr", "VBVMR_" . fName, "Ptr") } } /** * @private - Internal method * @description Looks up the installation path of Voicemeeter in the registry. * __________ * @returns {String} - The installation path of Voicemeeter. */ static _GetDLLPath() { local value := "", dir := "" try value := RegRead(VMRConsts.REGISTRY_KEY, "UninstallString") catch OSError throw VMRError("Failed to retrieve the installation path of Voicemeeter", VBVMR._GetDLLPath.Name) SplitPath(value, , &dir) return dir } /** * Opens a Communication Pipe With Voicemeeter. * __________ * @returns {Number} * - `0` : OK (no error). * - `1` : OK but Voicemeeter is not launched (need to launch it manually). * @throws {VMRError} - If an internal error occurs. */ static Login() { local result try result := DllCall(VBVMR.FUNC.Login) catch Error as err throw VMRError(err, VBVMR.Login.Name) if (result < 0) throw VMRError(result, VBVMR.Login.Name) return result } /** * Closes the Communication Pipe With Voicemeeter. * __________ * @returns {Number} * - `0` : OK (no error). * @throws {VMRError} - If an internal error occurs. */ static Logout() { local result try result := DllCall(VBVMR.FUNC.Logout) catch Error as err throw VMRError(err, VBVMR.Logout.Name) if (result < 0) throw VMRError(result, VBVMR.Logout.Name) return result } /** * Sets the value of a float (numeric) parameter. * @param {String} p_prefix - The prefix of the parameter, usually the name of the bus/strip (ex: `Bus[0]`). * @param {String} p_parameter - The name of the parameter (ex: `gain`). * @param {Number} p_value - The value to set. * __________ * @returns {Number} * - `0` : OK (no error). * @throws {VMRError} - If the parameter is not found, or an internal error occurs. */ static SetParameterFloat(p_prefix, p_parameter, p_value) { local result try result := DllCall(VBVMR.FUNC.SetParameterFloat, "AStr", p_prefix . "." . p_parameter, "Float", p_value, "Int") catch Error as err throw VMRError(err, VBVMR.SetParameterFloat.Name, p_prefix, p_parameter, p_value) if (result < 0) throw VMRError(result, VBVMR.SetParameterFloat.Name, p_prefix, p_parameter, p_value) return result } /** * Sets the value of a string parameter. * @param {String} p_prefix - The prefix of the parameter, usually the name of the bus/strip (ex: `Strip[1]`). * @param {String} p_parameter - The name of the parameter (ex: `name`). * @param {String} p_value - The value to set. * __________ * @returns {Number} * - `0` : OK (no error). * @throws {VMRError} - If the parameter is not found, or an internal error occurs. */ static SetParameterString(p_prefix, p_parameter, p_value) { local result try result := DllCall(VBVMR.FUNC.SetParameterStringW, "AStr", p_prefix . "." . p_parameter, "WStr", p_value, "Int") catch Error as err throw VMRError(err, VBVMR.SetParameterString.Name, p_prefix, p_parameter, p_value) if (result < 0) throw VMRError(result, VBVMR.SetParameterString.Name, p_prefix, p_parameter, p_value) return result } /** * Returns the value of a float (numeric) parameter. * @param {String} p_prefix - The prefix of the parameter, usually the name of the bus/strip (ex: `Bus[2]`). * @param {String} p_parameter - The name of the parameter (ex: `gain`). * __________ * @returns {Number} - The value of the parameter. * @throws {VMRError} - If the parameter is not found, or an internal error occurs. */ static GetParameterFloat(p_prefix, p_parameter) { local result, value := Buffer(4) try result := DllCall(VBVMR.FUNC.GetParameterFloat, "AStr", p_prefix . "." . p_parameter, "Ptr", value, "Int") catch Error as err throw VMRError(err, VBVMR.GetParameterFloat.Name, p_prefix, p_parameter) if (result < 0) throw VMRError(result, VBVMR.GetParameterFloat.Name, p_prefix, p_parameter) value := NumGet(value, 0, "Float") return value } /** * Returns the value of a string parameter. * @param {String} p_prefix - The prefix of the parameter, usually the name of the bus/strip (ex: `Strip[1]`). * @param {String} p_parameter - The name of the parameter (ex: `name`). * __________ * @returns {String} - The value of the parameter. * @throws {VMRError} - If the parameter is not found, or an internal error occurs. */ static GetParameterString(p_prefix, p_parameter) { local result, value := Buffer(1024) try result := DllCall(VBVMR.FUNC.GetParameterStringW, "AStr", p_prefix . "." . p_parameter, "Ptr", value, "Int") catch Error as err throw VMRError(err, VBVMR.GetParameterString.Name, p_prefix, p_parameter) if (result < 0) throw VMRError(result, VBVMR.GetParameterString.Name, p_prefix, p_parameter) return StrGet(value, 512) } /** * Returns the level of a single bus/strip channel. * @param {Number} p_type - The type of the returned level * - `0`: pre-fader * - `1`: post-fader * - `2`: post-mute * - `3`: output-levels * @param {Number} p_channel - The channel's zero-based index. * - Channel Indices depend on the type of voiceemeeter running. * - Channel Indices are incremented from the left to right (On the Voicemeeter UI), starting at `0`, Buses and Strips have separate Indices (see `p_type`). * - Physical (hardware) strips have 2 channels (left, right), Buses and virtual strips have 8 channels. * __________ * @returns {Number} - The level of the requested channel. * @throws {VMRError} - If the channel index is invalid, or an internal error occurs. */ static GetLevel(p_type, p_channel) { local result, level := Buffer(4) try result := DllCall(VBVMR.FUNC.GetLevel, "Int", p_type, "Int", p_channel, "Ptr", level) catch Error as err throw VMRError(err, VBVMR.GetLevel.Name, p_type, p_channel) if (result < 0) return 0 return NumGet(level, 0, "Float") } /** * Returns the type of Voicemeeter running. * @see {@link VMR.Types|`VMR.Types`} for possible values. * __________ * @returns {Number} - The type of Voicemeeter running. * @throws {VMRError} - If an internal error occurs. */ static GetVoicemeeterType() { local result, vtype := Buffer(4) try result := DllCall(VBVMR.FUNC.GetVoicemeeterType, "Ptr", vtype, "Int") catch Error as err throw VMRError(err, VBVMR.GetVoicemeeterType.Name) if (result < 0) throw VMRError(result, VBVMR.GetVoicemeeterType.Name) return NumGet(vtype, 0, "Int") } /** * Returns the version of Voicemeeter running. * - The version is returned as a 4-part string (v1.v2.v3.v4) * __________ * @returns {String} - The version of Voicemeeter running. * @throws {VMRError} - If an internal error occurs. */ static GetVoicemeeterVersion() { local result, version := Buffer(4) try result := DllCall(VBVMR.FUNC.GetVoicemeeterVersion, "Ptr", version, "Int") catch Error as err throw VMRError(err, VBVMR.GetVoicemeeterVersion.Name) if (result < 0) throw VMRError(result, VBVMR.GetVoicemeeterVersion.Name) version := NumGet(version, 0, "Int") local v1 := (version & 0xFF000000) >>> 24, v2 := (version & 0x00FF0000) >>> 16, v3 := (version & 0x0000FF00) >>> 8, v4 := version & 0x000000FF return Format("{:d}.{:d}.{:d}.{:d}", v1, v2, v3, v4) } /** * Returns the number of Output Devices available on the system. * __________ * @returns {Number} - The number of output devices. * @throws {VMRError} - If an internal error occurs. */ static Output_GetDeviceNumber() { local result try result := DllCall(VBVMR.FUNC.Output_GetDeviceNumber, "Int") catch Error as err throw VMRError(err, VBVMR.Output_GetDeviceNumber.Name) if (result < 0) throw VMRError(result, VBVMR.Output_GetDeviceNumber.Name) return result } /** * Returns the Descriptor of an output device. * @param {Number} p_index - The index of the device (zero-based). * __________ * @returns {VMRDevice} - An object containing the `Name`, `Driver` and `Hwid` of the device. * @throws {VMRError} - If an internal error occurs. */ static Output_GetDeviceDesc(p_index) { local result, name := Buffer(1024), hwid := Buffer(1024), driver := Buffer(4) try result := DllCall(VBVMR.FUNC.Output_GetDeviceDescW, "Int", p_index, "Ptr", driver, "Ptr", name, "Ptr", hwid, "Int") catch Error as err throw VMRError(err, VBVMR.Output_GetDeviceDesc.Name, p_index) if (result < 0) throw VMRError(result, VBVMR.Output_GetDeviceDesc.Name, p_index) return VMRDevice(StrGet(name, 512), NumGet(driver, 0, "UInt"), StrGet(hwid, 512)) } /** * Returns the number of Input Devices available on the system. * __________ * @returns {Number} - The number of input devices. * @throws {VMRError} - If an internal error occurs. */ static Input_GetDeviceNumber() { local result try result := DllCall(VBVMR.FUNC.Input_GetDeviceNumber, "Int") catch Error as err throw VMRError(err, VBVMR.Input_GetDeviceNumber.Name) if (result < 0) throw VMRError(result, VBVMR.Input_GetDeviceNumber.Name) return result } /** * Returns the Descriptor of an input device. * @param {Number} p_index - The index of the device (zero-based). * __________ * @returns {VMRDevice} - An object containing the `Name`, `Driver` and `Hwid` of the device. * @throws {VMRError} - If an internal error occurs. */ static Input_GetDeviceDesc(p_index) { local result, name := Buffer(1024), hwid := Buffer(1024), driver := Buffer(4) try result := DllCall(VBVMR.FUNC.Input_GetDeviceDescW, "Int", p_index, "Ptr", driver, "Ptr", name, "Ptr", hwid, "Int") catch Error as err throw VMRError(err, VBVMR.Input_GetDeviceDesc.Name, p_index) if (result < 0) throw VMRError(result, VBVMR.Input_GetDeviceDesc.Name, p_index) return VMRDevice(StrGet(name, 512), NumGet(driver, 0, "UInt"), StrGet(hwid, 512)) } /** * Checks if any parameters have changed. * __________ * @returns {Number} * - `0` : No change * - `1` : Some parameters have changed * @throws {VMRError} - If an internal error occurs. */ static IsParametersDirty() { local result try result := DllCall(VBVMR.FUNC.IsParametersDirty) catch Error as err throw VMRError(err, VBVMR.IsParametersDirty.Name) if (result < 0) throw VMRError(result, VBVMR.IsParametersDirty.Name) return result } /** * Returns the current status of a given button. * @param {Number} p_logicalButton - The index of the button (zero-based). * @param {Number} p_bitMode - The type of the returned value. * - `0`: button-state * - `2`: displayed-state * - `3`: trigger-state * __________ * @returns {Number} - The status of the button * - `0`: Off * - `1`: On * @throws {VMRError} - If an internal error occurs. */ static MacroButton_GetStatus(p_logicalButton, p_bitMode) { local pValue := Buffer(4) try errLevel := DllCall(VBVMR.FUNC.MacroButton_GetStatus, "Int", p_logicalButton, "Ptr", pValue, "Int", p_bitMode, "Int") catch Error as err throw VMRError(err, VBVMR.MacroButton_GetStatus.Name, p_logicalButton, p_bitMode) if (errLevel < 0) throw VMRError(errLevel, VBVMR.MacroButton_GetStatus.Name, p_logicalButton, p_bitMode) return NumGet(pValue, 0, "Float") } /** * Sets the status of a given button. * @param {Number} p_logicalButton - The index of the button (zero-based). * @param {Number} p_value - The value to set. * - `0`: Off * - `1`: On * @param {Number} p_bitMode - The type of the returned value. * - `0`: button-state * - `2`: displayed-state * - `3`: trigger-state * __________ * @returns {Number} - The status of the button * - `0`: Off * - `1`: On * @throws {VMRError} - If an internal error occurs. */ static MacroButton_SetStatus(p_logicalButton, p_value, p_bitMode) { local result try result := DllCall(VBVMR.FUNC.MacroButton_SetStatus, "Int", p_logicalButton, "Float", p_value, "Int", p_bitMode, "Int") catch Error as err throw VMRError(err, VBVMR.MacroButton_SetStatus.Name, p_logicalButton, p_value, p_bitMode) if (result < 0) throw VMRError(result, VBVMR.MacroButton_SetStatus.Name, p_logicalButton, p_value, p_bitMode) return p_value } /** * Checks if any Macro Buttons states have changed. * __________ * @returns {Number} * - `0` : No change * - `> 0` : Some buttons have changed * @throws {VMRError} - If an internal error occurs. */ static MacroButton_IsDirty() { local result try result := DllCall(VBVMR.FUNC.MacroButton_IsDirty) catch Error as err throw VMRError(err, VBVMR.MacroButton_IsDirty.Name) if (result < 0) throw VMRError(result, VBVMR.MacroButton_IsDirty.Name) return result } /** * Returns any available MIDI messages from Voicemeeter's MIDI mapping. * __________ * @returns {Array} - `[0xF0, 0xFF, ...]` An array of hex-formatted bytes that compose one or more MIDI messages, or an empty string `""` if no messages are available. * - A single message is usually 2 or 3 bytes long * - The returned array will contain at most `1024` bytes. * @throws {VMRError} - If an internal error occurs. */ static GetMidiMessage() { local result, data := Buffer(1024), messages := [] try result := DllCall(VBVMR.FUNC.GetMidiMessage, "Ptr", data, "Int", 1024) catch Error as err throw VMRError(err, VBVMR.GetMidiMessage.Name) if (result == -1) throw VMRError(result, VBVMR.GetMidiMessage.Name) if (result < 1) return "" loop (result) { messages.Push(Format("0x{:X}", NumGet(data, A_Index - 1, "UChar"))) } return messages } /** * Sets one or more parameters using a voicemeeter script. * @param {String} p_script - The script to execute (must be less than `48kb`). * - Scripts can contain one or more parameter changes * - Changes can be seperated by a new line, `;` or `,`. * - Indices inside the script are zero-based. * __________ * @returns {Number} * - `0` : OK (no error) * - `> 0` : Number of the line causing an error * @throws {VMRError} - If an internal error occurs. */ static SetParameters(p_script) { local result try result := DllCall(VBVMR.FUNC.SetParametersW, "WStr", p_script, "Int") catch Error as err throw VMRError(err, VBVMR.SetParameters.Name) if (result < 0) throw VMRError(result, VBVMR.SetParameters.Name) return result } } /** * A basic wrapper for an async operation. * * This is needed because the VMR API is asynchronous which means that operations like `SetFloatParameter` do not take effect immediately, * and so if the same parameter was fetched right after it was set, the old value would be returned (or sometimes it would return a completely invalid value). * * And unfortunately, the VMR API does not provide any meaningful way to wait for a particular operation to complete (callbacks, synchronous api), and so this class uses a normal timer to wait for the operation to complete. */ class VMRAsyncOp { static DEFAULT_DELAY := 50 /** * Creates a new async operation. * * @param {() => Any} p_supplier - (Optional) Supplies the result of the async operation. * @param {Number} p_autoResolveTimeout - (Optional) Automatically resolves the async operation after the specified number of milliseconds. */ __New(p_supplier?, p_autoResolveTimeout?) { if (IsSet(p_supplier)) { if !(p_supplier is Func) throw VMRError("p_supplier must be a function.", this.__New.Name, p_supplier) this._supplier := p_supplier } this._value := "" this._listeners := [] this.IsEmpty := false this.Resolved := false if (IsSet(p_autoResolveTimeout) && IsNumber(p_autoResolveTimeout)) { if (p_autoResolveTimeout = 0) this._Resolve() else SetTimer(this._Resolve.Bind(this), -Abs(p_autoResolveTimeout)) } } /** * Creates an empty async operation that's already been resolved. * @type {VMRAsyncOp} */ static Empty { get { local empty := VMRAsyncOp() empty.IsEmpty := true empty._Resolve() return empty } } /** * Adds a listener to the async operation. * * @param {(Any) => Any} p_listener - A function that will be called when the async operation is resolved. * @param {Number} p_innerOpDelay - (Optional) If passed, the returned async operation will be delayed by the specified number of milliseconds. * __________ * @returns {VMRAsyncOp} - a new async operation that will be resolved when the current operation is resolved and the listener is called. * @throws {VMRError} - if `p_listener` is not a function or has an invalid number of parameters. */ Then(p_listener, p_innerOpDelay := 0) { if !(p_listener is Func) throw VMRError("p_listener must be a function.", this.Then.Name, p_listener) if (p_listener.MinParams > 1) throw VMRError("p_listener must require 0 or 1 parameters.", this.Then.Name, p_listener) if (this.Resolved) { local result := this._SafeCall(p_listener) return VMRAsyncOp(() => result, p_innerOpDelay) } else { local innerOp := VMRAsyncOp() this._listeners.push({ func: p_listener, op: innerOp, delay: Abs(p_innerOpDelay) }) return innerOp } } /** * Waits for the async operation to be resolved. * * @param {Number} p_timeoutMs - (Optional) The maximum number of milliseconds to wait before throwing an error. * __________ * @returns {Any} - The result of the async operation. */ Await(p_timeoutMs := 0) { if (this.Resolved) return this._value local currentMs := A_TickCount while (!this.Resolved) { if (p_timeoutMs > 0 && A_TickCount - currentMs > p_timeoutMs) throw VMRError("The async operation timed out", this.Await.Name, p_timeoutMs) Sleep(VMRAsyncOp.DEFAULT_DELAY) } return this._value } /** * Resolves the async operation. * * @param {Any} p_value - (Optional) A value to resolve the async operation with, this will take precedence over the supplier. */ _Resolve(p_value?) { if (this.Resolved) throw VMRError("This async operation has already been resolved.", this._Resolve.Name) if (IsSet(p_value)) this._value := p_value else if (this._supplier is Func) this._value := this._supplier.Call() ; If the supplier returned another async operation, resolve to the actual value. if (this._value is VMRAsyncOp) this._value := this._value.Await() this.Resolved := true for (listener in this._listeners) { local value := this._SafeCall(listener.func) , delay := listener.delay > 0 ? listener.delay : VMRAsyncOp.DEFAULT_DELAY SetTimer(listener.op._Resolve.Bind(listener.op, value), -delay) } } /** * Calls the listener with the appropriate number of parameters and catches any thrown errors. * * @param {Func} p_listener - A function that will be called when the async operation is resolved. * __________ * @returns {Any} - The result of the listener call. */ _SafeCall(p_listener) { try { if (p_listener.MaxParams = 0) { return p_listener.Call() } else if (p_listener.MinParams < 2) { return p_listener.Call(this._value) } } } } /** * A base class for {@link VMRBus|`VMRBus`} and {@link VMRStrip|`VMRStrip`} */ class VMRAudioIO { static IS_CLASS_INIT := false /** * The object's upper gain limit * @type {Number} * * Setting the gain above the limit will reset it to this value. */ GainLimit := VMRConsts.AUDIO_IO_GAIN_MAX /** * Gets/Sets the gain as a percentage * @type {Number} - The gain as a percentage (e.g. `44` = 44%) * * @example * local gain := vm.Bus[1].GainPercentage ; get the gain as a percentage * vm.Bus[1].GainPercentage++ ; increases the gain by 1% */ GainPercentage { get => VMRUtils.DbToPercentage(this.GetParameter("gain")) set => this.SetParameter("gain", VMRUtils.PercentageToDb(Value)) } /** * Set/Get the object's EQ parameters. * * @type {Number} - The EQ parameter's value. * @param {Array} p_params - An array containing the EQ parameter name and the channel/cell numbers. * * - Bus EQ parameters: `EQ[param] := value` * - EQ channel/cells parameters: `EQ[param, channel, cell] := value` * * @example * vm.Bus[1].EQ["gain", 1, 1] := -6 * vm.Bus[1].EQ["q", 1, 1] := 90 * vm.Bus[1].EQ["AB"] := true */ EQ[p_params*] { get { if (p_params.Length == 3) this.GetParameter("EQ.channel[" p_params[2] - 1 "].cell[" p_params[3] - 1 "]." p_params[1]) else this.GetParameter("EQ." p_params[1]) } set { if (p_params.Length == 3) this.SetParameter("EQ.channel[" p_params[2] - 1 "].cell[" p_params[3] - 1 "]." p_params[1], Value) else this.SetParameter("EQ." p_params[1], Value) } } /** * Gets/Sets the object's current device * * @type {VMRDevice} - The device object. * - When setting the device, either a device name or a device object can be passed, the latter can be retrieved using `VMRStrip`/`VMRBus` `GetDevice()` methods. * * @param {String} p_driver - (Optional) The driver of the device (ex: `wdm`) * * @example * vm.Bus[1].Device := VMRBus.GetDevice("Headphones") ; using a substring of the device name * vm.Bus[1].Device := "Headphones (Virtual Audio Device)" ; using a device's full name */ Device[p_driver?] { get { local devices := this.Type == "Bus" ? VMRBus.Devices : VMRStrip.Devices ; TODO: Once Voicemeeter adds support for getting the type (driver) of the current device, we can ignore the p_driver parameter return this._MatchDevice(this.GetParameter("device.name"), p_driver ?? unset) } set { local deviceName := Value, deviceDriver := p_driver ?? VMRConsts.DEFAULT_DEVICE_DRIVER ; Allow setting the device using a device object if (IsObject(Value)) { deviceDriver := Value.Driver deviceName := Value.Name } if (VMRUtils.IndexOf(VMRConsts.DEVICE_DRIVERS, deviceDriver) == -1) throw VMRError(deviceDriver " is not a valid device driver", "Device", p_driver, Value) this.SetParameter("device." deviceDriver, deviceName) } } /** * An array of the object's channel levels * @type {Array} * * Physical (hardware) strips have 2 channels (left, right), Buses and virtual strips have 8 channels * __________ * @example Get the current peak level of a bus * local peakLevel := Max(vm.Bus[1].Level*) */ Level := Array() /** * The object's identifier that's used when calling VMR's functions. * Like `Bus[0]` or `Strip[3]` * * @type {String} */ Id := "" /** * The object's one-based index * @type {Number} */ Index := 0 /** * The object's type (`Bus` or `Strip`) * @type {String} */ Type := "" /** * Creates a new `VMRAudioIO` object. * @param {Number} p_index - The zero-based index of the bus/strip. * @param {String} p_ioType - The type of the object. (`Bus` or `Strip`) */ __New(p_index, p_ioType) { this._index := p_index this._isPhysical := false this.Id := p_ioType "[" p_index "]" this.Index := p_index + 1 this.Type := p_ioType } /** * @private - Internal method * @description Implements a default property getter, this is invoked when using the object access syntax. * @example * local sampleRate := bus.device["sr"] * MsgBox("Gain is " bus.gain) * * @param {String} p_key - The name of the parameter. * @param {Array} p_params - An extra param passed when using bracket syntax with a normal prop access (`bus.device["sr"]`). * __________ * @returns {Any} The value of the parameter. * @throws {VMRError} - If an internal error occurs. */ _Get(p_key, p_params) { if (!VMRAudioIO.IS_CLASS_INIT) return "" if (p_params.Length > 0) { for param in p_params { p_key .= IsNumber(param) ? "[" param - 1 "]" : "." param } } return this.GetParameter(p_key) } /** * @private - Internal method * @description Implements a default property setter, this is invoked when using the object access syntax. * @example * bus.gain := 0.5 * bus.device["mme"] := "Headset" * * @param {String} p_key - The name of the parameter. * @param {Array} p_params - An extra param passed when using bracket syntax with a normal prop access. `bus.device["wdm"] := "Headset"` * @param {Any} p_value - The value of the parameter. * __________ * @returns {Boolean} - `true` if the parameter was set successfully. * @throws {VMRError} - If an internal error occurs. */ _Set(p_key, p_params, p_value) { if (!VMRAudioIO.IS_CLASS_INIT) return false if (p_params.Length > 0) { for param in p_params { p_key .= IsNumber(param) ? "[" param - 1 "]" : "." param } } return !this.SetParameter(p_key, p_value).IsEmpty } /** * Implements a default indexer. * this is invoked when using the bracket access syntax. * @example * MsgBox(strip["mute"]) * bus["gain"] := 0.5 * * @param {String} p_key - The name of the parameter. * __________ * @type {Any} - The value of the parameter. * @throws {VMRError} - If an internal error occurs. */ __Item[p_key] { get => this.GetParameter(p_key) set => this.SetParameter(p_key, Value) } /** * Sets the value of a parameter. * * @param {String} p_name - The name of the parameter. * @param {Any} p_value - The value of the parameter. * __________ * @returns {VMRAsyncOp} - An async operation that resolves to `true` if the parameter was set successfully. * @throws {VMRError} - If invalid parameters are passed or if an internal error occurs. */ SetParameter(p_name, p_value) { if (!VMRAudioIO.IS_CLASS_INIT) return VMRAsyncOp.Empty local vmrFunc := VMRAudioIO._IsStringParam(p_name) ? VBVMR.SetParameterString.Bind(VBVMR) : VBVMR.SetParameterFloat.Bind(VBVMR) if (p_name = "gain") { p_value := VMRUtils.EnsureBetween(p_value, VMRConsts.AUDIO_IO_GAIN_MIN, this.GainLimit) } else if (p_name = "limit") { p_value := VMRUtils.EnsureBetween(p_value, VMRConsts.AUDIO_IO_LIMIT_MIN, VMRConsts.AUDIO_IO_LIMIT_MAX) } else if (p_name = "mute") { p_value := p_value < 0 ? !this.GetParameter("mute") : p_value } local result := vmrFunc.Call(this.Id, p_name, p_value) return VMRAsyncOp(() => result == 0, 50) } /** * Returns the value of a parameter. * * @param {String} p_name - The name of the parameter. * __________ * @returns {Any} - The value of the parameter. * @throws {VMRError} - If invalid parameters are passed or if an internal error occurs. */ GetParameter(p_name) { if (!VMRAudioIO.IS_CLASS_INIT) return -1 local vmrFunc := VMRAudioIO._IsStringParam(p_name) ? VBVMR.GetParameterString.Bind(VBVMR) : VBVMR.GetParameterFloat.Bind(VBVMR) switch p_name, false { case "gain", "limit": return Format("{:.2f}", vmrFunc.Call(this.Id, p_name)) case "device": p_name := "device.name" } return vmrFunc.Call(this.Id, p_name) } /** * Increments a parameter by a specific amount. * - It's recommended to use this method instead of incrementing the parameter directly (`++vm.Bus[1].Gain`). * - Since this method doesn't fetch the current value of the parameter to update it, {@link @VMRAudioIO.GainLimit|`GainLimit`} cannot be applied here. * * @param {String} p_param - The name of the parameter, must be a numeric parameter (see {@link VMRConsts.IO_STRING_PARAMETERS|`VMRConsts.IO_STRING_PARAMETERS`}). * @param {Number} p_amount - The amount to increment the parameter by, can be set to a negative value to decrement instead. * __________ * @returns {VMRAsyncOp} - An async operation that resolves with the incremented value. * @throws {VMRError} - If invalid parameters are passed or if an internal error occurs. * __________ * @example usage with callbacks * vm.Bus[1].Increment("gain", 1).Then(val => Tooltip(val)) ; increases the gain by 1dB * vm.Bus[1].Increment("gain", -5).Then(val => Tooltip(val)) ; decreases the gain by 5dB * * @example "synchronous" usage * ; increases the gain by 1dB and waits for the operation to complete * ; this is equivalent to `vm.Bus[1].Gain++` followed by `Sleep(50)` * gainValue := vm.Bus[1].Increment("gain", 1).Await() * */ Increment(p_param, p_amount) { if (!VMRAudioIO.IS_CLASS_INIT) return VMRAsyncOp.Empty if (!IsNumber(p_amount)) throw VMRError("p_amount must be a number", this.Increment.Name, p_param, p_amount) if (VMRAudioIO._IsStringParam(p_param)) throw VMRError("p_param must be a numeric parameter", this.Increment.Name, p_param, p_amount) local script := Format("{}.{} {} {}", this.Id, p_param, p_amount < 0 ? "-=" : "+=", Abs(p_amount)) VBVMR.SetParameters(script) return VMRAsyncOp(() => this.GetParameter(p_param), 50) } /** * Sets the gain to a specific value with a progressive fade. * * @param {Number} p_db - The gain value in dBs. * @param {Number} p_duration - The duration of the fade in milliseconds. * __________ * @returns {VMRAsyncOp} - An async operation that resolves with the final gain value. * @throws {VMRError} - If invalid parameters are passed or if an internal error occurs. */ FadeTo(p_db, p_duration) { if (!VMRAudioIO.IS_CLASS_INIT) return VMRAsyncOp.Empty if (this.SetParameter("FadeTo", "(" p_db ", " p_duration ")").IsEmpty) return VMRAsyncOp.Empty return VMRAsyncOp(() => this.GetParameter("gain"), p_duration + 50) } /** * Fades the gain by a specific amount. * * @param {Number} p_dbAmount - The amount to fade the gain by in dBs. * @param {Number} p_duration - The duration of the fade in milliseconds. * _________ * @returns {VMRAsyncOp} - An async operation that resolves with the final gain value. * @throws {VMRError} - If invalid parameters are passed or if an internal error occurs. */ FadeBy(p_dbAmount, p_duration) { if (!VMRAudioIO.IS_CLASS_INIT) return VMRAsyncOp.Empty if (!this.SetParameter("FadeBy", "(" p_dbAmount ", " p_duration ")")) return VMRAsyncOp.Empty return VMRAsyncOp(() => this.GetParameter("gain"), p_duration + 50) } /** * Returns `true` if the bus/strip is a physical (hardware) one. * __________ * @returns {Boolean} */ IsPhysical() => this._isPhysical /** * @private - Internal method * @description Returns `true` if the parameter is a string parameter. * * @param {String} p_param - The name of the parameter. * @returns {Boolean} */ static _IsStringParam(p_param) => VMRUtils.IndexOf(VMRConsts.IO_STRING_PARAMETERS, p_param) > 0 /** * @private - Internal method * @description Returns a device object. * * @param {Array} p_devicesArr - An array of {@link VMRDevice|`VMRDevice`} objects. * @param {String} p_name - The name of the device. * @param {String} p_driver - The driver of the device. * @see {@link VMRConsts.DEVICE_DRIVERS|`VMRConsts.DEVICE_DRIVERS`} for a list of valid drivers. * __________ * @returns {VMRDevice} - A device object, or an empty string `""` if the device was not found. */ static _GetDevice(p_devicesArr, p_name, p_driver?) { local device, index if (!IsSet(p_driver)) p_driver := VMRConsts.DEFAULT_DEVICE_DRIVER for (index, device in p_devicesArr) { if (device.driver = p_driver && InStr(device.name, p_name)) return device.Clone() } return "" } /** * @private - Internal method * @description Returns a device object that exactly matches the specified name. * @param {String} p_name - The name of the device. * __________ * @returns {VMRDevice} */ _MatchDevice(p_name, p_driver?) { local devices := this.Type == "Bus" ? VMRBus.Devices : VMRStrip.Devices for device in devices { if (device.name == p_name && (!IsSet(p_driver) || device.driver = p_driver)) return device.Clone() } return "" } } /** * A wrapper class for voicemeeter buses. * @extends {VMRAudioIO} */ class VMRBus extends VMRAudioIO { static LEVELS_COUNT := 0 /** * An array of bus (output) devices * @type {Array} - An array of {@link VMRDevice} objects. */ static Devices := Array() /** * The bus's name (as shown in voicemeeter's UI) * * @type {String} * * @example * local busName := VMRBus.Bus[1].Name ; "A1" or "A" depending on voicemeeter's type */ Name := "" /** * Creates a new VMRBus object. * @param {Number} p_index - The zero-based index of the bus. * @param {Number} p_vmrType - The type of the running voicemeeter. */ __New(p_index, p_vmrType) { super.__New(p_index, "Bus") this._channelCount := 8 this.Name := VMRConsts.BUS_NAMES[p_vmrType][p_index + 1] switch p_vmrType { case 1: super._isPhysical := true case 2: super._isPhysical := this._index < 3 case 3: super._isPhysical := this._index < 5 } ; Setup the bus's levels array this.Level.Length := this._channelCount ; A bus's level index starts at the current total count this._levelIndex := VMRBus.LEVELS_COUNT VMRBus.LEVELS_COUNT += this._channelCount this.DefineProp("__Get", { Call: super._Get }) this.DefineProp("__Set", { Call: super._Set }) } _UpdateLevels() { loop this._channelCount { local vmrIndex := this._levelIndex + A_Index - 1 local level := Round(20 * Log(VBVMR.GetLevel(3, vmrIndex))) this.Level[A_Index] := VMRUtils.EnsureBetween(level, -999, 999) } } /** * Retrieves a bus (output) device by its name/driver. * @param {String} p_name - The name of the device. * @param {String} p_driver - (Optional) The driver of the device, If omitted, {@link VMRConsts.DEFAULT_DEVICE_DRIVER|`VMRConsts.DEFAULT_DEVICE_DRIVER`} will be used. * @see {@link VMRConsts.DEVICE_DRIVERS|`VMRConsts.DEVICE_DRIVERS`} for a list of valid drivers. * __________ * @returns {VMRDevice} - A device object, or an empty string `""` if the device was not found. */ static GetDevice(p_name, p_driver?) => VMRAudioIO._GetDevice(VMRBus.Devices, p_name, p_driver ?? unset) } /** * A wrapper class for voicemeeter strips. * @extends {VMRAudioIO} */ class VMRStrip extends VMRAudioIO { static LEVELS_COUNT := 0 /** * An array of strip (input) devices * @type {Array} - An array of {@link VMRDevice} objects. */ static Devices := Array() /** * The strip's name (as shown in voicemeeter's UI) * * @example * local stripName := VMRBus.Strip[1].Name ; "Input #1" * * @readonly * @type {String} */ Name := "" /** * Sets an application's gain on the strip. * * @param {String|Number} p_app - The name of the application, or its one-based index. * @type {Number} - The application's gain (`0.0` to `1.0`). * __________ * @throws {VMRError} - If an internal error occurs. */ AppGain[p_app] { set { if (IsNumber(p_app)) this.SetParameter("App[" p_app - 1 "].Gain", VMRUtils.EnsureBetween(Round(Value, 2), 0.0, 1.0)) else this.SetParameter("AppGain", "(`"" p_app "`", " VMRUtils.EnsureBetween(Round(Value, 2), 0.0, 1.0) ")") } } /** * Sets an application's mute state on the strip. * * @param {String|Number} p_app - The name of the application, or its one-based index. * @type {Boolean} - The application's mute state. * __________ * @throws {VMRError} - If an internal error occurs. */ AppMute[p_app] { set { if (IsNumber(p_app)) this.SetParameter("App[" p_app - 1 "].Mute", Value) else this.SetParameter("AppMute", "(`"" p_app "`", " Value ")") } } /** * Creates a new VMRStrip object. * @param {Number} p_index - The zero-based index of the strip. * @param {Number} p_vmrType - The type of the running voicemeeter. */ __New(p_index, p_vmrType) { super.__New(p_index, "Strip") this.Name := VMRConsts.STRIP_NAMES[p_vmrType][this.Index] switch p_vmrType { case 1: super._isPhysical := this._index < 2 case 2: super._isPhysical := this._index < 3 case 3: super._isPhysical := this._index < 5 } ; physical strips have 2 channels, virtual strips have 8 this._channelCount := this.IsPhysical() ? 2 : 8 ; Setup the strip's levels array this.Level.Length := this._channelCount ; A strip's level index starts at the current total count this._levelIndex := VMRStrip.LEVELS_COUNT VMRStrip.LEVELS_COUNT += this._channelCount this.DefineProp("__Get", { Call: super._Get }) this.DefineProp("__Set", { Call: super._Set }) } _UpdateLevels() { loop this._channelCount { local vmrIndex := this._levelIndex + A_Index - 1 local level := Round(20 * Log(VBVMR.GetLevel(1, vmrIndex))) this.Level[A_Index] := VMRUtils.EnsureBetween(level, -999, 999) } } /** * Retrieves a strip (input) device by its name/driver. * @param {String} p_name - The name of the device. * @param {String} p_driver - (Optional) The driver of the device, If omitted, {@link VMRConsts.DEFAULT_DEVICE_DRIVER|`VMRConsts.DEFAULT_DEVICE_DRIVER`} will be used. * @see {@link VMRConsts.DEVICE_DRIVERS|`VMRConsts.DEVICE_DRIVERS`} for a list of valid drivers. * __________ * @returns {VMRDevice} - A device object, or an empty string `""` if the device was not found. */ static GetDevice(p_name, p_driver?) => VMRAudioIO._GetDevice(VMRStrip.Devices, p_name, p_driver ?? unset) } /** * Write-only actions that control voicemeeter */ class VMRCommands { /** * Restarts the Audio Engine * __________ * @returns {Boolean} - true if the command was successful */ Restart() => VBVMR.SetParameterFloat("Command", "Restart", true) == 0 /** * Shuts down Voicemeeter * __________ * @returns {Boolean} - true if the command was successful */ Shutdown() => VBVMR.SetParameterFloat("Command", "Shutdown", true) == 0 /** * Shows the Voicemeeter window * * @param {Boolean} p_open - (Optional) `true` to show the window, `false` to hide it * __________ * @returns {Boolean} - true if the command was successful */ Show(p_open := true) => VBVMR.SetParameterFloat("Command", "Show", p_open) == 0 /** * Locks the Voicemeeter UI * * @param {number} p_state - (Optional) `true` to lock the UI, `false` to unlock it * _________ * @returns {Boolean} - true if the command was successful */ Lock(p_state := true) => VBVMR.SetParameterFloat("Command", "Lock", p_state) == 0 /** * Ejects the recorder's cassette * __________ * @returns {Boolean} - true if the command was successful */ Eject() => VBVMR.SetParameterFloat("Command", "Eject", true) == 0 /** * Resets all voicemeeeter configuration * __________ * @returns {Boolean} - true if the command was successful */ Reset() => VBVMR.SetParameterFloat("Command", "Reset", true) == 0 /** * Saves the current configuration to a file * * @param {String} p_filePath - The path to save the configuration to * __________ * @returns {Boolean} - true if the command was successful */ Save(p_filePath) => VBVMR.SetParameterString("Command", "Save", p_filePath) == 0 /** * Loads configuration from a file * * @param {String} p_filePath - The path to load the configuration from * __________ * @returns {Boolean} - true if the command was successful */ Load(p_filePath) => VBVMR.SetParameterString("Command", "Load", p_filePath) == 0 /** * Shows the VBAN chat dialog * * @param {Boolean} p_show - (Optional) `true` to show the dialog, `false` to hide it * __________ * @returns {Boolean} - true if the command was successful */ ShowVBANChat(p_show := true) => VBVMR.SetParameterFloat("Command", "dialogshow.VBANCHAT", p_show) == 0 /** * Sets a macro button's parameter * * @param {Array} p_params - An array containing the button's one-based index and parameter name. * @example * vm.Command.Button[1, "State"] := 1 * vm.Command.Button[1, "Trigger"] := false * vm.Command.Button[1, "Color"] := 8 */ Button[p_params*] { set { if (p_params.length() != 2) throw VMRError("Invalid number of parameters for Command.Button[]", "Command.Button[]", p_params*) return VBVMR.SetParameterFloat("Command.Button[" . (p_params[1] - 1) . "]", p_params[2], Value) == 0 } } /** * Saves a bus's EQ settings to a file * * @param {Number} p_busIndex - The one-based index of the bus to save * @param {String} p_filePath - The path to save the EQ settings to * __________ * @returns {Boolean} - true if the command was successful */ SaveBusEQ(p_busIndex, p_filePath) { return VBVMR.SetParameterFloat("Command", "SaveBUSEQ[" p_busIndex - 1 "]", p_filePath) == 0 } /** * Loads a bus's EQ settings from a file * * @param {Number} p_busIndex - The one-based index of the bus to load * @param {String} p_filePath - The path to load the EQ settings from * __________ * @returns {Boolean} - true if the command was successful */ LoadBusEQ(p_busIndex, p_filePath) { return VBVMR.SetParameterFloat("Command", "LoadBUSEQ[" p_busIndex - 1 "]", p_filePath) == 0 } /** * Saves a strip's EQ settings to a file * * @param {Number} p_stripIndex - The one-based index of the strip to save * @param {String} p_filePath - The path to save the EQ settings to * __________ * @returns {Boolean} - true if the command was successful */ SaveStripEQ(p_stripIndex, p_filePath) { return VBVMR.SetParameterFloat("Command", "SaveStripEQ[" p_stripIndex - 1 "]", p_filePath) == 0 } /** * Loads a strip's EQ settings from a file * * @param {Number} p_stripIndex - The one-based index of the strip to load * @param {String} p_filePath - The path to load the EQ settings from * __________ * @returns {Boolean} - true if the command was successful */ LoadStripEQ(p_stripIndex, p_filePath) { return VBVMR.SetParameterFloat("Command", "LoadStripEQ[" p_stripIndex - 1 "]", p_filePath) == 0 } /** * Recalls a Preset Scene * * @param {String | Number} p_preset - The name of the preset to recall or its one-based index * __________ * @returns {Boolean} - true if the command was successful */ RecallPreset(p_preset) { if (IsNumber(p_preset)) return VBVMR.SetParameterFloat("Command", "Preset[" p_preset - 1 "].Recall", 1) == 0 else return VBVMR.SetParameterString("Command", "RecallPreset", p_preset) == 0 } } class VMRControllerBase { __New(p_id, p_stringParamChecker) { this.DefineProp("Id", { Get: (*) => p_id }) this.DefineProp("StringParamChecker", { Call: p_stringParamChecker }) } /** * @private - Internal method * @description Implements a default property getter, this is invoked when using the object access syntax. * * @param {String} p_key - The name of the parameter. * @param {Array} p_params - An extra param passed when using bracket syntax with a normal prop access (`bus.device["sr"]`). * __________ * @returns {Any} The value of the parameter. * @throws {VMRError} - If an internal error occurs. */ __Get(p_key, p_params) { if (p_params.Length > 0) { for param in p_params { p_key .= IsNumber(param) ? "[" param "]" : "." param } } return this.GetParameter(p_key) } /** * @private - Internal method * @description Implements a default property setter, this is invoked when using the object access syntax. * * @param {String} p_key - The name of the parameter. * @param {Array} p_params - An extra param passed when using bracket syntax with a normal prop access. `bus.device["wdm"] := "Headset"` * @param {Any} p_value - The value of the parameter. * __________ * @returns {Boolean} - `true` if the parameter was set successfully. * @throws {VMRError} - If an internal error occurs. */ __Set(p_key, p_params, p_value) { if (p_params.Length > 0) { for param in p_params { p_key .= IsNumber(param) ? "[" param "]" : "." param } } return this.SetParameter(p_key, p_value) == 0 } /** * Implements a default indexer. * this is invoked when using the bracket access syntax. * * @param {String} p_key - The name of the parameter. * __________ * @type {Any} - The value of the parameter. * @throws {VMRError} - If an internal error occurs. */ __Item[p_key] { get => this.GetParameter(p_key) set => this.SetParameter(p_key, Value) } /** * Sets the value of a parameter. * * @param {String} p_name - The name of the parameter. * @param {Any} p_value - The value of the parameter. * __________ * @returns {VMRAsyncOp} - An async operation that resolves to `true` if the parameter was set successfully. * @throws {VMRError} - If invalid parameters are passed or if an internal error occurs. */ SetParameter(p_name, p_value) { local vmrFunc := this.StringParamChecker(p_name) ? VBVMR.SetParameterString.Bind(VBVMR) : VBVMR.SetParameterFloat.Bind(VBVMR) local result := vmrFunc.Call(this.Id, p_name, p_value) return VMRAsyncOp(() => result == 0, 50) } /** * Returns the value of a parameter. * * @param {String} p_name - The name of the parameter. * __________ * @returns {Any} - The value of the parameter. * @throws {VMRError} - If invalid parameters are passed or if an internal error occurs. */ GetParameter(p_name) { local vmrFunc := this.StringParamChecker(p_name) ? VBVMR.GetParameterString.Bind(VBVMR) : VBVMR.GetParameterFloat.Bind(VBVMR) return vmrFunc.Call(this.Id, p_name) == 0 } } class VMRMacroButton { static EXECUTABLE := "VoicemeeterMacroButtons.exe" /** * Run the Voicemeeter Macro Buttons application. * * @returns {void} */ Run() => Run(VBVMR.DLL_PATH "\" VMRMacroButton.EXECUTABLE, VBVMR.DLL_PATH) /** * Shows/Hides the Voicemeeter Macro Buttons application. * @param {Boolean} p_show - Whether to show or hide the application */ Show(p_show := true) { if (p_show) { if (!WinExist("ahk_exe " VMRMacroButton.EXECUTABLE)) this.Run(), Sleep(500) WinShow("ahk_exe " VMRMacroButton.EXECUTABLE) } else { WinHide("ahk_exe " VMRMacroButton.EXECUTABLE) } } /** * Sets the status of a given button. * @param {Number} p_index - The one-based index of the button * @param {Number} p_value - The value to set * - `0`: Off * - `1`: On * @param {Number} p_bitMode - The type of the returned value * - `0`: button-state * - `2`: displayed-state * - `3`: trigger-state * __________ * @returns {Number} - The status of the button * - `0`: Off * - `1`: On * @throws {VMRError} - If an internal error occurs */ SetStatus(p_index, p_value, p_bitMode := 0) => VBVMR.MacroButton_SetStatus(p_index - 1, p_value, p_bitMode) /** * Gets the status of a given button. * @param {Number} p_index - The one-based index of the button * @param {Number} p_bitMode - The type of the returned value * - `0`: button-state * - `2`: displayed-state * - `3`: trigger-state * __________ * @returns {Number} - The status of the button * - `0`: Off * - `1`: On * @throws {VMRError} - If an internal error occurs */ GetStatus(p_index, p_bitMode := 0) => VBVMR.MacroButton_GetStatus(p_index - 1, p_bitMode) } class VMRRecorder extends VMRControllerBase { static _stringParameters := ["load"] __New(p_type) { super.__New("recorder", (_, p) => VMRUtils.IndexOf(VMRRecorder._stringParameters, p) != -1) this.DefineProp("TypeInfo", { Get: (*) => p_type }) } /** * Arms the specified bus for recording, switching the recording mode to `1` (bus). * Or returns the state of the specified bus (whether it's armed or not). * @param {number} p_index - The bus's one-based index. * __________ * @type {boolean} - Whether the bus is armed or not. * * @example Arm the first bus for recording. * VMR.Recorder.ArmBus[1] := true */ ArmBus[p_index] { get { return this.GetParameter("ArmBus(" (p_index - 1) ")") } set { this.SetParameter("mode.recbus", true) this.SetParameter("ArmBus(" . (p_index - 1) . ")", Value) } } /** * Arms the specified strip for recording, switching the recording mode to `0` (strip). * Or returns the state of the specified strip (whether it's armed or not). * @param {number} p_index - The strip's one-based index. * __________ * @type {boolean} - Whether the strip is armed or not. * * @example Arm a strip for recording. * VMR.Recorder.ArmStrip[4] := true */ ArmStrip[p_index] { get { return this.GetParameter("ArmStrip(" (p_index - 1) ")") } set { this.SetParameter("mode.recbus", false) this.SetParameter("ArmStrip(" . (p_index - 1) . ")", Value) } } /** * Arms the specified strips for recording, switching the recording mode to `0` (strip) and disarming any armed strips. * @param {Array} p_strips - The strips' one-based indices. * * @example Arm strips 1, 2, and 4 for recording. * VMR.Recorder.ArmStrips(1, 2, 4) */ ArmStrips(p_strips*) { loop this.TypeInfo.StripCount this.ArmStrip[A_Index] := false for i in p_strips this.ArmStrip[i] := true } /** * Loads the specified file into the recorder. * @param {String} p_path - The file's path. * __________ * @returns {VMRAsyncOp} - An async operation that resolves to `true` if the parameter was set successfully. * @throws {VMRError} - If invalid parameters are passed or if an internal error occurs. */ Load(p_path) => this.SetParameter("load", p_path) } class VMRVBAN extends VMRControllerBase { /** * Controls a VBAN input stream * @type {VMRControllerBase} */ Instream[p_index] { get { return this._instreams.Get(p_index) } } /** * Controls a VBAN output stream * @type {VMRControllerBase} */ Outstream[p_index] { get { return this._outstreams.Get(p_index) } } __New(p_type) { super.__New("vban", (*) => false) this.DefineProp("TypeInfo", { Get: (*) => p_type }) local stringParams := ["name", "ip"] local streamStringParamChecker := (_, p) => VMRUtils.IndexOf(stringParams, p) != -1 local instreams := Array(), outstreams := Array() loop p_type.VbanCount { instreams.Push(VMRControllerBase("vban.instream[" A_Index - 1 "]", streamStringParamChecker)) outstreams.Push(VMRControllerBase("vban.outstream[" A_Index - 1 "]", streamStringParamChecker)) } this.DefineProp("_instreams", { Get: (*) => instreams }) this.DefineProp("_outstreams", { Get: (*) => outstreams }) } } /** * A wrapper class for Voicemeeter Remote that hides the low-level API to simplify usage. * Must be initialized by calling {@link @VMR.Login|`Login()`} after creating the VMR instance. */ class VMR { /** * The type of Voicemeeter that is currently running. * @type {VMR.Types} - An object containing information about the current Voicemeeter type. * @see {@link VMR.Types|`VMR.Types`} for a list of available types. */ Type := "" /** * The version of Voicemeeter that is currently running. * @type {String} - The version string in the format `v1.v2.v3.v4` (ex: `2.1.0.5`). * @see The AHK function {@link VerCompare|`VerCompare`} can be used to compare version strings. */ Version := "" /** * An array of voicemeeter buses * @type {Array} - An array of {@link VMRBus|`VMRBus`} objects. */ Bus := Array() /** * An array of voicemeeter strips * @type {Array} - An array of {@link VMRStrip|`VMRStrip`} objects. */ Strip := Array() /** * Commands that control various aspects of Voicemeeter * @type {VMRCommands} * @see {@link VMRCommands|`VMRCommands`} for a list of available commands. */ Command := VMRCommands() /** * Controls Voicemeeter Potato's FX settings * #### This property is only available when running Voicemeeter Potato (`VMR.Type.Id == 3`). * @type {VMRControllerBase} */ Fx := "" /** * Controls Voicemeeter's Patch parameters * @type {VMRControllerBase} */ Patch := VMRControllerBase("Patch", (*) => false) /** * Controls Voicemeeter's System Settings * @type {VMRControllerBase} */ Option := VMRControllerBase("Option", (*) => false) /** * Controls Voicemeeter's Macro Buttons app * @type {VMRMacroButton} */ MacroButton := VMRMacroButton() /** * Controls Voicemeeter's Recorder * #### This property is only available when running Voicemeeter Banana or Potato (`VMR.Type.Id == 2 || VMR.Type.Id == 3`). * @type {VMRRecorder} */ Recorder := "" /** * Controls Voicemeeter's VBAN interface * @type {VMRVBAN} */ VBAN := "" /** * Creates a new VMR instance and initializes the {@link VBVMR|`VBVMR`} class. * @param {String} p_path - (Optional) The path to the Voicemeeter Remote DLL. If not specified, VBVMR will attempt to find it in the registry. * __________ * @throws {VMRError} - If the DLL is not found in the specified path or if voicemeeter is not installed. */ __New(p_path := "") { VBVMR.Init(p_path) this._eventListeners := Map() this._eventListeners.CaseSense := "Off" for (event in VMRConsts.Events.OwnProps()) this._eventListeners[event] := [] } /** * Initializes the VMR instance and opens the communication pipe with Voicemeeter. * @param {Boolean} p_launchVoicemeeter - (Optional) Whether to launch Voicemeeter if it's not already running. Defaults to `true`. * __________ * @returns {VMR} The {@link VMR|`VMR`} instance. * @throws {VMRError} - If an internal error occurs. */ Login(p_launchVoicemeeter := true) { local loginStatus := VBVMR.Login() ; Check if we should launch the Voicemeeter UI if (loginStatus != 0 && p_launchVoicemeeter) { local vmPID := this.RunVoicemeeter() WinWait("ahk_class VBCABLE0Voicemeeter0MainWindow0 ahk_pid" vmPID) Sleep(2000) } this.Version := VBVMR.GetVoicemeeterVersion() this.Type := VMR.Types.GetType(VBVMR.GetVoicemeeterType()) if (!this.Type) throw VMRError("Unsupported Voicemeeter type: " . VBVMR.GetVoicemeeterType(), this.Login.Name, p_launchVoicemeeter) OnExit(this.__Delete.Bind(this)) ; Initialize VMR components (bus/strip arrays, macro buttons, etc) this._InitializeComponents() ; Setup timers this._syncTimer := this.Sync.Bind(this) this._levelsTimer := this._UpdateLevels.Bind(this) SetTimer(this._syncTimer, VMRConsts.SYNC_TIMER_INTERVAL) SetTimer(this._levelsTimer, VMRConsts.LEVELS_TIMER_INTERVAL) ; Listen for device changes to update the device arrays this._updateDevicesCallback := this.UpdateDevices.Bind(this) OnMessage(VMRConsts.WM_DEVICE_CHANGE, this._updateDevicesCallback) this.Sync() return this } /** * Attempts to run Voicemeeter. * When passing a `p_type`, it will only attempt to run the specified Voicemeeter type, * otherwise it will attempt to run every voicemeeter type descendingly until one is successfully launched. * * @param {Number} p_type - (Optional) The type of Voicemeeter to run. * __________ * @returns {Number} The PID of the launched Voicemeeter process. * @throws {VMRError} If the specified Voicemeeter type is invalid, or if no Voicemeeter type could be launched. */ RunVoicemeeter(p_type?) { local vmPID := "" if (IsSet(p_type)) { local vmInfo := VMR.Types.GetType(p_type) if (!vmInfo) throw VMRError("Invalid Voicemeeter type: " . p_type, this.RunVoicemeeter.Name, p_type) local vmPath := VBVMR.DLL_PATH . "\" . vmInfo.Executable Run(vmPath, VBVMR.DLL_PATH, "Hide", &vmPID) return vmPID } local vmTypeCount := VMR.Types.Count loop vmTypeCount { try { vmPID := this.RunVoicemeeter((vmTypeCount + 1) - A_Index) return vmPID } } throw VMRError("Failed to launch Voicemeeter", this.RunVoicemeeter.Name) } /** * Registers a callback function to be called when the specified event is fired. * @param {String} p_event - The name of the event to listen for. * @param {Func} p_listener - The function to call when the event is fired. * __________ * @example vm.On(VMRConsts.Events.ParametersChanged, () => MsgBox("Parameters changed!")) * @see {@link VMRConsts.Events|`VMRConsts.Events`} for a list of available events. * __________ * @throws {VMRError} If the specified event is invalid, or if the listener is not a valid `Func` object. */ On(p_event, p_listener) { if (!this._eventListeners.Has(p_event)) throw VMRError("Invalid event: " p_event, this.On.Name, p_event, p_listener) if !(p_listener is Func) throw VMRError("Invalid listener: " String(p_listener), this.On.Name, p_event, p_listener) local eventListeners := this._eventListeners[p_event] if (VMRUtils.IndexOf(eventListeners, p_listener) == -1) eventListeners.Push(p_listener) } /** * Removes a callback function from the specified event. * @param {String} p_event - The name of the event. * @param {Func} p_listener - (Optional) The function to remove, if omitted, all listeners for the specified event will be removed. * __________ * @example vm.Off("parametersChanged", myListener) * @see {@link VMRConsts.Events|`VMRConsts.Events`} for a list of available events. * __________ * @returns {Boolean} Whether the listener was removed. * @throws {VMRError} If the specified event is invalid, or if the listener is not a valid `Func` object. */ Off(p_event, p_listener?) { if (!this._eventListeners.Has(p_event)) throw VMRError("Invalid event: " p_event, this.Off.Name, p_event, p_listener) if (!IsSet(p_listener)) { this._eventListeners[p_event] := Array() return true } if !(p_listener is Func) throw VMRError("Invalid listener: " String(p_listener), this.Off.Name, p_event, p_listener) local eventListeners := this._eventListeners[p_event] local listenerIndex := VMRUtils.IndexOf(eventListeners, p_listener) if (listenerIndex == -1) return false eventListeners.RemoveAt(listenerIndex) return true } /** * Synchronizes the VMR instance with Voicemeeter. * __________ * @returns {Boolean} - Whether voicemeeter state has changed since the last sync. */ Sync() { static ignoreMsg := false try { ; Prevent multiple syncs from running at the same time Critical("On") local dirtyParameters := VBVMR.IsParametersDirty() , dirtyMacroButtons := VBVMR.MacroButton_IsDirty() ; Api calls were successful -> reset ignoreMsg flag ignoreMsg := false if (dirtyParameters > 0) this._DispatchEvent(VMRConsts.Events.ParametersChanged) if (dirtyMacroButtons > 0) this._DispatchEvent(VMRConsts.Events.MacroButtonsChanged) ; Check if there are any listeners for midi messages local midiListeners := this._eventListeners[VMRConsts.Events.MidiMessage] if (midiListeners.Length > 0) { ; Get new midi messages and dispatch event if there's any local midiMessages := VBVMR.GetMidiMessage() if (midiMessages && midiMessages.Length > 0) this._DispatchEvent(VMRConsts.Events.MidiMessage, midiMessages) } Critical("Off") return dirtyParameters || dirtyMacroButtons } catch Error as err { Critical("Off") if (ignoreMsg) return false result := MsgBox( Format("An error occurred during VMR sync:`n{}`nDetails: {}`nAttempt to restart Voicemeeter?", err.Message, err.Extra), "VMR", "YesNo Icon! T10" ) switch (result) { case "Yes": this.runVoicemeeter(this.Type.id) case "No", "Timeout": ignoreMsg := true } Sleep(1000) return false } } /** * Executes a Voicemeeter script (**not** an AutoHotkey script). * - Scripts can contain one or more parameter changes * - Changes can be seperated by a new line, `;` or `,`. * - Indices in the script are zero-based. * * @param {String} p_script - The script to execute. * __________ * @throws {VMRError} If an error occurs while executing the script. */ Exec(p_script) { local result := VBVMR.SetParameters(p_script) if (result > 0) throw VMRError("An error occurred while executing the script at line: " . result, this.Exec.Name, p_script) } /** * Updates the list of strip/bus devices. * @param {Number} p_wParam - (Optional) If passed, must be equal to {@link VMRConsts.WM_DEVICE_CHANGE_PARAM|`VMRConsts.WM_DEVICE_CHANGE_PARAM`} to update the device arrays. * __________ * @throws {VMRError} If an internal error occurs. */ UpdateDevices(p_wParam?, *) { if (IsSet(p_wParam) && p_wParam != VMRConsts.WM_DEVICE_CHANGE_PARAM) return VMRStrip.Devices := Array() loop VBVMR.Input_GetDeviceNumber() VMRStrip.Devices.Push(VBVMR.Input_GetDeviceDesc(A_Index - 1)) VMRBus.Devices := Array() loop VBVMR.Output_GetDeviceNumber() VMRBus.Devices.Push(VBVMR.Output_GetDeviceDesc(A_Index - 1)) SetTimer(() => this._DispatchEvent(VMRConsts.Events.DevicesUpdated), -20) } ToString() { local value := "VMR:`n" if (this.Type) { value .= "Logged into " . this.Type.name . " in (" . VBVMR.DLL_PATH . ")" } else { value .= "Not logged in" } return value } /** * @private - Internal method * @description Dispatches an event to all listeners. * * @param {String} p_event - The name of the event to dispatch. * @param {Array} p_args - (Optional) An array of arguments to pass to the listeners. */ _DispatchEvent(p_event, p_args*) { local eventListeners := this._eventListeners[p_event] if (eventListeners.Length == 0) return _eventDispatcher() { for (listener in eventListeners) { if (p_args.Length == 0 || listener.MaxParams < p_args.Length) listener() else listener(p_args*) } } SetTimer(_eventDispatcher, -1) } /** * @private - Internal method * @description Initializes VMR's components (bus/strip arrays, macroButton obj, etc). */ _InitializeComponents() { this.Bus := Array() loop this.Type.busCount { this.Bus.Push(VMRBus(A_Index - 1, this.Type.id)) } this.Strip := Array() loop this.Type.stripCount { this.Strip.Push(VMRStrip(A_Index - 1, this.Type.id)) } if (this.Type.Id > 1) this.Recorder := VMRRecorder(this.Type) if (this.Type.Id == 3) this.Fx := VMRControllerBase("Fx", (*) => false) this.VBAN := VMRVBAN(this.Type) this.UpdateDevices() VMRAudioIO.IS_CLASS_INIT := true } /** * @private - Internal method * @description Updates the levels of all buses/strips. */ _UpdateLevels() { local bus, strip for (bus in this.Bus) { bus._UpdateLevels() } for (strip in this.Strip) { strip._UpdateLevels() } this._DispatchEvent(VMRConsts.Events.LevelsUpdated) } /** * @private - Internal method * @description Prepares the VMR instance for deletion (turns off timers, etc..) and logs out of Voicemeeter. */ __Delete(*) { if (!this.Type) return this.Type := "" this.Bus := "" this.Strip := "" if (this._syncTimer) SetTimer(this._syncTimer, 0) if (this._levelsTimer) SetTimer(this._levelsTimer, 0) if (this._updateDevicesCallback) OnMessage(VMRConsts.WM_DEVICE_CHANGE, this._updateDevicesCallback, 0) while (this.Sync()) { } ; Make sure all commands finish executing before logging out Sleep(100) VBVMR.Logout() } /** * Known Voicemeeter types info */ class Types { static Count := 3 static Standard := VMR.Types(1, "Voicemeeter", "voicemeeter.exe", 2, 3, 4) static Banana := VMR.Types(2, "Voicemeeter Banana", "voicemeeterpro.exe", 5, 5, 8) static Potato := VMR.Types(3, "Voicemeeter Potato", "voicemeeter8" (A_Is64bitOS ? "x64" : "") ".exe", 8, 8, 8) __New(id, name, executable, busCount, stripCount, vbanCount) { this.Id := id this.Name := name this.Executable := executable this.BusCount := busCount this.StripCount := stripCount this.VbanCount := vbanCount } /** * Returns the voicemeeter type with the specified id. * @param {Number} p_id - The id of the type. * @returns {VMR.Types} */ static GetType(p_id) { switch (p_id) { case 1: return VMR.Types.Standard case 2: return VMR.Types.Banana case 3: return VMR.Types.Potato } } } }