diff --git a/.gitignore b/.gitignore index bf47728..7912ca2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.exe _site .jekyll-cache -.vscode \ No newline at end of file +.vscode/launch.json +src/v1 \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..004aafa --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "thqby.vscode-autohotkey2-lsp", + "zero-plusplus.vscode-autohotkey-debug" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..67446cb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "AutoHotkey2.ActionWhenV1IsDetected": "Warn", + "AutoHotkey2.AutoLibInclude": "Local", + "AutoHotkey2.CompleteFunctionParens": true, + "AutoHotkey2.Files.ScanMaxDepth": 3, + "AutoHotkey2.FormatOptions": { + "brace_style": "One True Brace Variant", + "keep_array_indentation": true, + "keyword_start_with_uppercase": false, + "max_preserve_newlines": 3, + "preserve_newlines": true, + "space_after_double_colon": true, + "switch_case_alignment": false, + "symbol_with_same_case": true + }, + "AutoHotkey2.Warn.CallWithoutParentheses": "On", + "AutoHotkey2.Warn.LocalSameAsGlobal": true, +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..3f37483 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Build VMR", + "type": "process", + "command": "${config:AutoHotkey2.InterpreterPath}", + "options": { + "cwd": "${workspaceFolder}" + }, + "args": [ + { + "value": ".\\src\\Build.ahk", + "quoting": "strong" + }, + { + "value": ".\\src\\VMR.ahk", + "quoting": "strong" + }, + { + "value": ".\\dist\\VMR.ahk", + "quoting": "strong" + }, + { + "value": "${input:buildVersion}", + "quoting": "strong" + } + ], + "problemMatcher": [], + } + ], + "inputs": [ + { + "id": "buildVersion", + "description": "Please enter the build version", + "type": "promptString", + "default": "ahkpm" + } + ], +} \ No newline at end of file diff --git a/Build.ahk b/Build.ahk deleted file mode 100644 index 19b2ddb..0000000 --- a/Build.ahk +++ /dev/null @@ -1,52 +0,0 @@ -; Run command: autohotkey.exe .\Build.ahk -global new_line:= "`r`n" -, header := " -(LTrim Join`r`n - ; - ; VMR.ahk v" A_Args[1] " - ; Build timestamp: " A_NowUTC " - ; Repo: https://github.com/SaifAqqad/VMR.ahk - ; Docs: https://saifaqqad.github.io/VMR.ahk - ; -)" -, output_path:= A_ScriptDir "\dist" -, src_path:= A_ScriptDir "\src" -, base_file := "VMR.ahk" -, file_content:= FileOpen(src_path "\" base_file, "r").Read() - -global include_regex:= "imO)^( *)`;\s*vmr-include\s+?'(.+?)'$" -, current_match:="" -, current_pos:=1 - -Loop{ - current_pos:= RegExMatch(file_content, include_regex, current_match, current_pos) - if(current_pos = 0) - break - str_to_replace:= current_match.Value() - indent_value:= current_match.Value(1) - include_path:= src_path "\" current_match.Value(2) - include_file_content:= FileOpen(include_path, "r").Read() - if(indent_value) - include_file_content:= insertPrefix(include_file_content, indent_value) - include_file_length:= StrLen(include_file_content) - if(include_file_length){ - file_content:= RegExReplace(file_content, "\Q" str_to_replace "\E", include_file_content,, 1, current_pos) - current_pos+= include_file_length - } -} - -global header_regex:= "imO)^( *)(?:`;\s*)vmr-header" -file_content:= RegExReplace(file_content, header_regex, header,,1) - -FileCreateDir, % output_path -FileOpen(output_path "\" base_file, "w`n").Write(file_content) - - -insertPrefix(text, prefix){ - output:="" - Loop, parse, text, `n, `r - { - output.= prefix A_LoopField new_line - } - return output -} \ No newline at end of file diff --git a/README.md b/README.md index bdc5a07..c2631c4 100644 --- a/README.md +++ b/README.md @@ -6,31 +6,43 @@ VMR.ahk

## Getting Started + To use `VMR.ahk` in your script, follow one of the following methods: -### A - ahkpm installation -1. Install and set up [ahkpm](https://github.com/joshuacc/ahkpm), then run `ahkpm install gh:SaifAqqad/VMR.ahk` -2. Run `ahkpm include -f my-script.ahk gh:SaifAqqad/VMR.ahk` to add an include directive in your script - ###### Replace *my-script.ahk* with your script's path +### A. ahkpm installation + +1. Install and set up [ahkpm](https://github.com/joshuacc/ahkpm) +2. Run `ahkpm install gh:SaifAqqad/VMR.ahk` +3. Include VMR in your script by running `ahkpm include gh:SaifAqqad/VMR.ahk -f myScript.ahk` + ###### Replace *myScript.ahk* with your script's path + +### B. Manual Installation -### B - Manual Installation -1. Download the latest pre-built version from the [`dist` folder](https://raw.githubusercontent.com/SaifAqqad/VMR.ahk/master/dist/VMR.ahk) or follow the build instructions below +1. Download the latest pre-built version from the [`dist` folder](https://raw.githubusercontent.com/SaifAqqad/VMR.ahk/master/dist/VMR.ahk) / [latest release](https://github.com/SaifAqqad/VMR.ahk/releases) or follow the build instructions below +2. Include it using `#Include VMR.ahk` or copy it to a [library folder](https://www.autohotkey.com/docs/v2/Scripts.htm#lib) and use `#Include ` -2. Include it using `#Include VMR.ahk` or copy it to a [library folder](https://www.autohotkey.com/docs/Functions.htm#lib) and use `#Include ` +> [!IMPORTANT] +> The current version of VMR ***only*** supports AHK v2, The AHK v1 version is still available on the [v1 branch](https://github.com/SaifAqqad/VMR.ahk/tree/v1) but will (probably) not receive any updates. -3. Create an instance of the VMR class and log in to the API: - ```ahk - voicemeeter:= new VMR().login() - ``` -4. The `VMR` object will have two arrays (`bus` and`strip`), as well as other objects, that will allow you to control voicemeeter in AHK. - ```ahk - voicemeeter.bus[1].mute:= true - voicemeeter.strip[4].gain++ - ``` +## Basic usage +- Create an instance of the `VMR` class and log in to the API: + ```ahk + voicemeeter := VMR().Login() + ``` +- The `VMR` instance will have two arrays (`Bus` and `Strip`), as well as other properties/methods that will allow you to control voicemeeter in AHK + ```ahk + voicemeeter.Bus[1].mute := true + voicemeeter.Strip[4].gain++ + ``` + ##### For more info, check out the [documentation](https://saifaqqad.github.io/VMR.ahk/) and the [examples](./examples/) ## Build instructions -To build `VMR.ahk` yourself, run the build script: + +To build `VMR.ahk`, either run the vscode task `Build VMR` or run the build script using ahkpm or manually: + ```powershell -.\Build.ahk +# ahkpm +ahkpm run build +# Manually +Autohotkey.exe ".\Build.ahk" ".\VMR.ahk" "..\dist\VMR.ahk" "" ``` -##### For more info, check out the [documentation](https://saifaqqad.github.io/VMR.ahk/) diff --git a/ahkpm.json b/ahkpm.json index 456d7fe..db8fd7b 100644 --- a/ahkpm.json +++ b/ahkpm.json @@ -1,5 +1,5 @@ { - "version": "1.2.0", + "version": "2.0.0", "description": "AutoHotkey wrapper class for Voicemeeter's Remote API", "repository": "https://github.com/SaifAqqad/VMR.ahk", "website": "https://saifaqqad.github.io/VMR.ahk", @@ -11,5 +11,8 @@ "email": "", "website": "" }, + "scripts": { + "build": "& .\\src\\Build.ahk '.\\src\\VMR.ahk' '.\\dist\\VMR.ahk' 'ahkpm'" + }, "dependencies": {} -} +} \ No newline at end of file diff --git a/dist/VMR.ahk b/dist/VMR.ahk index 9b505d6..1f08bec 100644 --- a/dist/VMR.ahk +++ b/dist/VMR.ahk @@ -1,851 +1,2052 @@ -; -; VMR.ahk v1.2.0 -; Build timestamp: 20230520071651 -; Repo: https://github.com/SaifAqqad/VMR.ahk -; Docs: https://saifaqqad.github.io/VMR.ahk -; -class VMR{ - bus:="" - , strip:="" - , recorder:="" - , option:="" - , patch:="" - , fx:="" - , onUpdateLevels:="" - , onUpdateParameters:="" - , onUpdateMacrobuttons:="" - , onMidiMessage:="" - - __New(p_path:=""){ - VBVMR.DLL_PATH := p_path? p_path : this.__getDLLPath() - VBVMR.DLL_FILE := A_PtrSize = 8 ? "VoicemeeterRemote64.dll" : "VoicemeeterRemote.dll" - if(!FileExist(VBVMR.DLL_PATH . "\" . VBVMR.DLL_FILE)) - Throw, Exception(Format("Voicemeeter is not installed in the path :`n{}", VBVMR.DLL_PATH)) - VBVMR.STR_TYPE := A_IsUnicode? "W" : "A" - VBVMR.DLL := DllCall("LoadLibrary", "Str", VBVMR.DLL_PATH . "\" . VBVMR.DLL_FILE, "Ptr") - VBVMR.__getAddresses() - } - - login(){ - if(VBVMR.Login()){ - this.runVoicemeeter() - WinWait, ahk_class VBCABLE0Voicemeeter0MainWindow0 - sleep, 2000 - } - OnExit(ObjBindMethod(this, "__onExit")) - syncWithDLL := ObjBindMethod(this, "__syncWithDLL") - SetTimer, %syncWithDLL%, 20 - this.getType() - this.__init_arrays() - this.__init_obj() - this.__syncWithDLL() - return this +/** + * 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) } - - - getType(){ - if(!VBVMR.VM_TYPE){ - VBVMR.VM_TYPE:= VBVMR.GetVoicemeeterType() - Switch VBVMR.VM_TYPE { - case 1: - VBVMR.BUSCOUNT:= 2 - VBVMR.STRIPCOUNT:= 3 - VBVMR.VBANINCOUNT:= 4 - VBVMR.VBANOUTCOUNT:= 4 - case 2: - VBVMR.BUSCOUNT:= 5 - VBVMR.STRIPCOUNT:= 5 - VBVMR.VBANINCOUNT:= 8 - VBVMR.VBANOUTCOUNT:= 8 + /** + * 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: - VBVMR.BUSCOUNT:= 8 - VBVMR.STRIPCOUNT:= 8 - VBVMR.VBANINCOUNT:= 8 - VBVMR.VBANOUTCOUNT:= 8 + driver := "wdm" + case 4: + driver := "ks" + case 5: + driver := "asio" + default: + driver := "mme" } } - return VBVMR.VM_TYPE - } - - runVoicemeeter(p_type := ""){ - if(p_type){ - Run, % VBVMR.DLL_PATH "\" this.__getTypeExecutable(p_type) , % VBVMR.DLL_PATH, UseErrorLevel Hide - }else{ - loop 3 { - Run, % VBVMR.DLL_PATH "\" this.__getTypeExecutable(4-A_Index) , % VBVMR.DLL_PATH, UseErrorLevel Hide - if(!ErrorLevel) - return - } + 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") } - if(ErrorLevel) - Throw, Exception("Could not run Voicemeeter") - } - - updateDevices(){ - VMR.BusStrip.BusDevices:= Array() - VMR.BusStrip.StripDevices:= Array() - loop % VBVMR.Output_GetDeviceNumber() - VMR.BusStrip.BusDevices.Push(VBVMR.Output_GetDeviceDesc(A_Index-1)) - loop % VBVMR.Input_GetDeviceNumber() - VMR.BusStrip.StripDevices.Push(VBVMR.Input_GetDeviceDesc(A_Index-1)) - } - - getBusDevices(){ - return VMR.BusStrip.BusDevices - } - - getStripDevices(){ - return VMR.BusStrip.StripDevices - } - - exec(script){ - Try errLn:= VBVMR.SetParameters(script) - if(errLn != 0) - Throw, Exception("exec:`nScript error at line " . errLn) - return errLn - } - - __getDLLPath(){ - vmkey := "VB:Voicemeeter {17359A74-1236-5467}" - key := "HKLM\Software{}\Microsoft\Windows\CurrentVersion\Uninstall\{}" - Try RegRead, value, % Format(key, A_Is64bitOS?"\WOW6432Node":"", vmkey), UninstallString - catch { - Throw, Exception("Voicemeeter is not installed") - } - SplitPath, value,, dir + } + /** + * @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 } - - __init_obj(){ - this.option:= new this.OptionBase - this.vban.init() - this.vban.stream.initiated:=1 - if(this.getType() >= 2){ - this.patch:= new this.PatchBase - this.recorder:= new this.RecorderBase - } - if(this.getType() >= 3) - this.fx := new this.FXBase - } - - __init_arrays(){ - this.bus:= Array() - this.strip:= Array() - loop % VBVMR.BUSCOUNT { - this.bus.Push(new this.BusStrip("Bus")) - } - loop % VBVMR.STRIPCOUNT { - this.strip.Push(new this.BusStrip("Strip")) - } - this.updateDevices() - VMR.BusStrip.initiated:=1 - } - - __getTypeExecutable(p_type){ - switch (p_type) { - case 1: return "voicemeeter.exe" - case 2: return "voicemeeterpro.exe" - case 3: return Format("voicemeeter8{}.exe", A_Is64bitOS? "x64":"") - } - } - - __syncWithDLL(){ - static ignore_msg:=0 + /** + * 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 { - ;sync vmr parameters - isParametersDirty:= VBVMR.IsParametersDirty() - - ;sync macro buttons states - isMacroButtonsDirty:= VBVMR.MacroButton_IsDirty() - - ;sync bus/strip level arrays - loop % VBVMR.BUSCOUNT { - this.bus[A_Index].__updateLevel() + if (p_listener.MaxParams = 0) { + return p_listener.Call() } - loop % VBVMR.STRIPCOUNT { - this.strip[A_Index].__updateLevel() - } - - ;sync successful - ignore_msg:=0 - - ;level callback - if(this.onUpdateLevels){ - this.onUpdateLevels.Call() - } - - ;parameter callback - if(isParametersDirty && this.onUpdateParameters){ - this.onUpdateParameters.Call() - } - - ;macrobutton callback - if(isMacroButtonsDirty && this.onUpdateMacrobuttons){ - this.onUpdateMacrobuttons.Call() - } - - ;midi callback - if(this.onMidiMessage && midiMessages:= VBVMR.GetMidiMessage()){ - this.onMidiMessage.Call(midiMessages) - } - return isParametersDirty || isMacroButtonsDirty || midiMessages - } catch e { - if(!ignore_msg){ - MsgBox, 52, VMR.ahk, % Format("An error occurred during synchronization: {}`nAttempt to restart VoiceMeeter?", e.Message), 10 - IfMsgBox Yes - this.runVoicemeeter(VBVMR.VM_TYPE) - IfMsgBox, No - ignore_msg:=1 - IfMsgBox, Timeout - ignore_msg:=1 - sleep, 1000 + else if (p_listener.MinParams < 2) { + return p_listener.Call(this._value) } } } - - __onExit(){ - while(this.__syncWithDLL()){ +} +/** + * 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]) } - Sleep, 100 ; to make sure all commands are executed before exiting - VBVMR.Logout() - DllCall("FreeLibrary", "Ptr", VBVMR.DLL) - } - - class BusStrip { - static BUS_COUNT:=0 - , BUS_LEVEL_COUNT:=0 - , BusDevices:=Array() - , STRIP_COUNT:=0 - , STRIP_LEVEL_COUNT:=0 - , StripDevices:=Array() - , BUS_STRIP_NAMES:= - ( Join LTrim ; ahk - { - 1: { - "Bus": [ - "A", - "B" - ], - "Strip": [ - "Input #1", - "Input #2", - "Virtual Input #1" - ] - }, - 2: { - "Bus": [ - "A1", - "A2", - "A3", - "B1", - "B2" - ], - "Strip": [ - "Input #1", - "Input #2", - "Input #3", - "Virtual Input #1", - "Virtual Input #2" - ] - }, - 3: { - "Bus": [ - "A1", - "A2", - "A3", - "A4", - "A5", - "B1", - "B2", - "B3" - ], - "Strip": [ - "Input #1", - "Input #2", - "Input #3", - "Input #4", - "Input #5", - "Virtual Input #1", - "Virtual Input #2", - "Virtual Input #3" - ] - } - } - ) - , initiated:=0 - - __Set(p_name, p_value, p_sec_value:=""){ - if(VMR.BusStrip.initiated && this.BUS_STRIP_ID){ - switch p_name { - case "gain": - return Format("{:.1f}",this.setParameter(p_name, max(-60.0, min(p_value, this.gain_limit)))) - case "limit": - return Format("{:.1f}",this.setParameter(p_name, max(-40.0, min(p_value, 12.0)))) - case "device": - if(IsObject(p_value)) - return this.__setDevice(p_value) - if(!p_value) - return this.__setDevice({name:"",driver:"wdm"}) - driver:= p_sec_value? p_value : "wdm" - name:= p_sec_value? p_sec_value : p_value - return this.__setDevice(this.__getDeviceObj(name,driver)) - case "mute": - if(p_value = -1) - p_value:= !this.mute - } - return this.setParameter(p_name,p_value) + 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) } - - __Get(p_name){ - if(VMR.BusStrip.initiated && this.BUS_STRIP_ID){ - switch p_name { - case "gain","limit": - return Format("{:.1f}",this.getParameter(p_name)) - case "device": - return this.getParameter("device.name") - } - return this.getParameter(p_name) + } + /** + * 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 } } - - __New(p_type){ - this.BUS_STRIP_TYPE := p_type - this.level := Array() - this.LEVEL_INDEX := Array() - this.gain_limit:= 12.0 - if (p_type="Strip") { - this.BUS_STRIP_INDEX := VMR.BusStrip.STRIP_COUNT++ - loop % this.isPhysical() ? 2 : 8 - this.LEVEL_INDEX.Push(VMR.BusStrip.STRIP_LEVEL_COUNT++) - }else{ - this.BUS_STRIP_INDEX := VMR.BusStrip.BUS_COUNT++ - loop 8 - this.LEVEL_INDEX.Push(VMR.BusStrip.BUS_LEVEL_COUNT++) + 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 } - this.BUS_STRIP_ID := this.BUS_STRIP_TYPE . "[" . this.BUS_STRIP_INDEX . "]" - this.name := VMR.BusStrip.BUS_STRIP_NAMES[VBVMR.VM_TYPE][this.BUS_STRIP_TYPE][this.BUS_STRIP_INDEX+1] - } - - getGainPercentage(){ - return Format("{:.2f}",this.getPercentage(this.gain)) - } - - setGainPercentage(percentage){ - return this.gain := this.getdB(percentage) - } - - getPercentage(dB){ - min_s := 10**(-60/20), max_s := 10**(0/20) - return ((10**(dB/20))-min_s)/(max_s-min_s)*100 - } - - getdB(percentage){ - min_s := 10**(-60/20), max_s := 10**(0/20) - return 20*Log(min_s + percentage/100*(max_s-min_s)) - } - - setParameter(parameter, value){ - local func - if parameter contains device,FadeTo,Label,FadeBy,AppGain,AppMute - func:= "setParameterString" + } + 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 - func:= "setParameterFloat" - return (VBVMR)[func](this.BUS_STRIP_ID, parameter, value) - } - - getParameter(parameter){ - local func - if parameter contains device,FadeTo,Label,FadeBy,AppGain,AppMute - func:= "getParameterString" + 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 - func:= "getParameterFloat" - return (VBVMR)[func](this.BUS_STRIP_ID, parameter) + this.SetParameter("AppMute", "(`"" p_app "`", " Value ")") } - - ; Returns 1 if the bus/strip is a physical one (Hardware bus/strip), 0 otherwise - isPhysical(){ - Switch VBVMR.VM_TYPE { - case 1: - if(this.BUS_STRIP_TYPE = "Strip") - return this.BUS_STRIP_INDEX < 2 - else - return 1 - case 2: - return this.BUS_STRIP_INDEX < 3 - case 3: - return this.BUS_STRIP_INDEX < 5 - } + } + /** + * 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 } - - __setDevice(device){ - if (!this.isPhysical()) - return -4 - if device.driver not in wdm,mme,ks,asio - return -5 - return this.setParameter("device." . device.driver,device.name) - } - - __getDeviceObj(substring,driver){ - local devices:= VMR.BusStrip[this.BUS_STRIP_TYPE . "Devices"] - for i in devices - if (devices[i].driver = driver && InStr(devices[i].name, substring)) - return devices[i] - return {name:"",driver:"wdm"} - } - - __updateLevel(){ - local type := this.BUS_STRIP_TYPE="Bus" ? 3 : 1 - loop % this.LEVEL_INDEX.Length() { - level := VBVMR.GetLevel(type, this.LEVEL_INDEX[A_Index]) - this.level[A_Index] := Max(Round(20 * Log(level)), -999) - } + ; 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) } } - - - class FXBase { - - reverb(onOff := -2) { - switch (onOff) { - case -2: ;getParam - return VBVMR.GetParameterFloat("Fx.Reverb", "on") - case -1: ;invert state - onOff := !VBVMR.GetParameterFloat("Fx.Reverb", "on") - } - return VBVMR.SetParameterFloat("Fx.Reverb","on", onOff) - } - - delay(onOff := -2) { - switch (onOff) { - case -2: ;getParam - return VBVMR.GetParameterFloat("Fx.delay", "on") - case -1: ;invert state - onOff := !VBVMR.GetParameterFloat("Fx.delay", "on") - } - return VBVMR.SetParameterFloat("Fx.delay","on", onOff) - } - } - - - ; These are read-only commands - class Command { - - restart(){ - return VBVMR.SetParameterFloat("Command","Restart",1) - } - - shutdown(){ - return VBVMR.SetParameterFloat("Command","Shutdown",1) - } - - show(open := 1){ - return VBVMR.SetParameterFloat("Command","Show",open) - } - - lock(state := 1){ - return VBVMR.SetParameterFloat("Command","Lock",state) - } - - eject(){ - return VBVMR.SetParameterFloat("Command","Eject",1) - } - - reset(){ - return VBVMR.SetParameterFloat("Command","Reset",1) - } - - save(filePath){ - return VBVMR.SetParameterString("Command","Save",filePath) - } - - load(filePath){ - return VBVMR.SetParameterString("Command","Load",filePath) - } - - showVBANChat(show := 1) { - return VBVMR.SetParameterFloat("Command","dialogshow.VBANCHAT",show) - } - - state(buttonNum, newState) { - return VBVMR.SetParameterFloat("Command.Button[" . buttonNum . "]", "State", newState) - } - - stateOnly(buttonNum, newState) { - return VBVMR.SetParameterFloat("Command.Button[" . buttonNum . "]", "stateOnly", newState) - } - - trigger(buttonNum, newState) { - return VBVMR.SetParameterFloat("Command.Button[" . buttonNum . "]", "trigger", newState) - } - - saveBusEQ(busIndex, filePath) { - return VBVMR.SetParameterFloat("Command","SaveBUSEQ[" busIndex "]", filePath) - } - - loadBusEQ(busIndex, filePath) { - return VBVMR.SetParameterFloat("Command","LoadBUSEQ[" busIndex "]", filePath) - } - } - - - class VBAN { - static instream:="" - , outstream:="" - - enable{ - set{ - return VBVMR.SetParameterFloat("vban", "Enable", value) - } - get{ - return VBVMR.GetParameterFloat("vban", "Enable") - } + /** + * 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 } - - init(){ - VMR.VBAN.instream:= Array() - VMR.VBAN.outstream:= Array() - loop % VBVMR.VBANINCOUNT - VMR.VBAN.instream.Push(new VMR.VBAN.Stream("in", A_Index)) - loop % VBVMR.VBANOUTCOUNT - VMR.VBAN.outstream.Push(new VMR.VBAN.Stream("out", A_Index)) - } - - class Stream{ - static initiated:= 0 - __New(p_type,p_index){ - this.PARAM_PREFIX:= Format("vban.{}stream[{}]", p_type, p_index) - } - __Set(p_name,p_value){ - if(VMR.VBAN.stream.initiated) { - if p_name contains name, ip - return VBVMR.SetParameterString(this.PARAM_PREFIX, p_name, p_value) - return VBVMR.SetParameterFloat(this.PARAM_PREFIX, p_name, p_value) - } - - } - __Get(p_name){ - if(VMR.VBAN.stream.initiated){ - if p_name contains name, ip - return VBVMR.GetParameterString(this.PARAM_PREFIX, p_name) - return VBVMR.GetParameterFloat(this.PARAM_PREFIX, p_name) - } - } - } - } - - - class MacroButton { - - run(){ - Run, % VBVMR.DLL_PATH "\VoicemeeterMacroButtons.exe", % VBVMR.DLL_PATH, UseErrorLevel - if(ErrorLevel) - Throw, Exception("Could not run MacroButtons") - } - - show(state := 1){ - if(state) - WinShow, ahk_exe VoicemeeterMacroButtons.exe - else - WinHide, ahk_exe VoicemeeterMacroButtons.exe - } - - setStatus(buttonIndex, newStatus, bitmode:=0) { - return VBVMR.MacroButton_SetStatus(buttonIndex, newStatus, bitmode) - } - - getStatus(buttonIndex, bitmode:=0){ - return VBVMR.MacroButton_GetStatus(buttonIndex, bitmode) - } - - } - - - - class PatchBase { - __Set(p_name, p_value){ - return VBVMR.SetParameterFloat("Patch", p_name, p_value) - } - - __Get(p_name){ - return VBVMR.GetParameterFloat("Patch", p_name) - } - } - - - class OptionBase { - __Set(p_name, p_value){ - return VBVMR.SetParameterFloat("Option", p_name, p_value) - } - - __Get(p_name){ - return VBVMR.GetParameterFloat("Option", p_name) - } - - delay(busNum, p_delay := "") { - ; in keeping with the 1 indexed class... - busNum := busNum - 1 - if(p_delay == "") { - ; get the value - return VBVMR.GetParameterFloat("Option", "delay[" . busNum . "]") + } + /** + * 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 } - else { - ; set it to a new value - return VBVMR.SetParameterFloat("Option", "delay[" . busNum . "]", Min(Max(p_delay,0),500)) + } + 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 }) } - - - class RecorderBase { - __Set(p_name,p_value){ - local type:= "Float" - if p_name contains load - type:= "String" - return (VBVMR)["SetParameter" type]("Recorder", p_name, p_value) - } - - __Get(p_name){ - local type:= "Float" - if p_name contains load - type:= "String" - return (VBVMR)["GetParameter" type]("Recorder", p_name) - } - - ArmBus(bus, set:=-1){ - if(set > -1){ - VBVMR.SetParameterFloat("Recorder","mode.recbus", 1) - VBVMR.SetParameterFloat("Recorder","ArmBus(" (bus-1) ")", set) - }else{ - return VBVMR.GetParameterFloat("Recorder","ArmBus(" (bus-1) ")") +} +/** + * 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 } } - - ArmStrips(strip*){ - loop { - Try - this.armStrip(A_Index,0) - Catch - Break + 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) } - for i in strip - Try this.armStrip(strip[i],1) - } - - ArmStrip(strip, set:=-1){ - if(set > -1){ - VBVMR.SetParameterFloat("Recorder","mode.recbus", 0) - VBVMR.SetParameterFloat("Recorder","ArmStrip(" . (strip-1) . ")", set) - }else{ - return VBVMR.GetParameterFloat("Recorder","ArmStrip(" (strip-1) ")") + 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 } } - -} - -class VBVMR { - static DLL - , DLL_PATH - , DLL_FILE - , VM_TYPE - , BUSCOUNT - , STRIPCOUNT - , STR_TYPE - , VBANINCOUNT - , VBANOUTCOUNT - , FUNC_ADDR:={ Login:0 - , Logout:0 - , SetParameterFloat:0 - , SetParameterStringW:0 - , SetParameterStringA:0 - , GetParameterFloat:0 - , GetParameterStringW:0 - , GetParameterStringA:0 - , GetVoicemeeterType:0 - , GetLevel:0 - , Output_GetDeviceNumber:0 - , Output_GetDeviceDescW:0 - , Output_GetDeviceDescA:0 - , Input_GetDeviceNumber:0 - , Input_GetDeviceDescW:0 - , Input_GetDeviceDescA:0 - , IsParametersDirty:0 - , MacroButton_IsDirty:0 - , MacroButton_GetStatus:0 - , MacroButton_SetStatus:0 - , GetMidiMessage:0 - , SetParameters:0 - , SetParametersW:0} - - Login(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.Login) - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Login returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - return errLevel - } - - Logout(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.Logout) - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Logout returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - return errLevel - } - - SetParameterFloat(p_prefix, p_parameter, p_value){ - errLevel := DllCall(VBVMR.FUNC_ADDR.SetParameterFloat, "AStr" , p_prefix . "." . p_parameter , "Float" , p_value, "Int") - if (errLevel<0) - Throw, Exception(Format("`nVBVMR_SetParameterFloat returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - return p_value + /** + * 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) } - - SetParameterString(p_prefix, p_parameter, p_value){ - errLevel := DllCall(VBVMR.FUNC_ADDR["SetParameterString" . VBVMR.STR_TYPE], "AStr", p_prefix . "." . p_parameter , VBVMR.STR_TYPE . "Str" , p_value , "Int") - if (errLevel<0) - Throw, Exception(Format("`nVBVMR_SetParameterString returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - return p_value + /** + * 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) } - - GetParameterFloat(p_prefix, p_parameter){ - local value - VarSetCapacity(value, 4) - errLevel := DllCall(VBVMR.FUNC_ADDR.GetParameterFloat, "AStr" , p_prefix . "." . p_parameter , "Ptr" , &value, "Int") - if (errLevel<0) - Throw, Exception(Format("`nVBVMR_GetParameterFloat returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - value := NumGet(&value, 0, "Float") + 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 } - - GetParameterString(p_prefix, p_parameter){ - local value - VarSetCapacity(value, A_IsUnicode? 1024 : 512) - errLevel := DllCall(VBVMR.FUNC_ADDR["GetParameterString" . VBVMR.STR_TYPE], "AStr" , p_prefix . "." . p_parameter , "Ptr" , &value , "Int") - if (errLevel<0) - Throw, Exception(Format("`nVBVMR_GetParameterString returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - value := StrGet(&value,512) - 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) } - - GetLevel(p_type, p_channel){ - local level - VarSetCapacity(level,4) - errLevel := DllCall(VBVMR.FUNC_ADDR.GetLevel, "Int", p_type, "Int", p_channel, "Ptr", &level) - if(errLevel<0){ - SetTimer,, Off - Throw, Exception(Format("`nVBVMR_GetLevel returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - } - level := NumGet(&level, 0, "Float") - return level - } - - GetVoicemeeterType(){ - local vtype - VarSetCapacity(vtype, 4) - errLevel := DllCall(VBVMR.FUNC_ADDR.GetVoicemeeterType, "Ptr", &vtype, "Int") - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_GetVoicemeeterType returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - vtype:= NumGet(&vtype, 0, "Int") - return vtype - } - - Output_GetDeviceNumber(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.Output_GetDeviceNumber,"Int") - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Output_GetDeviceNumber returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - else - return errLevel - } - - Output_GetDeviceDesc(p_index){ - local name, driver, device := {} - VarSetCapacity(name, A_IsUnicode? 1024 : 512) - VarSetCapacity(driver, 4) - errLevel := DllCall(VBVMR.FUNC_ADDR["Output_GetDeviceDesc" . VBVMR.STR_TYPE], "Int", p_index, "Ptr" , &driver , "Ptr", &name, "Ptr", 0, "Int") - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Output_GetDeviceDesc returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - driver := NumGet(&driver, 0, "UInt") - name := StrGet(&name,512) - device.name := name - device.driver := (driver=3 ? "wdm" : (driver=4 ? "ks" : (driver=5 ? "asio" : "mme"))) - return device - } - - Input_GetDeviceNumber(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.Input_GetDeviceNumber,"Int") - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Input_GetDeviceNumber returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - else - return errLevel - } - - Input_GetDeviceDesc(p_index){ - local name, driver, device := {} - VarSetCapacity(name, A_IsUnicode? 1024 : 512) - VarSetCapacity(driver, 4) - errLevel := DllCall(VBVMR.FUNC_ADDR["Input_GetDeviceDesc" . VBVMR.STR_TYPE], "Int", p_index, "Ptr" , &driver , "Ptr", &name, "Ptr", 0, "Int") - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Input_GetDeviceDesc returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - driver := NumGet(&driver, 0, "UInt") - name := StrGet(&name,512) - device.name := name - device.driver := (driver=3 ? "wdm" : (driver=4 ? "ks" : (driver=5 ? "asio" : "mme"))) - return device - } - - IsParametersDirty(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.IsParametersDirty) - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_IsParametersDirty returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - else - return errLevel - } - - MacroButton_GetStatus(nuLogicalButton, bitMode){ - local pValue - VarSetCapacity(pValue, 4) - errLevel := DllCall(VBVMR.FUNC_ADDR.MacroButton_GetStatus, "Int" , nuLogicalButton , "Ptr", &pValue, "Int", bitMode, "Int") - if (errLevel<0) - Throw, Exception("VBVMR_MacroButton_GetStatus returned " . errLevel . "`n DllCall returned " . ErrorLevel, -1) - pValue := NumGet(&pValue, 0, "Float") - return pValue - } - - MacroButton_SetStatus(nuLogicalButton, fValue, bitMode){ - errLevel := DllCall(VBVMR.FUNC_ADDR.MacroButton_SetStatus, "Int" , nuLogicalButton , "Float" , fValue, "Int", bitMode, "Int") - if (errLevel<0) - Throw, Exception("VBVMR_MacroButton_SetStatus returned " . errLevel, -1) - return fValue - } - - MacroButton_IsDirty(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.MacroButton_IsDirty) - if(errLevel<0) - Throw, Exception("VBVMR_MacroButton_IsParametersDirty returned " . errLevel, -1) - else - return errLevel - } - - GetMidiMessage(){ - local nBytes:= 1024, dBuffer:="", tempArr:= Array() - VarSetCapacity(dBuffer, nBytes) - errLevel := DllCall(VBVMR.FUNC_ADDR.GetMidiMessage, "Ptr", &dBuffer, "Int", nBytes) - if errLevel between -1 and -2 - Throw, Exception("VBVMR_GetMidiMessage returned " . errLevel, -1) - loop %errLevel% { - tempArr[A_Index]:= Format("0x{:X}",NumGet(&dBuffer, A_Index - 1, "UChar")) - } - return tempArr.Length()? tempArr : "" - } - - SetParameters(script){ - errLevel := DllCall(VBVMR.FUNC_ADDR["SetParameters" . VBVMR.STR_TYPE], VBVMR.STR_TYPE . "Str" , script , "Int") - if (errLevel<0) - Throw, Exception(Format("`nVBVMR_SetParameters returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - return errLevel - } - - __getAddresses(){ - for fName in VBVMR.FUNC_ADDR - (VBVMR.FUNC_ADDR)[fName]:= DllCall("GetProcAddress", "Ptr", VBVMR.DLL, "AStr", "VBVMR_" . fName, "Ptr") - } -} \ No newline at end of file + /** + * @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 + } + } + } +} diff --git a/docs/README.md b/docs/README.md index 8321584..92088fc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,41 @@ -## Getting started -To use `VMR.ahk` in your script, follow these steps: -1. Download the latest pre-built version from the [`dist` folder](https://raw.githubusercontent.com/SaifAqqad/VMR.ahk/master/dist/VMR.ahk) or follow the build instructions below - -2. Include it using `#Include VMR.ahk` or copy it to a [library folder](https://www.autohotkey.com/docs/Functions.htm#lib) and use `#Include ` - -3. create an instance of the VMR class and log in to the API: - ```autohotkey - voicemeeter := new VMR().login() - ``` - you can optionally pass the path to voicemeeter's folder: - ```autohotkey - voicemeeter := new VMR("C:\path\to\voicemeeter\").login() - ``` -4. The `VMR` object will have two arrays, `bus` and`strip`, as well as other objects, that will allow you to control voicemeeter in AHK. - ```autohotkey - voicemeeter.bus[1].mute := true - voicemeeter.strip[4].gain++ - ``` +## Quick start + +To use `VMR.ahk` in your script, follow one of the following methods: + +### ahkpm installation + +1. Install and set up [ahkpm](https://github.com/joshuacc/ahkpm) +2. Run `ahkpm install gh:SaifAqqad/VMR.ahk` +3. Include VMR in your script by running `ahkpm include gh:SaifAqqad/VMR.ahk -f myScript.ahk` + +### Manual Installation + +1. Download the latest pre-built version from the [`dist` folder](https://raw.githubusercontent.com/SaifAqqad/VMR.ahk/master/dist/VMR.ahk ':target=_blank') / [latest release](https://github.com/SaifAqqad/VMR.ahk/releases ':target=_blank') or follow the build instructions below +2. Include it using `#Include VMR.ahk` or copy it to a [library folder](https://www.autohotkey.com/docs/v2/Scripts.htm#lib ':target=_blank') and use `#Include ` + +!> The current version of VMR ***only*** supports AHK v2, the AHK v1 version is still available on the [v1 branch](https://github.com/SaifAqqad/VMR.ahk/tree/v1 ':target=_blank'), see [v1 docs](https://vmr-v1.vercel.app/ ':target=_blank'). + +## Basic usage +- Create an instance of the [`VMR`](./classes/vmr) class and log in to the API: + ```autohotkey + voicemeeter := VMR().Login() + ``` +- The [`VMR`](./classes/vmr) instance will have two arrays (`Bus` and `Strip`), as well as other properties/methods that will allow you to control voicemeeter in AHK + ```autohotkey + voicemeeter.Bus[1].mute := true + voicemeeter.Strip[4].gain++ + ``` ## Build instructions -To build `VMR.ahk` yourself, run the build script: + +To build `VMR.ahk`, either run the vscode task `Build VMR` or run the build script using ahkpm or manually: + ```powershell -.\Build.ahk -# Or if you installed autohotkey using scoop: -# autohotkey.exe .\Build.ahk +# ahkpm +ahkpm run build +# Manually +Autohotkey.exe ".\Build.ahk" ".\VMR.ahk" "..\dist\VMR.ahk" "" ``` -This documentation is for VMR.ahk, If you need help with the Voicemeeter API check out [their documentation](http://download.vb-audio.com/Download_CABLE/VoicemeeterRemoteAPI.pdf) +This documentation is for VMR.ahk, If you need help with the Voicemeeter API check out [their documentation](http://download.vb-audio.com/Download_CABLE/VoicemeeterRemoteAPI.pdf ':target=_blank') diff --git a/docs/VMR-Class/README.md b/docs/VMR-Class/README.md deleted file mode 100644 index f15ffff..0000000 --- a/docs/VMR-Class/README.md +++ /dev/null @@ -1,65 +0,0 @@ -## `VMR` Class - - -### `__New([p_path])` Constructor -Initilizes the VBVMR class (the actual wrapper) by setting the DLL path and type (64/32) as well as the string encoding which is based on the type of AHK that's running the script (Unicode/ANSI), then loads the correct DLL and its functions addresses. - -*Note: for VMR to work properly, the script needs to be persistent, scripts that have GUIs or hotkeys are implicitly persistent, to make a regular script persistent add `#Persistent` to the top of the script* - ---- - -### Properties - -* #### `bus` and `strip` Arrays -Array of [`bus`/`strip` objects](/VMR-Class/bus-strip-object.md 'Bus/Strip object'). -* #### [`recorder`](/VMR-Class/recorder-object.md 'Recorder object') -Use this object to control voicemeeter's recorder. -* #### [`vban`](/VMR-Class/vban-object.md 'VBAN object') -Use this object to control voicemeeter's VBAN interface -* #### [`command`](/VMR-Class/command-object.md 'Command object') -Use this object to access command methods. -* #### [`option`](/VMR-Class/option-object.md 'Option object') -Use this object to access/modify option parameters. -* #### [`macroButton`](/VMR-Class/macrobutton-object.md 'MacroButton object') -Use this object to access/modify macro buttons statuses. - ---- - -### Methods - -* #### `login()` -Calls voicemeeter's login function and initilizes VMR class properties (objects and arrays). -This method needs to be called first, in order to use the VMR class. -* #### `getType()` -Returns voicemeeter's type. (1 -> voicemeeter, 2 -> banana, 3 -> potato). -* #### `runVoicemeeter([type])` -Runs the highest version installed , or a specific version if `type` is passed. -* #### `updateDevices()` -Updates the internal array of input and output devices, that's used for setting bus/strips devices -* #### `exec(script)` -Executes a string of voicemeeter commands, see [`script_example.ahk`](https://github.com/SaifAqqad/VMR.ahk/blob/master/examples/script_example.ahk) -* #### `getBusDevices()`/`getStripDevices()` -Returns an array of input/output devices, each device is an object with `name` and `driver` properties - ---- - -### Callback functions -Set callback functions for certain events (e.g. to update a user interface) - -* #### `onUpdateLevels` -called whenever the [`level`](/VMR-Class/bus-strip-object?id=level-array) array for bus/strip objects is updated. -* #### `onUpdateParameters` -called whenever voicemeeter's parameters change on the UI or by another app. -* #### `onUpdateMacrobuttons` -called whenever a macrobutton's state is changed. -* #### `onMidiMessage` -called whenever voicemeeter receives a MIDI message - -```autohotkey - ; Set a function object - voicemeeter.onUpdateLevels := Func("syncLevels") -``` - -- See [ui_example.ahk](https://github.com/SaifAqqad/VMR.ahk/blob/master/examples/ui_example.ahk) -- See [midi_message_example.ahk](https://github.com/SaifAqqad/VMR.ahk/blob/master/examples/midi_message_example.ahk) -- [More info on function objects (AHK docs)](https://www.autohotkey.com/docs/objects/Func.htm) diff --git a/docs/VMR-Class/bus-strip-object.md b/docs/VMR-Class/bus-strip-object.md deleted file mode 100644 index ed6d894..0000000 --- a/docs/VMR-Class/bus-strip-object.md +++ /dev/null @@ -1,91 +0,0 @@ -## `bus`/`strip` - -Use this object to access or modify bus/strip parameters. - -### Properties - -* #### `gain_limit` - The maximum gain (in dB) that can be applied to a bus/strip, Increamenting the gain above this value will reapply the limit -* #### `level` array - Contains the current level (in dB) for every channel a bus/strip has, physical (hardware) strips have 2 channels (left, right), Buses and virtual strips have 8 channels -* #### `name` - The name of the bus/strip (e.g. `A1`, `B1`, `Virtual Input #1`) - ---- -### Methods - -* #### `setParameter(parameter, value)` - Sets the value of a bus/strip's parameter -* #### `getParameter(parameter)` - Returns the value of a bus/strip's parameter -* #### `getGainPercentage()` - Returns the bus/strip's gain as a scalar percentage -* #### `setGainPercentage(percentage)` - Sets the bus/strip's gain using a scalar percentage -* #### `getPercentage(dB)` - Converts a dB value to a scalar percentage -* #### `getdB(percentage)` - Converts a scalar percentage to a dB value -* #### `isPhysical()` - Returns `1` if the bus/strip is a physical one (Hardware bus/strip), otherwise return `0` - ---- - -### Parameters -for an up-to-date list of all `bus`/`strip` parameters, check out [VBVMR docs](http://download.vb-audio.com/Download_CABLE/VoicemeeterRemoteAPI.pdf#page=11) - ---- - -### Examples -#### Set any parameter - -```autohotkey - voicemeeter.bus[1].gain := 10.5 - voicemeeter.strip[3].FadeTo := "(-10.0, 3000)" - ; or you can use setParameter() - voicemeeter.bus[1].setParameter("gain", 10.5) -``` - -#### Retrieve any parameter -```autohotkey - current_gain := voicemeeter.bus[3].gain - is_assigned := voicemeeter.strip[1].B2 - is_muted := voicemeeter.bus[1].mute -``` - -#### Set/Retrieve the device -```autohotkey - ; bus[1].device[driver] := device_name - ; driver can be ["wdm","mme","asio","ks] - ; device_name can be any substring of the full name - voicemeeter.strip[2].device["ks"] := "Microphone (2)" - - ; Retrieve the device's name - device_name := voicemeeter.bus[2].device -``` - -#### Increment/decrement gain parameter -```autohotkey - voicemeeter.bus[3].gain-- - db := ++voicemeeter.bus[1].gain - ; db contains the incremented value -``` - -#### Toggle mute parameter -```autohotkey - voicemeeter.bus[1].mute := 1 - - voicemeeter.bus[1].mute := -1 - is_muted := voicemeeter.bus[1].mute ; 0 - ; or - voicemeeter.bus[1].mute-- - is_muted := voicemeeter.bus[1].mute ; 1 -``` - -#### Retrieve the current peak level of a bus/strip - - -```autohotkey - peak_level := Max(voicemeeter.bus[1].level*) -``` -See [level_stabilizer_example.ahk](https://github.com/SaifAqqad/VMR.ahk/blob/master/examples/level_stabilizer_example.ahk) diff --git a/docs/VMR-Class/command-object.md b/docs/VMR-Class/command-object.md deleted file mode 100644 index 6386b19..0000000 --- a/docs/VMR-Class/command-object.md +++ /dev/null @@ -1,65 +0,0 @@ -## `command` - -Use this object to access command methods. - -### Methods - -* #### `restart()` -Restarts Voicemeeter's audio engine -* #### `shutdown()` -Closes Voicemeeter completely -* #### `show([state])` -Shows/Hides Voicemeeter's window -```autohotkey - voicemeeter.command.show(false) ; hides the window -``` -* #### `lock([state])` -Locks/Unlocks Voicemeeter's window -```autohotkey - voicemeeter.command.lock(true) ; locks the window -``` -* #### `eject()` -Ejects the recorder's cassette (releases the audio file) -* #### `reset()` -Resets All configuration -* #### `save([filePath])` -Saves Voicemeeter's configuration to a file -```autohotkey - voicemeeter.command.save("VMconfig.xml") ; saves the file in the user's documents folder -``` -* #### `load([filePath])` -Loads Voicemeeter's configuration from a file -```autohotkey - voicemeeter.command.load("C:\config\voicemeeter.xml") -``` -* #### `showVBANChat([state])` -Shows/hides the VBAN-Chat Dialog -```autohotkey - voicemeeter.command.showVBANChat(true) -``` -* #### `state(buttonIndex, state)` -Changes the actual state of a macro button. `buttonIndex` is zero-based. -```autohotkey - voicemeeter.command.state(0,1) ; sets the state of the first macro button to 1 -``` -* #### `stateOnly(buttonIndex, state)` -Changes the visual state of a macro button. -```autohotkey - voicemeeter.command.stateOnly(2,0) - ; releases the key visually but does not run the code programmed into the macrobutton. -``` -* #### `trigger(buttonIndex, state)` -Changes a button's trigger state. -```autohotkey - voicemeeter.command.trigger(3,1) -``` -* #### `saveBusEQ(busIndex, filePath)` -Saves the bus EQ settings to a file. `busIndex` is zero-based. -```autohotkey - voicemeeter.command.saveBusEQ(0,"C:\config\bus0_eq.xml") -``` -* #### `loadBusEQ(busIndex, filePath)` -Loads the bus EQ settings from a file. -```autohotkey - voicemeeter.command.loadBusEQ(2,"C:\config\bus2_eq.xml") -``` \ No newline at end of file diff --git a/docs/VMR-Class/macrobutton-object.md b/docs/VMR-Class/macrobutton-object.md deleted file mode 100644 index f07f60c..0000000 --- a/docs/VMR-Class/macrobutton-object.md +++ /dev/null @@ -1,45 +0,0 @@ -## `macroButton` - -Use this object to access/modify macro buttons status. - -### Methods -* #### `show([state])` - Shows/Hides the MacroButtons app window - ```autohotkey - voicemeeter.macroButton.show(1) - - voicemeeter.macroButton.show(0) - ``` - -* #### `run()` - Runs the MacroButtons app - ```autohotkey - voicemeeter.macroButton.run() - ``` - -* #### `setStatus(buttonIndex, newStatus [, bitmode])` - Set the status of a macro button. - - `buttonIndex` is zero-based. - - `bitmode` defines what kind of value will be set, it's optional and `0` by default, possible values are: - - - `0` : Actual button's state - - `2` : Displayed (visual) State only - - `3` : Trigger state - ```autohotkey - ; set macrobutton 0 to on. - voicemeeter.macroButton.setStatus(0,1) - - ; sets macro button 2 to have trigger on - voicemeeter.macroButton.setStatus(2,1,3) - ``` - -* #### `getStatus(buttonIndex [, bitmode])` - Retrieve the status of a macro button - ```autohotkey - buttonStatus := voicemeeter.macroButton.getStatus(1) - ``` - - - See [VBVMR docs](http://download.vb-audio.com/Download_CABLE/VoicemeeterRemoteAPI.pdf#page=8) for more info \ No newline at end of file diff --git a/docs/VMR-Class/option-object.md b/docs/VMR-Class/option-object.md deleted file mode 100644 index d4e15d4..0000000 --- a/docs/VMR-Class/option-object.md +++ /dev/null @@ -1,30 +0,0 @@ -## `option` - -Use this object to access/modify option parameters. - -### Methods - -* #### `delay(busIndex, [delay])` -Set the bus output delay. If `delay` is not passed, it will return the current delay for that bus - ---- - -### Parameters -for an up-to-date list of all `option` parameters, check out [VBVMR docs](https://download.vb-audio.com/Download_CABLE/VoicemeeterRemoteAPI.pdf#page=14) - ---- - -### Examples -#### Set any parameter - -```autohotkey - voicemeeter.option.sr := 44.1 - voicemeeter.option["buffer.wdm"] := 1024 - voicemeeter.option.delay(2,200) -``` -#### Retrieve any parameter -```autohotkey - is_exclusif := voicemeeter.option["mode.exclusif"] - delay := voicemeeter.option.delay(1) -``` - diff --git a/docs/VMR-Class/recorder-object.md b/docs/VMR-Class/recorder-object.md deleted file mode 100644 index 1df3115..0000000 --- a/docs/VMR-Class/recorder-object.md +++ /dev/null @@ -1,51 +0,0 @@ -## `recorder` - -Use this object to control VoiceMeeter Banana/Potato's recorder. - -### Methods - -* #### `ArmBus(index, [onOff])` -If `onOff` is passed to the method, it switches the recording mode to 1 (bus) and arms/disarms the given bus, otherwise it returns the state of the given bus. - -* #### `ArmStrip(index, [onOff])` -If `onOff` is passed to the method, it switches the recording mode to 0 (strip) and arms/disarms the given strip, otherwise it returns the state of the given strip. - -* #### `ArmStrips(index*)` -Switches the recording mode to 0 (strip), arms the given strips, disarming the others - ---- - -### Parameters -for an up-to-date list of all `recorder` parameters, check out [VBVMR docs](https://download.vb-audio.com/Download_CABLE/VoicemeeterRemoteAPI.pdf#page=15) - ---- - -### Examples -#### Set any parameter - -```autohotkey - voicemeeter.recorder.record := true - voicemeeter.recorder.goto := 90 ; go to 00:01:30 - - ; if no path is specified, the file is assumed to be in the Documents folder - voicemeeter.recorder.load := "C:\audio\audioFile.mp3" - - ; use bracket syntax for parameters with '.' - voicemeeter.recorder["mode.PlayOnLoad"] := true -``` - -#### Retrieve any parameter -```autohotkey - recorder_gain := voicemeeter.recorder.gain -``` - -#### Arm/Disarm buses and strips -```autohotkey - voicemeeter.recorder.ArmBus(3,true) - isArmed := voicemeeter.recorder.ArmBus(2) - voicemeeter.recorder.ArmStrip(1,true) - - voicemeeter.recorder.ArmStrip(2,true) ; strip[2] is armed - voicemeeter.recorder.ArmStrips(1,3,5) ; strip[2] is disarmed and strip[1,3,5] are all armed - -``` diff --git a/docs/VMR-Class/vban-object.md b/docs/VMR-Class/vban-object.md deleted file mode 100644 index f3276a2..0000000 --- a/docs/VMR-Class/vban-object.md +++ /dev/null @@ -1,28 +0,0 @@ -## `vban` - -Use this object to control VoiceMeeter’s VBAN interface. - -### Parameters -for an up-to-date list of all `vban` parameters, check out [VBVMR docs](https://download.vb-audio.com/Download_CABLE/VoicemeeterRemoteAPI.pdf#page=16) - ---- -### Examples -#### Set any parameter - -```autohotkey - voicemeeter.vban.enable := true - - voicemeeter.vban.instream[1].ip := "192.168.0.122" - voicemeeter.vban.instream[1].port := "5959" - voicemeeter.vban.instream[1].on := true - - voicemeeter.vban.outstream[3].name := "defStream" - voicemeeter.vban.outstream[3].quality := 4 - voicemeeter.vban.outstream[3].bit := 2 -``` - -#### Retrieve any parameter -```autohotkey - stream_channels := voicemeeter.vban.instream[2].channel -``` - diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 01fdf8a..901d976 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,8 +1,22 @@ -* [Getting started](/ 'VMR.ahk Docs') -* [VMR Class](/VMR-Class/ 'VMR Class') - - [Bus/Strip](/VMR-Class/bus-strip-object.md 'Bus/Strip object') - - [Recorder](/VMR-Class/recorder-object.md 'Recorder object') - - [VBAN](/VMR-Class/vban-object.md 'VBAN object') - - [Option](/VMR-Class/option-object.md 'Option object') - - [command](/VMR-Class/command-object.md 'Command object') - - [MacroButton](/VMR-Class/macrobutton-object.md 'MacroButton object') +* [Home](/ 'VMR.ahk Docs') + + +* Classes + - [`VMR`](/classes/vmr.md 'VMR Class') + - [`VMRBus`](/classes/vmrbus.md 'VMRBus Class') + - [`VMRStrip`](/classes/vmrstrip.md 'VMRStrip Class') + - [`VMRAsyncOp`](/classes/vmrasyncop.md 'VMRAsyncOp Class') + - [`VMRAudioIO`](/classes/vmraudioio.md 'VMRAudioIO Class') + - [`VMRDevice`](/classes/vmrdevice.md 'VMRDevice Class') + - [`VMRRecorder`](/classes/vmrrecorder.md 'VMRRecorder Class') + - [`VMRCommands`](/classes/vmrcommands.md 'VMRCommands Class') + - [`VMRMacroButton`](/classes/vmrmacrobutton.md 'VMRMacroButton Class') + - [`VMRVBAN`](/classes/vmrvban.md 'VMRVBAN Class') + - [`VMRControllerBase`](/classes/vmrcontrollerbase.md 'VMRControllerBase Class') + - [`VMRError`](/classes/vmrerror.md 'VMRError Class') + - [`VMRUtils`](/classes/vmrutils.md 'VMRUtils Class') + - [`VBVMR`](/classes/vbvmr.md 'VBVMR Class') \ No newline at end of file diff --git a/docs/classes/vbvmr.md b/docs/classes/vbvmr.md new file mode 100644 index 0000000..0586b68 --- /dev/null +++ b/docs/classes/vbvmr.md @@ -0,0 +1,300 @@ +# `VBVMR` + + A static wrapper class for the Voicemeeter Remote DLL. + + ?> The class must be initialized by calling [`Init()`](/classes/vbvmr?id=static-init) before using any of its static methods. + +## Properties +* #### **Static** `DLL` : `Ptr` :id=static-dll + The handle to the loaded Voicemeeter DLL +* #### **Static** `DLL_PATH` : `String` :id=static-dll-path + The path to the loaded Voicemeeter DLL + + +## Methods +* ### `Init(p_path := "")` :id=static-init + 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. + + **Parameters**: + - **Optional** `p_path` : `String` - The path to the Voicemeeter Remote DLL. If not specified, it will be looked up in the registry. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If the DLL is not found in the specified path or if voicemeeter is not installed. + + + +______ +* ### `Login()` :id=static-login + Opens a Communication Pipe With Voicemeeter. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `Number` - `0` : OK (no error). + - `1` : OK but Voicemeeter is not launched (need to launch it manually). + + +______ +* ### `Logout()` :id=static-logout + Closes the Communication Pipe With Voicemeeter. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `Number` - `0` : OK (no error). + + +______ +* ### `SetParameterFloat(p_prefix, p_parameter, p_value)` :id=static-setparameterfloat + Sets the value of a float (numeric) parameter. + + **Parameters**: + - `p_prefix` : `String` - The prefix of the parameter, usually the name of the bus/strip (ex: `Bus[0]`). + + - `p_parameter` : `String` - The name of the parameter (ex: `gain`). + + - `p_value` : `Number` - The value to set. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If the parameter is not found, or an internal error occurs. + + **Returns**: `Number` - `0` : OK (no error). + + +______ +* ### `SetParameterString(p_prefix, p_parameter, p_value)` :id=static-setparameterstring + Sets the value of a string parameter. + + **Parameters**: + - `p_prefix` : `String` - The prefix of the parameter, usually the name of the bus/strip (ex: `Strip[1]`). + + - `p_parameter` : `String` - The name of the parameter (ex: `name`). + + - `p_value` : `String` - The value to set. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If the parameter is not found, or an internal error occurs. + + **Returns**: `Number` - `0` : OK (no error). + + +______ +* ### `GetParameterFloat(p_prefix, p_parameter)` :id=static-getparameterfloat + Returns the value of a float (numeric) parameter. + + **Parameters**: + - `p_prefix` : `String` - The prefix of the parameter, usually the name of the bus/strip (ex: `Bus[2]`). + + - `p_parameter` : `String` - The name of the parameter (ex: `gain`). + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If the parameter is not found, or an internal error occurs. + + **Returns**: `Number` - The value of the parameter. + + +______ +* ### `GetParameterString(p_prefix, p_parameter)` :id=static-getparameterstring + Returns the value of a string parameter. + + **Parameters**: + - `p_prefix` : `String` - The prefix of the parameter, usually the name of the bus/strip (ex: `Strip[1]`). + + - `p_parameter` : `String` - The name of the parameter (ex: `name`). + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If the parameter is not found, or an internal error occurs. + + **Returns**: `String` - The value of the parameter. + + +______ +* ### `GetLevel(p_type, p_channel)` :id=static-getlevel + Returns the level of a single bus/strip channel. + + **Parameters**: + - `p_type` : `Number` - The type of the returned level + - `0`: pre-fader + - `1`: post-fader + - `2`: post-mute + - `3`: output-levels + + - `p_channel` : `Number` - 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. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If the channel index is invalid, or an internal error occurs. + + **Returns**: `Number` - The level of the requested channel. + + +______ +* ### `GetVoicemeeterType()` :id=static-getvoicemeetertype + Returns the type of Voicemeeter running. + + + **See also**: + [`VMR.Types`](/classes/vmr?id=static-types) for possible values. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `Number` - The type of Voicemeeter running. + + +______ +* ### `GetVoicemeeterVersion()` :id=static-getvoicemeeterversion + Returns the version of Voicemeeter running. + - The version is returned as a 4-part string (v1.v2.v3.v4) + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `String` - The version of Voicemeeter running. + + +______ +* ### `Output_GetDeviceNumber()` :id=static-output_getdevicenumber + Returns the number of Output Devices available on the system. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `Number` - The number of output devices. + + +______ +* ### `Output_GetDeviceDesc(p_index)` :id=static-output_getdevicedesc + Returns the Descriptor of an output device. + + **Parameters**: + - `p_index` : `Number` - The index of the device (zero-based). + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: [`VMRDevice`](/classes/vmrdevice) - An object containing the `Name`, `Driver` and `Hwid` of the device. + + +______ +* ### `Input_GetDeviceNumber()` :id=static-input_getdevicenumber + Returns the number of Input Devices available on the system. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `Number` - The number of input devices. + + +______ +* ### `Input_GetDeviceDesc(p_index)` :id=static-input_getdevicedesc + Returns the Descriptor of an input device. + + **Parameters**: + - `p_index` : `Number` - The index of the device (zero-based). + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: [`VMRDevice`](/classes/vmrdevice) - An object containing the `Name`, `Driver` and `Hwid` of the device. + + +______ +* ### `IsParametersDirty()` :id=static-isparametersdirty + Checks if any parameters have changed. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `Number` - `0` : No change + - `1` : Some parameters have changed + + +______ +* ### `MacroButton_GetStatus(p_logicalButton, p_bitMode)` :id=static-macrobutton_getstatus + Returns the current status of a given button. + + **Parameters**: + - `p_logicalButton` : `Number` - The index of the button (zero-based). + + - `p_bitMode` : `Number` - The type of the returned value. + - `0`: button-state + - `2`: displayed-state + - `3`: trigger-state + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `Number` - The status of the button + - `0`: Off + - `1`: On + + +______ +* ### `MacroButton_SetStatus(p_logicalButton, p_value, p_bitMode)` :id=static-macrobutton_setstatus + Sets the status of a given button. + + **Parameters**: + - `p_logicalButton` : `Number` - The index of the button (zero-based). + + - `p_value` : `Number` - The value to set. + - `0`: Off + - `1`: On + + - `p_bitMode` : `Number` - The type of the returned value. + - `0`: button-state + - `2`: displayed-state + - `3`: trigger-state + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `Number` - The status of the button + - `0`: Off + - `1`: On + + +______ +* ### `MacroButton_IsDirty()` :id=static-macrobutton_isdirty + Checks if any Macro Buttons states have changed. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `Number` - `0` : No change + - `> 0` : Some buttons have changed + + +______ +* ### `GetMidiMessage()` :id=static-getmidimessage + Returns any available MIDI messages from Voicemeeter's MIDI mapping. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **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. + + +______ +* ### `SetParameters(p_script)` :id=static-setparameters + Sets one or more parameters using a voicemeeter script. + + **Parameters**: + - `p_script` : `String` - 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. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: `Number` - `0` : OK (no error) + - `> 0` : Number of the line causing an error + +## Notes +While this class can be used to directly call the Voicemeeter Remote DLL functions, it's recommended to use the `VMR` class instead of this one, as it simplifies usage of the API. \ No newline at end of file diff --git a/docs/classes/vmr.md b/docs/classes/vmr.md new file mode 100644 index 0000000..114e55b --- /dev/null +++ b/docs/classes/vmr.md @@ -0,0 +1,142 @@ +# `VMR` + + A wrapper class for Voicemeeter Remote that hides the low-level API to simplify usage. + Must be initialized by calling [`Login()`](/classes/vmr?id=login) after creating the VMR instance. +## Constructor `__New(p_path := "")` :id=constructor + Creates a new VMR instance and initializes the [`VBVMR`](/classes/vbvmr) class. + + **Parameters**: + - **Optional** `p_path` : `String` - The path to the Voicemeeter Remote DLL. If not specified, VBVMR will attempt to find it in the registry. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If the DLL is not found in the specified path or if voicemeeter is not installed. + + +## Properties +* #### `Type` : `VMR.Types` :id=type + The type of Voicemeeter that is currently running. + + **See also**: + [`VMR.Types`](/classes/vmr?id=static-types) for a list of available types. +* #### `Version` : `String` :id=version + The version of Voicemeeter that is currently running. + The AHK function [`VerCompare`](https://www.autohotkey.com/docs/v2/lib/VerCompare.htm) can be used to compare version strings. +* #### `Bus` : `Array` :id=bus + An array of voicemeeter buses +* #### `Strip` : `Array` :id=strip + An array of voicemeeter strips +* #### `Command` : [`VMRCommands`](/classes/vmrcommands) :id=command + Commands that control various aspects of Voicemeeter + + **See also**: + [`VMRCommands`](/classes/vmrcommands) for a list of available commands. +* #### `Fx` : [`VMRControllerBase`](/classes/vmrcontrollerbase) :id=fx + Controls Voicemeeter Potato's FX settings + + ?> This property is only available when running Voicemeeter Potato (`VMR.Type == VMR.Types.Potato`). +* #### `Patch` : [`VMRControllerBase`](/classes/vmrcontrollerbase) :id=patch + Controls Voicemeeter's Patch parameters +* #### `Option` : [`VMRControllerBase`](/classes/vmrcontrollerbase) :id=option + Controls Voicemeeter's System Settings +* #### `MacroButton` : [`VMRMacroButton`](/classes/vmrmacrobutton) :id=macrobutton + Controls Voicemeeter's Macro Buttons app +* #### `Recorder` : [`VMRRecorder`](/classes/vmrrecorder) :id=recorder + Controls Voicemeeter's Recorder + ?> This property is only available when running Voicemeeter Banana or Potato (`VMR.Type.Id == 2 || VMR.Type.Id == 3`). + +## Methods +* ### `Login(p_launchVoicemeeter := true)` :id=login + Initializes the VMR instance and opens the communication pipe with Voicemeeter. + + **Parameters**: + - **Optional** `p_launchVoicemeeter` : `Boolean` - Whether to launch Voicemeeter if it's not already running. Defaults to `true`. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + + **Returns**: [`VMR`](/classes/vmr) - The `VMR` instance. + + +______ +* ### `RunVoicemeeter(p_type := unset)` :id=runvoicemeeter + 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. + + **Parameters**: + - **Optional** `p_type` : `Number` - The type of Voicemeeter to run. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If the specified Voicemeeter type is invalid, or if no Voicemeeter type could be launched. + + **Returns**: `Number` - The PID of the launched Voicemeeter process. + + +______ +* ### `On(p_event, p_listener)` :id=on + Registers a callback function to be called when the specified event is fired. + + + **See also**: + `VMRConsts.Events` for a list of available events. + + **Parameters**: + - `p_event` : `String` - The name of the event to listen for. + + - `p_listener` : `Func` - The function to call when the event is fired. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If the specified event is invalid, or if the listener is not a valid `Func` object. + + + +______ +* ### `Off(p_event, p_listener := unset)` :id=off + Removes a callback function from the specified event. + + + **See also**: + `VMRConsts.Events` for a list of available events. + + **Parameters**: + - `p_event` : `String` - The name of the event. + + - **Optional** `p_listener` : `Func` - The function to remove, if omitted, all listeners for the specified event will be removed. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If the specified event is invalid, or if the listener is not a valid `Func` object. + + **Returns**: `Boolean` - Whether the listener was removed. + + +______ +* ### `Sync()` :id=sync + Synchronizes the VMR instance with Voicemeeter. + + **Returns**: `Boolean` - Whether voicemeeter state has changed since the last sync. + + +______ +* ### `Exec(p_script)` :id=exec + 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. + + **Parameters**: + - `p_script` : `String` - The script to execute. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an error occurs while executing the script. + + +______ +* ### `UpdateDevices(p_wParam := unset, *)` :id=updatedevices + Updates the list of strip/bus devices. + + **Parameters**: + - **Optional** `p_wParam` : `Number` - If passed, must be equal to `VMRConsts.WM_DEVICE_CHANGE_PARAM` to update the device arrays. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs. + diff --git a/docs/classes/vmrasyncop.md b/docs/classes/vmrasyncop.md new file mode 100644 index 0000000..1dfddd4 --- /dev/null +++ b/docs/classes/vmrasyncop.md @@ -0,0 +1,50 @@ +# `VMRAsyncOp` + + 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. +## Constructor `__New(p_supplier := unset, p_autoResolveTimeout := unset)` :id=constructor + Creates a new async operation. + + **Parameters**: + - **Optional** `p_supplier` : `() => Any` - Supplies the result of the async operation. + + - **Optional** `p_autoResolveTimeout` : `Number` - Automatically resolves the async operation after the specified number of milliseconds. + + +## Properties +* #### **Static** `Empty` : `VMRAsyncOp` :id=static-empty + Returns an empty async operation that's already been resolved. +* #### **Static** `DEFAULT_DELAY` : `Number` :id=static-default_delay + The default delay that's used when awaiting the async operation. +* #### `IsEmpty` : `Boolean` :id=isempty + Whether the operation is an empty operation returned by `VMRAsyncOp.Empty`. +* #### `Resolved` : `Boolean` :id=resolved + Whether the operation has already been resolved. + +## Methods +* ### `Then(p_listener, p_innerOpDelay := 0)` :id=then + Adds a listener to the async operation. + + **Parameters**: + - `p_listener` : `(Any) => Any` - A function that will be called when the async operation is resolved. + + - **Optional** `p_innerOpDelay` : `Number` - If passed, the returned async operation will be delayed by the specified number of milliseconds. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - if `p_listener` is not a function or has an invalid number of parameters. + + **Returns**: [`VMRAsyncOp`](/classes/vmrasyncop) - a new async operation that will be resolved when the current operation is resolved and the listener is called. + + +______ +* ### `Await(p_timeoutMs := 0)` :id=await + Waits for the async operation to be resolved. + + **Parameters**: + - **Optional** `p_timeoutMs` : `Number` - The maximum number of milliseconds to wait before throwing an error. + + **Returns**: `Any` - The result of the async operation. \ No newline at end of file diff --git a/docs/classes/vmraudioio.md b/docs/classes/vmraudioio.md new file mode 100644 index 0000000..b868050 --- /dev/null +++ b/docs/classes/vmraudioio.md @@ -0,0 +1,109 @@ +# `VMRAudioIO` + + A base class for [`VMRBus`](/classes/vmrbus) and [`VMRStrip`](/classes/vmrstrip) +## Constructor `__New(p_index, p_ioType)` :id=constructor + Creates a new `VMRAudioIO` object. + + **Parameters**: + - `p_index` : `Number` - The zero-based index of the bus/strip. + + - `p_ioType` : `String` - The type of the object. (`Bus` or `Strip`) + + +## Properties +* #### `GainPercentage` : `Number` :id=gainpercentage + Gets/Sets the gain as a percentage +* #### `Device` : [`VMRDevice`](/classes/vmrdevice) :id=device + Gets/Sets the object's current device +* #### `GainLimit` : `Number` :id=gainlimit + The object's upper gain limit +* #### `Level` : `Array` :id=level + An array of the object's channel levels +* #### `Id` : `String` :id=id + The object's identifier that's used when calling VMR's functions. + ex: `Bus[0]` or `Strip[3]` +* #### `Index` : `Number` :id=index + The object's one-based index +* #### `Type` : `String` :id=type + The object's type (`Bus` or `Strip`) + +## Methods +* ### `SetParameter(p_name, p_value)` :id=setparameter + Sets the value of a parameter. + + **Parameters**: + - `p_name` : `String` - The name of the parameter. + + - `p_value` : `Any` - The value of the parameter. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If invalid parameters are passed or if an internal error occurs. + + **Returns**: [`VMRAsyncOp`](/classes/vmrasyncop) - An async operation that resolves to `true` if the parameter was set successfully. + + +______ +* ### `GetParameter(p_name)` :id=getparameter + Returns the value of a parameter. + + **Parameters**: + - `p_name` : `String` - The name of the parameter. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If invalid parameters are passed or if an internal error occurs. + + **Returns**: `Any` - The value of the parameter. + + +______ +* ### `Increment(p_param, p_amount)` :id=increment + 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, [`GainLimit`](/classes/vmraudioio?id=gainlimit) cannot be applied here. + + **Parameters**: + - `p_param` : `String` - The name of the parameter, must be a numeric parameter. + + - `p_amount` : `Number` - The amount to increment the parameter by, can be set to a negative value to decrement instead. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If invalid parameters are passed or if an internal error occurs. + + **Returns**: [`VMRAsyncOp`](/classes/vmrasyncop) - An async operation that resolves with the incremented value. + + +______ +* ### `FadeTo(p_db, p_duration)` :id=fadeto + Sets the gain to a specific value with a progressive fade. + + **Parameters**: + - `p_db` : `Number` - The gain value in dBs. + + - `p_duration` : `Number` - The duration of the fade in milliseconds. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If invalid parameters are passed or if an internal error occurs. + + **Returns**: [`VMRAsyncOp`](/classes/vmrasyncop) - An async operation that resolves with the final gain value. + + +______ +* ### `FadeBy(p_dbAmount, p_duration)` :id=fadeby + Fades the gain by a specific amount. + + **Parameters**: + - `p_dbAmount` : `Number` - The amount to fade the gain by in dBs. + + - `p_duration` : `Number` - The duration of the fade in milliseconds. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If invalid parameters are passed or if an internal error occurs. + + **Returns**: [`VMRAsyncOp`](/classes/vmrasyncop) - An async operation that resolves with the final gain value. + + +______ +* ### `IsPhysical()` :id=isphysical + Returns `true` if the bus/strip is a physical (hardware) one. + + **Returns**: `Boolean` \ No newline at end of file diff --git a/docs/classes/vmrbus.md b/docs/classes/vmrbus.md new file mode 100644 index 0000000..4887b40 --- /dev/null +++ b/docs/classes/vmrbus.md @@ -0,0 +1,34 @@ +# `VMRBus` + + A wrapper class for voicemeeter buses. + +#### Extends: [`VMRAudioIO`](/classes/vmraudioio) +## Constructor `__New(p_index, p_vmrType)` :id=constructor + Creates a new VMRBus object. + + **Parameters**: + - `p_index` : `Number` - The zero-based index of the bus. + + - `p_vmrType` : `Number` - The type of the running voicemeeter. + + +## Properties +* #### **Static** `Devices` : `Array` :id=static-devices + An array of bus (output) devices +* #### `Name` : `String` :id=name + The bus's name (as shown in voicemeeter's UI) + +## Methods +* ### `GetDevice(p_name, p_driver := unset)` :id=static-getdevice + Retrieves a bus (output) device by its name/driver. + + + **See also**: + `VMRConsts.DEVICE_DRIVERS` for a list of valid drivers. + + **Parameters**: + - `p_name` : `String` - The name of the device. + + - **Optional** `p_driver` : `String` - The driver of the device, If omitted, `VMRConsts.DEFAULT_DEVICE_DRIVER` will be used. + + **Returns**: [`VMRDevice`](/classes/vmrdevice) - A device object, or an empty string `""` if the device was not found. \ No newline at end of file diff --git a/docs/classes/vmrcommands.md b/docs/classes/vmrcommands.md new file mode 100644 index 0000000..65ca830 --- /dev/null +++ b/docs/classes/vmrcommands.md @@ -0,0 +1,142 @@ +# `VMRCommands` + + Write-only actions that control voicemeeter + +## Properties +* #### `Button` : `Any` :id=button + Sets a macro button's parameter + +## Methods +* ### `Restart()` :id=restart + Restarts the Audio Engine + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `Shutdown()` :id=shutdown + Shuts down Voicemeeter + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `Show(p_open := true)` :id=show + Shows the Voicemeeter window + + **Parameters**: + - **Optional** `p_open` : `Boolean` - `true` to show the window, `false` to hide it + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `Lock(p_state := true)` :id=lock + Locks the Voicemeeter UI + + **Parameters**: + - **Optional** `p_state` : `number` - `true` to lock the UI, `false` to unlock it + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `Eject()` :id=eject + Ejects the recorder's cassette + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `Reset()` :id=reset + Resets all voicemeeeter configuration + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `Save(p_filePath)` :id=save + Saves the current configuration to a file + + **Parameters**: + - `p_filePath` : `String` - The path to save the configuration to + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `Load(p_filePath)` :id=load + Loads configuration from a file + + **Parameters**: + - `p_filePath` : `String` - The path to load the configuration from + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `ShowVBANChat(p_show := true)` :id=showvbanchat + Shows the VBAN chat dialog + + **Parameters**: + - **Optional** `p_show` : `Boolean` - `true` to show the dialog, `false` to hide it + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `SaveBusEQ(p_busIndex, p_filePath)` :id=savebuseq + Saves a bus's EQ settings to a file + + **Parameters**: + - `p_busIndex` : `Number` - The one-based index of the bus to save + + - `p_filePath` : `String` - The path to save the EQ settings to + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `LoadBusEQ(p_busIndex, p_filePath)` :id=loadbuseq + Loads a bus's EQ settings from a file + + **Parameters**: + - `p_busIndex` : `Number` - The one-based index of the bus to load + + - `p_filePath` : `String` - The path to load the EQ settings from + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `SaveStripEQ(p_stripIndex, p_filePath)` :id=savestripeq + Saves a strip's EQ settings to a file + + **Parameters**: + - `p_stripIndex` : `Number` - The one-based index of the strip to save + + - `p_filePath` : `String` - The path to save the EQ settings to + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `LoadStripEQ(p_stripIndex, p_filePath)` :id=loadstripeq + Loads a strip's EQ settings from a file + + **Parameters**: + - `p_stripIndex` : `Number` - The one-based index of the strip to load + + - `p_filePath` : `String` - The path to load the EQ settings from + + **Returns**: `Boolean` - true if the command was successful + + +______ +* ### `RecallPreset(p_preset)` :id=recallpreset + Recalls a Preset Scene + + **Parameters**: + - `p_preset` : `String | Number` - The name of the preset to recall or its one-based index + + **Returns**: `Boolean` - true if the command was successful \ No newline at end of file diff --git a/docs/classes/vmrcontrollerbase.md b/docs/classes/vmrcontrollerbase.md new file mode 100644 index 0000000..0fefa29 --- /dev/null +++ b/docs/classes/vmrcontrollerbase.md @@ -0,0 +1,29 @@ +# `VMRControllerBase` + + +## Methods +* ### `SetParameter(p_name, p_value)` :id=setparameter + Sets the value of a parameter. + + **Parameters**: + - `p_name` : `String` - The name of the parameter. + + - `p_value` : `Any` - The value of the parameter. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If invalid parameters are passed or if an internal error occurs. + + **Returns**: [`VMRAsyncOp`](/classes/vmrasyncop) - An async operation that resolves to `true` if the parameter was set successfully. + + +______ +* ### `GetParameter(p_name)` :id=getparameter + Returns the value of a parameter. + + **Parameters**: + - `p_name` : `String` - The name of the parameter. + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If invalid parameters are passed or if an internal error occurs. + + **Returns**: `Any` - The value of the parameter. diff --git a/docs/classes/vmrdevice.md b/docs/classes/vmrdevice.md new file mode 100644 index 0000000..48a0509 --- /dev/null +++ b/docs/classes/vmrdevice.md @@ -0,0 +1,10 @@ +# `VMRDevice` + + +## Properties +* #### `Name` : `String` :id=name + The device's name +* #### `Hwid` : `String` :id=hwid + The device's hardware id +* #### `Driver` : `String` :id=driver + The device's driver, See `VMRConsts.DEVICE_DRIVERS` for a list of valid drivers. diff --git a/docs/classes/vmrerror.md b/docs/classes/vmrerror.md new file mode 100644 index 0000000..1599fb5 --- /dev/null +++ b/docs/classes/vmrerror.md @@ -0,0 +1,14 @@ +# `VMRError` + + +#### Extends: `Error` + +## Properties +* #### `ReturnCode` : `Number` :id=returncode + The return code of the Voicemeeter function that failed +* #### `What` : `String` :id=what + The name of the function that threw the error +* #### `Message` : `String` :id=message + An error message +* #### `Extra` : `String` :id=extra + Extra information about the error \ No newline at end of file diff --git a/docs/classes/vmrmacrobutton.md b/docs/classes/vmrmacrobutton.md new file mode 100644 index 0000000..5de23b7 --- /dev/null +++ b/docs/classes/vmrmacrobutton.md @@ -0,0 +1,60 @@ +# `VMRMacroButton` + + + +## Methods +* ### `Run()` :id=run + Runs the Voicemeeter Macro Buttons application. + + +______ +* ### `Show(p_show := true)` :id=show + Shows/Hides the Voicemeeter Macro Buttons application. + + **Parameters**: + - **Optional** `p_show` : `Boolean` - Whether to show or hide the application + + + +______ +* ### `SetStatus(p_index, p_value, p_bitMode := 0)` :id=setstatus + Sets the status of a given button. + + **Parameters**: + - `p_index` : `Number` - The one-based index of the button + + - `p_value` : `Number` - The value to set + - `0`: Off + - `1`: On + + - **Optional** `p_bitMode` : `Number` - The type of the returned value + - `0`: button-state + - `2`: displayed-state + - `3`: trigger-state + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs + + **Returns**: `Number` - The status of the button + - `0`: Off + - `1`: On + + +______ +* ### `GetStatus(p_index, p_bitMode := 0)` :id=getstatus + Gets the status of a given button. + + **Parameters**: + - `p_index` : `Number` - The one-based index of the button + + - **Optional** `p_bitMode` : `Number` - The type of the returned value + - `0`: button-state + - `2`: displayed-state + - `3`: trigger-state + + **Throws**: + - [`VMRError`](/classes/vmrerror) - If an internal error occurs + + **Returns**: `Number` - The status of the button + - `0`: Off + - `1`: On \ No newline at end of file diff --git a/docs/classes/vmrrecorder.md b/docs/classes/vmrrecorder.md new file mode 100644 index 0000000..efde902 --- /dev/null +++ b/docs/classes/vmrrecorder.md @@ -0,0 +1,29 @@ +# `VMRRecorder` + + + +#### Extends: [`VMRControllerBase`](/classes/vmrcontrollerbase) + +## Properties +* #### `ArmBus` : `Boolean` :id=armbus + 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). +* #### `ArmStrip` : `Boolean` :id=armstrip + 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). + +## Methods +* ### `ArmStrips(p_strips)` :id=armstrips + Arms the specified strips for recording, switching the recording mode to `0` (strip) and disarming any armed strips. + + **Parameters**: + - `p_strips` : `Array` - The strips' one-based indices. + + + +______ +* ### `Load(p_path)` :id=load + Loads a file into the recorder. + + **Parameters**: + - `p_path` : `String` \ No newline at end of file diff --git a/docs/classes/vmrstrip.md b/docs/classes/vmrstrip.md new file mode 100644 index 0000000..7243db7 --- /dev/null +++ b/docs/classes/vmrstrip.md @@ -0,0 +1,38 @@ +# `VMRStrip` + + A wrapper class for voicemeeter strips. + +#### Extends: [`VMRAudioIO`](/classes/vmraudioio) +## Constructor `__New(p_index, p_vmrType)` :id=constructor + Creates a new VMRStrip object. + + **Parameters**: + - `p_index` : `Number` - The zero-based index of the strip. + + - `p_vmrType` : `Number` - The type of the running voicemeeter. + + +## Properties +* #### **Static** `Devices` : `Array` :id=static-devices + An array of strip (input) devices +* #### `AppGain` : `Number` :id=appgain + Sets an application's gain on the strip. +* #### `AppMute` : `Boolean` :id=appmute + Sets an application's mute state on the strip. +* #### `Name` : `String` :id=name + The strip's name (as shown in voicemeeter's UI) + +## Methods +* ### `GetDevice(p_name, p_driver := unset)` :id=static-getdevice + Retrieves a strip (input) device by its name/driver. + + + **See also**: + `VMRConsts.DEVICE_DRIVERS` for a list of valid drivers. + + **Parameters**: + - `p_name` : `String` - The name of the device. + + - **Optional** `p_driver` : `String` - The driver of the device, If omitted, `VMRConsts.DEFAULT_DEVICE_DRIVER` will be used. + + **Returns**: [`VMRDevice`](/classes/vmrdevice) - A device object, or an empty string `""` if the device was not found. \ No newline at end of file diff --git a/docs/classes/vmrutils.md b/docs/classes/vmrutils.md new file mode 100644 index 0000000..b1d0628 --- /dev/null +++ b/docs/classes/vmrutils.md @@ -0,0 +1,72 @@ +# `VMRUtils` + + + +## Methods +* ### `DbToPercentage(p_dB)` :id=static-dbtopercentage + Converts a dB value to a percentage value. + + **Parameters**: + - `p_dB` : `Number` - The dB value to convert. + + **Returns**: `Number` - The percentage value. + + +______ +* ### `PercentageToDb(p_percentage)` :id=static-percentagetodb + Converts a percentage value to a dB value. + + **Parameters**: + - `p_percentage` : `Number` - The percentage value to convert. + + **Returns**: `Number` - The dB value. + + +______ +* ### `EnsureBetween(p_value, p_min, p_max)` :id=static-ensurebetween + Applies an upper and a lower bound on a passed value. + + **Parameters**: + - `p_value` : `Number` - The value to apply the bounds on. + + - `p_min` : `Number` - The lower bound. + + - `p_max` : `Number` - The upper bound. + + **Returns**: `Number` - The value with the bounds applied. + + +______ +* ### `IndexOf(p_array, p_value)` :id=static-indexof + Returns the index of the first occurrence of a value in an array, or -1 if it's not found. + + **Parameters**: + - `p_array` : `Array` - The array to search in. + + - `p_value` : `Any` - 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. + + +______ +* ### `Join(p_params, p_seperator, p_maxLength := 30)` :id=static-join + Returns a string with the passed parameters joined using the passed seperator. + + **Parameters**: + - `p_params` : `Array` - The parameters to join. + + - `p_seperator` : `String` - The seperator to use. + + - **Optional** `p_maxLength` : `Number` - The maximum length of each parameter. + + **Returns**: `String` - The joined string. + + +______ +* ### `ToString(p_value)` :id=static-tostring + Converts a value to a string. + + **Parameters**: + - `p_value` : `Any` - The value to convert to a string. + + **Returns**: `String` - The string representation of the passed value \ No newline at end of file diff --git a/docs/classes/vmrvban.md b/docs/classes/vmrvban.md new file mode 100644 index 0000000..4514ae8 --- /dev/null +++ b/docs/classes/vmrvban.md @@ -0,0 +1,11 @@ +# `VMRVBAN` + + + +#### Extends: [`VMRControllerBase`](/classes/vmrcontrollerbase) + +## Properties +* #### `Instream` : [`VMRControllerBase`](/classes/vmrcontrollerbase) :id=instream + Controls a VBAN input stream +* #### `Outstream` : [`VMRControllerBase`](/classes/vmrcontrollerbase) :id=outstream + Controls a VBAN output stream \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 257f252..02c8f76 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2,38 +2,66 @@ - - VMR.ahk Docs - - - - - - - + + VMR.ahk Docs + + + + + + + + + + + + + + + -
- - - - - - - +
+ + + + + + + + + + + + - + \ No newline at end of file diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 0000000..498866b --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,84 @@ +:root { + --search-clear-icon-color1: #4a5e69; +} + +* { + color-scheme: var(--color-scheme); +} + +body.close .sidebar+.content { + transform: none; +} + +h4 { + margin-top: 1em !important; +} + +/* TOC */ +aside.toc-nav { + color: var(--toogleBackground); + top: 50vh; + transform: translateY(-50%); + right: 0%; + width: 250px; + max-height: 80vh; +} + +.page_toc div[class^="lv"] a { + font-weight: normal; + color: unset; +} + +.page_toc div.lv4{ + display: flex; +} + +.page_toc div.lv4 a:not(:first-child) { + text-indent: 10px; +} + +.page_toc div.active { + border-left-color: #43c385; + transition: border-left-color 0.23s; +} + +.page_toc div { + border-left: 2px solid #e8e8e845 +} + +@media only screen and (max-width: 1299px) { + aside.toc-nav { + background: var(--sidebar-background); + border: solid 1px; + border-color: var(--sidebar-border-color); + border-right: none; + padding-left: 10px; + width: 225px; + max-width: 35px; + max-height: 60vh; + } +} + +/* Sidebar */ +.sidebar .search .input-wrap { + align-items: center; +} + +.docs-title { + font-family: "Open Sans", sans-serif; + font-size: 1.4em; + font-weight: 300; +} + +.sidebar-nav ul:not(.app-sub-sidebar)>li:not(.file)::before { + top: 9px; + left: -20px; + height: 8px; + width: 8px; + border-right: 2px solid #495e68; + border-bottom: 2px solid #495e68; +} + +.sidebar-nav ul:not(.app-sub-sidebar)>li:not(.file) { + margin: var(--sidebar-nav-pagelink-padding, var(--sidebar-nav-link-padding)); +} \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 8edb763..4503045 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,8 +1,13 @@ ## VMR.ahk Examples + * ### [Hotkey example](./hotkey_example.ahk): create hotkeys to control VoiceMeeter. + * ### [UI example](./ui_example.ahk): a simple GUI that controls VoiceMeeter's buses - ![UI example](https://user-images.githubusercontent.com/47293197/118356850-d7be9e80-b57f-11eb-843a-db7f8dd996d1.gif) + ![UI example](https://user-images.githubusercontent.com/47293197/118356850-d7be9e80-b57f-11eb-843a-db7f8dd996d1.gif) + * ### [Level stabilizer](./level_stabilizer_example.ahk): stabilizes the audio level by adjusting the gain. - ![Level stabilizer](https://user-images.githubusercontent.com/47293197/118352761-d2575900-b56b-11eb-98d0-9b4e43024249.gif) + ![Level stabilizer](https://user-images.githubusercontent.com/47293197/118352761-d2575900-b56b-11eb-98d0-9b4e43024249.gif) + * ### [Script example](./script_example.ahk): demonstrates two ways to write and execute a script. + * ### [MIDI example](./midi_message_example.ahk): demonstrates the usage of `onMidiMessage` callback. diff --git a/examples/hotkey_example.ahk b/examples/hotkey_example.ahk index 78daa2a..a29c388 100644 --- a/examples/hotkey_example.ahk +++ b/examples/hotkey_example.ahk @@ -1,54 +1,117 @@ -#Include, %A_ScriptDir%\..\dist\VMR.ahk +#Requires AutoHotkey >=2.0 -voicemeeter := new VMR().login() -vol := 0.5 -voicemeeter.strip[6].AppGain := Format("(""Spotify"", {:.1f})", vol) ;set initial Spotify volume -voicemeeter.bus[1].gain_limit:=0 +#Include %A_ScriptDir%\..\dist\VMR.ahk -for i, bus in voicemeeter.bus { - bus.gain:=0 ; set gain to 0 for all busses at startup +voicemeeter := VMR().Login() + +; Set the gain to 0 for all busses at startup +for (bus in voicemeeter.Bus) { + bus.gain := 0 } -Volume_Up::voicemeeter.bus[1].gain++ ;bind volume up key to increase bus[1] gain -Volume_Down::voicemeeter.bus[1].gain-- +; jsdoc type annotations are not needed, but might allow your editor to offer relevant suggestions (see vscode-autohotkey2-lsp plugin) +/** @type {VMRBus} */ +mainOutput := voicemeeter.Bus[1] +mainOutput.GainLimit := 0 + +/** @type {VMRStrip} */ +auxInput := voicemeeter.Strip[3] + +; Check if we're running voicemeeter potato +if (voicemeeter.Type == VMR.Types.Potato) { + ; Set initial Spotify volume + spotifyVol := 0.5 + auxInput.AppGain["Spotify"] := spotifyVol +} + +; Bind ctrl+M to toggle mute bus[1] +^M:: mainOutput.mute := -1 + +; Bind volume keys to increase/decrease bus[1] gain +Volume_Up:: mainOutput.gain++ +Volume_Down:: mainOutput.gain-- +; Or using the increment method (check below for an explanation) +; Volume_Up:: mainOutput.Increment("gain", 1).Then(DisplayTooltip) +; Volume_Down:: mainOutput.Increment("gain", -1).Then(DisplayTooltip) + +/** + * `Increment` and several other methods return a {@link VMRAsyncOp|`VMRAsyncOp`} object which allows you to pass a callback function that receives the result of the operation once it's done. + * + * Although incrementing the gain directly (like this: `mainOutput.gain++`) and then getting the new value immediately might work, more often than not, the returned value will be wrong as the parameter has not been set yet, + * this happens because the voicemeeter API is asynchronous. + * + * Functionally, VMRAsyncOp is similar to a javascript promise, but it actually just uses a timer to resolve the operation which then calls all registered callbacks. + * + * @example Equivalent to the code below but without VMRAsyncOp + * auxInput.gain += 5 + * SetTimer(() => ToolTip(auxInput.gain) && SetTimer(() => ToolTip(), -1000), -50) + */ +^Volume_Up:: auxInput + .Increment("gain", 5) + .Then(gain => ToolTip(gain), 1000) + .Then(() => ToolTip()) +^Volume_Down:: auxInput + .Increment("gain", -5) + .Then(gain => ToolTip(gain), 1000) + .Then(() => ToolTip()) -^M::voicemeeter.bus[1].mute-- ; bind ctrl+M to toggle mute bus[1] +monitorSpeakers := VMRBus.GetDevice("LG") ; Returns the first output device with "LG" in its name using the default driver (wdm) +microphone := VMRStrip.GetDevice("amazonbasics", "mme") ; Get the first input device with "amazonbasics" in its name using the mme driver +F6:: mainOutput.device := monitorSpeakers +F7:: voicemeeter.Strip[2].device := microphone -^Volume_Up::ToolTip, % voicemeeter.strip[5].gain+=5 -^Volume_Down::ToolTip, % voicemeeter.strip[5].gain-=5 +^G:: { + MsgBox(mainOutput.Name " gain:" . mainOutput.gain . " dB") + MsgBox(mainOutput.Name " gain percentage:" mainOutput.GainPercentage "%") + MsgBox(mainOutput.Name " " (mainOutput.mute ? "Muted" : "Unmuted")) +} -F6::voicemeeter.bus[1].device:= "LG" ; set bus[1] to the first device with "LG" in its name using wdm driver -F7::voicemeeter.strip[2].device["mme"]:= "amazonbasics" +^Y:: voicemeeter.Command.Show() -^G:: -MsgBox, % "bus[1] gain:" . voicemeeter.bus[1].gain . " dB" -MsgBox, % "bus[1] gain percentage:" . voicemeeter.bus[1].getGainPercentage() . "%" -MsgBox, % "bus[1] " . (voicemeeter.bus[1].mute ? "Muted" : "Unmuted") -return +^K:: mainOutput.FadeBy(-3, 2000) + .Then(finalGain => ToolTip("Faded to " finalGain " dB"), 3000) + .Then(() => ToolTip()) +; Or using a normal parameter setter: +; ^K:: mainOutput.FadeBy := "(-3.0, 2000)" -^Y::voicemeeter.command.show() +^T:: MsgBox(mainOutput.Name " Level: " . mainOutput.Level[1]) -^K::voicemeeter.bus[1].FadeTo:="(-18.0, 2000)" ;set any parameter for a bus/strip +!r:: { + voicemeeter.Recorder.ArmStrip[4] := true + voicemeeter.Recorder.mode["loop"] := 1 ; Or voicemeeter.Recorder.SetParameter("mode.loop", 1) + voicemeeter.Recorder.record := true +} -^T::MsgBox, % "Bus[1] Level: " . voicemeeter.bus[1].level[1] +!e:: { + voicemeeter.Recorder.stop := true + voicemeeter.Command.Eject() +} -!r:: -voicemeeter.recorder.ArmStrip(4,1) -voicemeeter.recorder["mode.Loop"]:=1 -voicemeeter.recorder.record:=1 -return +; Decrease Spotify volume by 0.1 +^A:: { + global spotifyVol := VMRUtils.EnsureBetween(spotifyVol - 0.1, 0, 1) + auxInput.AppGain["Spotify"] := spotifyVol + DisplayTooltip("Spotify: " spotifyVol) + ; Or using an index + ; auxInput.AppGain[1] := spotifyVol +} -!s:: -voicemeeter.recorder.stop:=1 -voicemeeter.command.eject(1) -return +; Increase Spotify volume by 0.1 +^D:: { + global spotifyVol := VMRUtils.EnsureBetween(spotifyVol + 0.1, 0, 1) + auxInput.AppGain["Spotify"] := spotifyVol + DisplayTooltip("Spotify: " spotifyVol) +} + +; Show/hide voicemeeter +!S:: voicemeeter.Command.Show(true) +^!S:: voicemeeter.Command.Show(false) -^A:: -vol -= 0.1 -voicemeeter.strip[6].AppGain := Format("(""Spotify"", {:.1f})", vol) ;increase Spotify volume by 0.1 -return +DisplayTooltip(txt) { + ToolTip(txt) + SetTimer(HideTooltip, -2000) -^D:: -vol += 0.1 -voicemeeter.strip[6].AppGain := Format("(""Spotify"", {:.1f})", vol) ;decrease Spotify volume by 0.1 -return \ No newline at end of file + static HideTooltip() { + ToolTip() + } +} diff --git a/examples/level_stabilizer_example.ahk b/examples/level_stabilizer_example.ahk index 3a13fb1..592e873 100644 --- a/examples/level_stabilizer_example.ahk +++ b/examples/level_stabilizer_example.ahk @@ -1,40 +1,53 @@ -#Include, %A_ScriptDir%\..\dist\VMR.ahk -#Persistent - -; This is just an example to demo the `level` array -; It works best when the limits are set 20dBs apart - -global voicemeeter:= (new VMR).login() -, device := voicemeeter.bus[1] -, default_gain := device.gain -, upper_limit := -20 -, lower_limit := -40 -voicemeeter.onUpdateLevels:= Func("levelStabilizer") -OnExit("reset",-1) - -; stabilize the audio level by adjusting the gain -levelStabilizer(){ - static is_stable - lvl:= Max(device.level*) ; get the current peak level - if(lvl = -999) ; if there's no sound - device.gain:= default_gain - else if(lvl >= upper_limit){ ; if the level is higher than the upper_limit - device.FadeTo := "(" device.gain-5 ", 300)" ; lower the gain by 5dBs over 200 ms - is_stable:=0 - }else if((device.gain < default_gain && !is_stable) || lvl <= lower_limit ){ - ; raise the gain if the level is lower than the lower_limit - ; or if the level isn't stable and is lower than the default_limit - device.FadeTo := "(" device.gain+1 ", 200)" - if(lvl >= upper_limit) - device.FadeTo := "(" device.gain-1 ", 200)" - is_stable:=1 +#Requires AutoHotkey >=2.0 +#Include %A_ScriptDir%\..\dist\VMR.ahk + +; Persistent() is needed to keep the script running in the background since no hotkeys are defined +Persistent(true) + +/** + * This is just an example to demo the bus/strip `Level` array + */ +voicemeeter := VMR().Login() + +/** @type {VMRBus} */ +device := voicemeeter.Bus[1] + +defaultGain := device.gain +upperLimit := -20 +lowerLimit := -30 + +voicemeeter.On(VMRConsts.Events.LevelsUpdated, StabilizeLevel) +OnExit(Reset, -1) + +; stabilizes the audio level by adjusting the gain +StabilizeLevel() { + static isStable := false + + ; get the current peak level + lvl := Max(device.Level*) + if (lvl = -999) { ; There's no sound + device.gain := defaultGain + } + else if (lvl >= upperLimit) { ; The level is higher than the upper limit + ; Lower the gain by 5dBs over 300 ms + device.FadeBy(-5, 300) + isStable := false + } + else if ((!isStable && device.gain < defaultGain) || lvl <= lowerLimit) { + ; Raise the gain if the level isn't stable and is lower than the default_limit + ; or if the level is lower than the lower_limit + device.FadeBy(1, 300) + if (lvl >= upperLimit) + device.FadeBy(-1, 300) + isStable := true } } -reset(){ - device.gain:= default_gain ; reset to default gain - Sleep, 100 +Reset(*) { + ; Reset the bus's gain + device.gain := defaultGain + Sleep(100) } -*<^<+Q:: ;bind LCtrl + LShift + Q to exit -ExitApp \ No newline at end of file +; Bind LCtrl + LShift + Q to exit +*<^<+Q:: ExitApp() \ No newline at end of file diff --git a/examples/midi_message_example.ahk b/examples/midi_message_example.ahk index 60db9e1..8aca072 100644 --- a/examples/midi_message_example.ahk +++ b/examples/midi_message_example.ahk @@ -1,12 +1,18 @@ -#Include, %A_ScriptDir%\..\dist\VMR.ahk -#Persistent +#Requires AutoHotkey >=2.0 +#Include %A_ScriptDir%\..\dist\VMR.ahk -global voicemeeter := new VMR().login() -voicemeeter.onMidiMessage:= Func("writeMidi") -return +Persistent(true) -; recieves an array of bytes that represents midi messages (every 3 elements represent a single midi message) -; writes midi messages to a file (messages.txt) -writeMidi(midi){ - FileAppend,% Format("Midi Message: {}, {}, {}`n",midi[1],midi[2],midi[3]), messages.txt +voicemeeter := VMR().login() +voicemeeter.On(VMRConsts.Events.MidiMessage, WriteMidi) + +/** + * Receives an array with the hex-formatted bytes of the message and writes them to a file + * + * @param {Array} p_midi - `[0xF0, 0xFF, ...]` An array of hex-formatted bytes that compose one or more MIDI messages. + * - A single message is usually 2 or 3 bytes long + * - The returned array will contain at most `1024` bytes. + */ +WriteMidi(p_midi) { + FileAppend(Format("Midi Message: {}, {}, {}`n", p_midi[1], p_midi[2], p_midi[3]), "messages.txt") } diff --git a/examples/script_example.ahk b/examples/script_example.ahk index 9d89245..02f050d 100644 --- a/examples/script_example.ahk +++ b/examples/script_example.ahk @@ -1,35 +1,34 @@ -#Include, %A_ScriptDir%\..\dist\VMR.ahk -#Persistent +#Requires AutoHotkey >=2.0 +#Include %A_ScriptDir%\..\dist\VMR.ahk -global voicemeeter := (new VMR).login() -AhkScript() -;VoiceMeeterScript() -ExitApp +Persistent(true) -AhkScript(){ - ; this is an AHK script - ; indexes are one-based - voicemeeter.strip[1].A1 := 1 - voicemeeter.strip[1].B1 := 0 - voicemeeter.bus[2].gain := -6.0 - voicemeeter.strip[3].gain := 12.0 - voicemeeter.recorder.A1 := 1 - voicemeeter.vban.outstream[4].name := "stream example" -} +voicemeeter := VMR().login() -; OR +/** + * Scripts can be written in AHK (indices are one-based) + */ +voicemeeter.Strip[1].A1 := true +voicemeeter.Strip[1].B1 := false +voicemeeter.Bus[2].gain := -6.0 +voicemeeter.Strip[3].gain := 12.0 +voicemeeter.Recorder.A1 := 1 +voicemeeter.VBAN.Outstream[4].name := "stream example" -VoiceMeeterScript(){ - script = - ( LTrim Comments - ; this is a voicemeeter script (not AHK) - ; indexes are zero-based +/** + * Or as a string of voiceemeeter commands (indices are zero-based) + * - Useful for loading scripts from a file + * - Commands can be delimited by newlines or semicolons (see {@link VMR.Exec|VMR.Exec()}) + */ +script := " + ( LTrim Strip[0].A1 = 1 Strip[0].B1 = 0 Bus[1].gain = -6.0 Strip[2].gain = 12.0 Recorder.A1 = 1 vban.outstream[3].name = "stream example" - ) - voicemeeter.exec(script) -} \ No newline at end of file + )" +voicemeeter.Exec(script) + +ExitApp() \ No newline at end of file diff --git a/examples/ui_example.ahk b/examples/ui_example.ahk index 40e9af4..6725c28 100644 --- a/examples/ui_example.ahk +++ b/examples/ui_example.ahk @@ -1,109 +1,128 @@ -#Include, %A_ScriptDir%\..\dist\VMR.ahk -SetBatchLines, 20ms - -Global vm, GUI_hwnd, is_win_pos_changing:=0 - -vm := new VMR().login() -showUI() -vm.onUpdateLevels:= Func("syncLevel") ; register level callback func -vm.onUpdateParameters:= Func("syncParameters") ; register params callback func -OnMessage(0x46, Func("onPosChanging")) - -showUI(){ - Global - Gui, vm:New, +HwndGUI_hwnd, VoiceMeeter Remote UI - xPos:=10 - Loop % vm.bus.Length() { ; add UI controls for each bus - ;bus title - yPos:=0, funcObj:="" - Gui, Add, Text, x%xPos% y%yPos% w100, Bus[%A_Index%] - - ;bus level - yPos+= 30 - Gui, Add, Progress, x%xPos% y%yPos% w20 h200 Range-72-20 c0x70C399 Background0x2C3D4D vertical Hwndbus_%A_Index%_level - - ;bus gain - yPos+= 220 - Gui, Add, Edit, w50 x%xPos% y%yPos% ReadOnly - Gui, Add, UpDown,Hwndbus_%A_Index%_gain Range-60-12 x0 - funcObj:= Func("updateParam").bind("gain", A_Index) - GuiControl +g, % bus_%A_Index%_gain, % FuncObj - - ;bus mute - yPos+= 30 - Gui, Add, CheckBox, x%xPos% y%yPos% Hwndbus_%A_Index%_mute, Mute - funcObj:= Func("updateParam").bind("mute", A_Index) - GuiControl +g, % bus_%A_Index%_mute, % FuncObj - - ;bus device - if(vm.bus[A_Index].isPhysical()){ ; make sure the bus is a physical one (eg. 1-3 in banana) - yPos+= 30 - Gui, Add, DropDownList, x%xPos% y%yPos% Hwndbus_%A_Index%_device - funcObj:= Func("updateParam").bind("device", A_Index) - GuiControl +g, % bus_%A_Index%_device, % FuncObj - refreshDevices(A_Index) - } - - xPos+=150 - } - syncParameters() ; get initial values for gui controls - Gui, Show, H350, VoiceMeeter Remote UI -} +#Requires AutoHotkey >=2.0 +#Include %A_ScriptDir%\..\dist\VMR.ahk -; update vm bus parameters when they change on the AHK UI -updateParam(param, index){ - GuiControlGet, val,,% bus_%index%_%param% - if(param == "device"){ - RegExMatch(val, "iO)(?\w+): (?.+)", match) - if(match) - vm.bus[index].device[match.driver]:= match.name - else - vm.bus[index].device:= "" - }else{ - vm.bus[index][param]:= val - } - SetTimer, syncParameters, -500 ; make sure params are in sync +voicemeeter := VMR().Login() + +ui := Gui("-Resize", "Voicemeeter Remote UI") +ui.OnEvent("Close", (*) => ExitApp()) +ui.SetFont(, "Segoe UI") + +title := ui.Add("Text", "w500 r2", voicemeeter.Type.Name " v" voicemeeter.version) +title.SetFont("s16 bold Q5") +title.GetPos(, &initialYPos) + +; Add strip UI controls +xPos := 10 +initialYPos += 50 +for i, strip in voicemeeter.Strip { + AddControls(strip, xPos, initialYPos) + xPos += 150 } -; sync AHK UI controls with vm bus parameters -syncParameters(){ - Loop % vm.bus.Length() { - GuiControl,, % bus_%A_Index%_gain, % vm.bus[A_Index].gain - GuiControl,, % bus_%A_Index%_mute, % Format("{:i}", vm.bus[A_Index].mute) ; convert 0.0/1.0 to 0/1 - if(vm.bus[A_Index].__isPhysical()) - refreshDevices(A_Index) - } +; Add bus UI controls +xPos := 10 +initialYPos += 350 +for i, bus in voicemeeter.Bus { + AddControls(bus, xPos, initialYPos) + xPos += 150 } -; sync level meters with vm bus levels -syncLevel(){ - if(is_win_pos_changing) ;dont update levels if the window is changing its position - return - Loop % vm.bus.Length() { - GuiControl,, % bus_%A_Index%_level, % Max(vm.bus[A_Index].level*) ; get peak level for the bus +ui.Show("AutoSize") + +/** + * Adds the controls for a given VMRAudioIO object + * + * @param {VMRAudioIO} vmObj + * @param {Number} xPos + * @param {Number} yPos + */ +AddControls(vmObj, xPos, yPos) { + ; Title + objTitle := ui.Add("Text", "x" xPos " y" yPos " w100", vmObj.Id "`n" vmObj.Name) + + ; Level Indicator + yPos += 30 + levelIndicator := ui.Add("Progress", "x" xPos " y" yPos " w20 h200 Range-72-20 c0x70C399 Background0x2C3D4D vertical") + ; Bind voicemeeter changes to the UI + voicemeeter.On("levelsUpdated", (*) => levelIndicator.Value := Max(vmObj.Level*)) + + ; Gain Controls + yPos += 220 + gainControl := ui.Add("Edit", "x" xPos " y" yPos " w50 ReadOnly") + gainUpDn := ui.Add("UpDown", "x" xPos " y" yPos " Range-60-12") + ; Initial state + gainUpDn.Value := vmObj.Gain + ; Bind UI changes to voicemeeter + gainUpDn.OnEvent("Change", (*) => vmObj.Gain := Number(gainUpDn.Value)) + ; Bind voicemeeter changes to the UI + voicemeeter.On("ParametersChanged", (*) => gainUpDn.Value := vmObj.Gain) + + ; Mute Controls + yPos += 30 + objMute := ui.Add("CheckBox", "x" xPos " y" yPos, "Mute") + ; Initial state + objMute.Value := vmObj.Mute + ; Bind UI changes to voicemeeter + objMute.OnEvent("Click", (*) => vmObj.Mute := objMute.Value) + ; Bind voicemeeter changes to the UI + voicemeeter.On("ParametersChanged", (*) => objMute.Value := vmObj.Mute) + + ; Device selector (only for physical strips and buses) + if (vmObj.IsPhysical()) { + yPos += 30 + deviceDdl := ui.Add("DropDownList", "x" xPos " y" yPos " w130", []) + ; Initial state + RefreshDevices(deviceDdl, vmObj) + ; Bind UI changes to voicemeeter + deviceDdl.OnEvent("Change", (*) => vmObj.Device := GetDeviceByDisplayName(deviceDdl.Text)) + ; Bind voicemeeter changes to the UI + voicemeeter.On("DevicesUpdated", (*) => RefreshDevices(deviceDdl, vmObj)) + ; Delay setting the ddl value to avoid getting the wrong device name + voicemeeter.On("ParametersChanged", (*) => SetTimer(() => deviceDdl.Choose(GetDeviceDisplayName(vmObj.Device)), -1000)) } } -; clears the bus's drop-down and reinserts the devices -refreshDevices(index){ - elems:="| |" ; extra empty element for removing the device - preSelected:= vm.bus[index].device ; get the pre-selected device - for i, device in vm.getBusDevices() { - elems.= Format("{:U}: {}", device.driver, device.name) - elems.= device.name == preSelected? "||" : "|" +/** + * Refreshes the devices in the dropdown list + * @param {Gui.DDL} ddl + * @param {VMRAudioIO} ioObj + */ +RefreshDevices(ddl, ioObj) { + local device, + ; Get the devices array based on the object type + devicesArr := ioObj is VMRStrip ? VMRStrip.Devices : VMRBus.Devices, + deviceNames := [""] ; Add an empty item to allow removing the selected device + + ; Get the display name of every device + for device in devicesArr { + deviceNames.Push(GetDeviceDisplayName(device)) } - GuiControl,, % bus_%index%_device, % elems -} -vmGuiClose(){ - ExitApp + ; Replace the items in the dropdown list + ddl.Delete() + ddl.Add(deviceNames) + + ; Choose the current selected device + ddl.Choose(GetDeviceDisplayName(ioObj.Device)) } -onPosChanging(){ - is_win_pos_changing:=1 - SetTimer, onPosChanged, -50 +/** + * @param {VMRDevice} device + * @returns {String} - The display name for the given device + */ +GetDeviceDisplayName(device) { + if (!device) + return "" + return Format("{} - {}", StrUpper(device.driver), device.name) } -onPosChanged(){ - is_win_pos_changing:=0 +/** + * @param {String} displayName + * @returns {VMRDevice} - The device that matches the given display name + */ +GetDeviceByDisplayName(displayName) { + if (!displayName) + return "" + RegExMatch(displayName, "i)(\w+) - (.+)", &match) + return VMRBus.GetDevice(match[2], match[1]) || VMRStrip.GetDevice(match[2], match[1]) } diff --git a/src/Build.ahk b/src/Build.ahk new file mode 100644 index 0000000..d338efb --- /dev/null +++ b/src/Build.ahk @@ -0,0 +1,91 @@ +#Requires AutoHotkey >=2.0 + +; Set default encoding to UTF-8 (without BOM) +FileEncoding("UTF-8-RAW") +lineEnding := "`r`n" ; CRLF +SetWorkingDir(A_InitialWorkingDir) + +; Only supports a normal #include with a file path (no Lib or IncludeAgain) +includeRegex := "im)^( *)#include\s+(.+?) *$" +requiresRegex := "im)^( *)#requires.*" +extraLinesRegex := "im)\r?\n\r?\n(\r?\n)*" + +; Run command: Build.ahk +arg_entryFile := A_Args.Has(1) ? A_Args[1] : "VMR.ahk" +arg_outputFile := A_Args.Has(2) ? A_Args[2] : "..\dist\VMR.ahk" +arg_version := A_Args.Has(3) ? A_Args[3] : "1.0.0" + +; Check if we should use the version from ahkpm.json +if (arg_version = "ahkpm") { + ahkpmJson := FileRead("./ahkpm.json") + RegExMatch(ahkpmJson, 'i)"version": "(.+?)"', &versionMatch) + arg_version := versionMatch[1] +} + +currentPath := A_WorkingDir +entryFileFullPath := currentPath "\" arg_entryFile +SplitPath(entryFileFullPath, &entryFileName, &entryFileDir) +SplitPath(arg_outputFile, &outputFileName, &outputFileDir) + +outputContent := FileRead(entryFileFullPath) +includedFiles := Map() + +; Avoid infinite recursion by adding the entry file to the list of included files +includedFiles.Set(entryFileName, true) + +; Recursively include all files +currentPos := 1 +loop { + currentPos := RegExMatch(outputContent, includeRegex, ¤tMatch, currentPos) + if (currentPos == 0) + break + + includePrefix := currentMatch[1] + includePath := currentMatch[2] + + SplitPath(includePath, &fileName, &fileDir) + replacement := "" + + ; Check if the file has already been included + if (!includedFiles.Has(fileName)) { + includedFiles.Set(fileName, true) + fileDir := fileDir ? fileDir : "." + replacement := ProcessScript(FileRead(entryFileDir "\" fileDir "\" fileName), includePrefix) + } + + outputContent := RegExReplace(outputContent, "\Q" currentMatch[0] "\E\n{0,1}", replacement) +} + +; Remove extra lines +outputContent := RegExReplace(outputContent, extraLinesRegex, lineEnding) + +; Replace placeholder variables +SubstituteVariable(&outputContent, "buildVersion", arg_version) +SubstituteVariable(&outputContent, "buildTimestamp", FormatTime(A_NowUTC, "yyyy-MM-dd HH:mm:ss UTC")) + +; ensure the output directory exists +if (outputFileDir && !DirExist(outputFileDir)) + DirCreate(outputFileDir) + +; Write the output file +FileOpen(arg_outputFile, "w").Write(outputContent) + +ProcessScript(content, prefix) { + content := RegExReplace(content, requiresRegex, "") + content := Trim(content, " `t`r`n") "`n" + content := InsertPrefix(content, prefix) + return content +} + +InsertPrefix(text, prefix) { + output := "" + loop parse text, "`n", "`r" { + output .= prefix A_LoopField lineEnding + } + return output +} + +SubstituteVariable(&text, variableName, value) { + static varRegexTemplate := "i)<{}>" + text := RegExReplace(text, Format(varRegexTemplate, variableName), value) +} diff --git a/src/BusStrip.ahk b/src/BusStrip.ahk deleted file mode 100644 index c8b4328..0000000 --- a/src/BusStrip.ahk +++ /dev/null @@ -1,191 +0,0 @@ -class BusStrip { - static BUS_COUNT:=0 - , BUS_LEVEL_COUNT:=0 - , BusDevices:=Array() - , STRIP_COUNT:=0 - , STRIP_LEVEL_COUNT:=0 - , StripDevices:=Array() - , BUS_STRIP_NAMES:= - ( Join LTrim ; ahk - { - 1: { - "Bus": [ - "A", - "B" - ], - "Strip": [ - "Input #1", - "Input #2", - "Virtual Input #1" - ] - }, - 2: { - "Bus": [ - "A1", - "A2", - "A3", - "B1", - "B2" - ], - "Strip": [ - "Input #1", - "Input #2", - "Input #3", - "Virtual Input #1", - "Virtual Input #2" - ] - }, - 3: { - "Bus": [ - "A1", - "A2", - "A3", - "A4", - "A5", - "B1", - "B2", - "B3" - ], - "Strip": [ - "Input #1", - "Input #2", - "Input #3", - "Input #4", - "Input #5", - "Virtual Input #1", - "Virtual Input #2", - "Virtual Input #3" - ] - } - } - ) - , initiated:=0 - - __Set(p_name, p_value, p_sec_value:=""){ - if(VMR.BusStrip.initiated && this.BUS_STRIP_ID){ - switch p_name { - case "gain": - return Format("{:.1f}",this.setParameter(p_name, max(-60.0, min(p_value, this.gain_limit)))) - case "limit": - return Format("{:.1f}",this.setParameter(p_name, max(-40.0, min(p_value, 12.0)))) - case "device": - if(IsObject(p_value)) - return this.__setDevice(p_value) - if(!p_value) - return this.__setDevice({name:"",driver:"wdm"}) - driver:= p_sec_value? p_value : "wdm" - name:= p_sec_value? p_sec_value : p_value - return this.__setDevice(this.__getDeviceObj(name,driver)) - case "mute": - if(p_value = -1) - p_value:= !this.mute - } - return this.setParameter(p_name,p_value) - } - } - - __Get(p_name){ - if(VMR.BusStrip.initiated && this.BUS_STRIP_ID){ - switch p_name { - case "gain","limit": - return Format("{:.1f}",this.getParameter(p_name)) - case "device": - return this.getParameter("device.name") - } - return this.getParameter(p_name) - } - } - - __New(p_type){ - this.BUS_STRIP_TYPE := p_type - this.level := Array() - this.LEVEL_INDEX := Array() - this.gain_limit:= 12.0 - if (p_type="Strip") { - this.BUS_STRIP_INDEX := VMR.BusStrip.STRIP_COUNT++ - loop % this.isPhysical() ? 2 : 8 - this.LEVEL_INDEX.Push(VMR.BusStrip.STRIP_LEVEL_COUNT++) - }else{ - this.BUS_STRIP_INDEX := VMR.BusStrip.BUS_COUNT++ - loop 8 - this.LEVEL_INDEX.Push(VMR.BusStrip.BUS_LEVEL_COUNT++) - } - this.BUS_STRIP_ID := this.BUS_STRIP_TYPE . "[" . this.BUS_STRIP_INDEX . "]" - this.name := VMR.BusStrip.BUS_STRIP_NAMES[VBVMR.VM_TYPE][this.BUS_STRIP_TYPE][this.BUS_STRIP_INDEX+1] - } - - getGainPercentage(){ - return Format("{:.2f}",this.getPercentage(this.gain)) - } - - setGainPercentage(percentage){ - return this.gain := this.getdB(percentage) - } - - getPercentage(dB){ - min_s := 10**(-60/20), max_s := 10**(0/20) - return ((10**(dB/20))-min_s)/(max_s-min_s)*100 - } - - getdB(percentage){ - min_s := 10**(-60/20), max_s := 10**(0/20) - return 20*Log(min_s + percentage/100*(max_s-min_s)) - } - - setParameter(parameter, value){ - local func - if parameter contains device,FadeTo,Label,FadeBy,AppGain,AppMute - func:= "setParameterString" - else - func:= "setParameterFloat" - return (VBVMR)[func](this.BUS_STRIP_ID, parameter, value) - } - - getParameter(parameter){ - local func - if parameter contains device,FadeTo,Label,FadeBy,AppGain,AppMute - func:= "getParameterString" - else - func:= "getParameterFloat" - return (VBVMR)[func](this.BUS_STRIP_ID, parameter) - } - - ; Returns 1 if the bus/strip is a physical one (Hardware bus/strip), 0 otherwise - isPhysical(){ - Switch VBVMR.VM_TYPE { - case 1: - if(this.BUS_STRIP_TYPE = "Strip") - return this.BUS_STRIP_INDEX < 2 - else - return 1 - case 2: - return this.BUS_STRIP_INDEX < 3 - case 3: - return this.BUS_STRIP_INDEX < 5 - } - } - - __setDevice(device){ - if (!this.isPhysical()) - return -4 - if device.driver not in wdm,mme,ks,asio - return -5 - return this.setParameter("device." . device.driver,device.name) - } - - __getDeviceObj(substring,driver){ - local devices:= VMR.BusStrip[this.BUS_STRIP_TYPE . "Devices"] - for i in devices - if (devices[i].driver = driver && InStr(devices[i].name, substring)) - return devices[i] - return {name:"",driver:"wdm"} - } - - __updateLevel(){ - local type := this.BUS_STRIP_TYPE="Bus" ? 3 : 1 - loop % this.LEVEL_INDEX.Length() { - level := VBVMR.GetLevel(type, this.LEVEL_INDEX[A_Index]) - this.level[A_Index] := Max(Round(20 * Log(level)), -999) - } - } -} \ No newline at end of file diff --git a/src/Command.ahk b/src/Command.ahk deleted file mode 100644 index 314283d..0000000 --- a/src/Command.ahk +++ /dev/null @@ -1,59 +0,0 @@ -; These are read-only commands -class Command { - - restart(){ - return VBVMR.SetParameterFloat("Command","Restart",1) - } - - shutdown(){ - return VBVMR.SetParameterFloat("Command","Shutdown",1) - } - - show(open := 1){ - return VBVMR.SetParameterFloat("Command","Show",open) - } - - lock(state := 1){ - return VBVMR.SetParameterFloat("Command","Lock",state) - } - - eject(){ - return VBVMR.SetParameterFloat("Command","Eject",1) - } - - reset(){ - return VBVMR.SetParameterFloat("Command","Reset",1) - } - - save(filePath){ - return VBVMR.SetParameterString("Command","Save",filePath) - } - - load(filePath){ - return VBVMR.SetParameterString("Command","Load",filePath) - } - - showVBANChat(show := 1) { - return VBVMR.SetParameterFloat("Command","dialogshow.VBANCHAT",show) - } - - state(buttonNum, newState) { - return VBVMR.SetParameterFloat("Command.Button[" . buttonNum . "]", "State", newState) - } - - stateOnly(buttonNum, newState) { - return VBVMR.SetParameterFloat("Command.Button[" . buttonNum . "]", "stateOnly", newState) - } - - trigger(buttonNum, newState) { - return VBVMR.SetParameterFloat("Command.Button[" . buttonNum . "]", "trigger", newState) - } - - saveBusEQ(busIndex, filePath) { - return VBVMR.SetParameterFloat("Command","SaveBUSEQ[" busIndex "]", filePath) - } - - loadBusEQ(busIndex, filePath) { - return VBVMR.SetParameterFloat("Command","LoadBUSEQ[" busIndex "]", filePath) - } -} \ No newline at end of file diff --git a/src/Fx.ahk b/src/Fx.ahk deleted file mode 100644 index 9cb9260..0000000 --- a/src/Fx.ahk +++ /dev/null @@ -1,22 +0,0 @@ -class FXBase { - - reverb(onOff := -2) { - switch (onOff) { - case -2: ;getParam - return VBVMR.GetParameterFloat("Fx.Reverb", "on") - case -1: ;invert state - onOff := !VBVMR.GetParameterFloat("Fx.Reverb", "on") - } - return VBVMR.SetParameterFloat("Fx.Reverb","on", onOff) - } - - delay(onOff := -2) { - switch (onOff) { - case -2: ;getParam - return VBVMR.GetParameterFloat("Fx.delay", "on") - case -1: ;invert state - onOff := !VBVMR.GetParameterFloat("Fx.delay", "on") - } - return VBVMR.SetParameterFloat("Fx.delay","on", onOff) - } -} \ No newline at end of file diff --git a/src/MacroButton.ahk b/src/MacroButton.ahk deleted file mode 100644 index 828bc55..0000000 --- a/src/MacroButton.ahk +++ /dev/null @@ -1,24 +0,0 @@ -class MacroButton { - - run(){ - Run, % VBVMR.DLL_PATH "\VoicemeeterMacroButtons.exe", % VBVMR.DLL_PATH, UseErrorLevel - if(ErrorLevel) - Throw, Exception("Could not run MacroButtons") - } - - show(state := 1){ - if(state) - WinShow, ahk_exe VoicemeeterMacroButtons.exe - else - WinHide, ahk_exe VoicemeeterMacroButtons.exe - } - - setStatus(buttonIndex, newStatus, bitmode:=0) { - return VBVMR.MacroButton_SetStatus(buttonIndex, newStatus, bitmode) - } - - getStatus(buttonIndex, bitmode:=0){ - return VBVMR.MacroButton_GetStatus(buttonIndex, bitmode) - } - -} diff --git a/src/Option.ahk b/src/Option.ahk deleted file mode 100644 index 4ee033d..0000000 --- a/src/Option.ahk +++ /dev/null @@ -1,22 +0,0 @@ -class OptionBase { - __Set(p_name, p_value){ - return VBVMR.SetParameterFloat("Option", p_name, p_value) - } - - __Get(p_name){ - return VBVMR.GetParameterFloat("Option", p_name) - } - - delay(busNum, p_delay := "") { - ; in keeping with the 1 indexed class... - busNum := busNum - 1 - if(p_delay == "") { - ; get the value - return VBVMR.GetParameterFloat("Option", "delay[" . busNum . "]") - } - else { - ; set it to a new value - return VBVMR.SetParameterFloat("Option", "delay[" . busNum . "]", Min(Max(p_delay,0),500)) - } - } -} \ No newline at end of file diff --git a/src/Patch.ahk b/src/Patch.ahk deleted file mode 100644 index 7125f04..0000000 --- a/src/Patch.ahk +++ /dev/null @@ -1,9 +0,0 @@ -class PatchBase { - __Set(p_name, p_value){ - return VBVMR.SetParameterFloat("Patch", p_name, p_value) - } - - __Get(p_name){ - return VBVMR.GetParameterFloat("Patch", p_name) - } -} \ No newline at end of file diff --git a/src/Recorder.ahk b/src/Recorder.ahk deleted file mode 100644 index 64a4a2b..0000000 --- a/src/Recorder.ahk +++ /dev/null @@ -1,44 +0,0 @@ -class RecorderBase { - __Set(p_name,p_value){ - local type:= "Float" - if p_name contains load - type:= "String" - return (VBVMR)["SetParameter" type]("Recorder", p_name, p_value) - } - - __Get(p_name){ - local type:= "Float" - if p_name contains load - type:= "String" - return (VBVMR)["GetParameter" type]("Recorder", p_name) - } - - ArmBus(bus, set:=-1){ - if(set > -1){ - VBVMR.SetParameterFloat("Recorder","mode.recbus", 1) - VBVMR.SetParameterFloat("Recorder","ArmBus(" (bus-1) ")", set) - }else{ - return VBVMR.GetParameterFloat("Recorder","ArmBus(" (bus-1) ")") - } - } - - ArmStrips(strip*){ - loop { - Try - this.armStrip(A_Index,0) - Catch - Break - } - for i in strip - Try this.armStrip(strip[i],1) - } - - ArmStrip(strip, set:=-1){ - if(set > -1){ - VBVMR.SetParameterFloat("Recorder","mode.recbus", 0) - VBVMR.SetParameterFloat("Recorder","ArmStrip(" . (strip-1) . ")", set) - }else{ - return VBVMR.GetParameterFloat("Recorder","ArmStrip(" (strip-1) ")") - } - } -} \ No newline at end of file diff --git a/src/VBAN.ahk b/src/VBAN.ahk deleted file mode 100644 index 10cdaea..0000000 --- a/src/VBAN.ahk +++ /dev/null @@ -1,44 +0,0 @@ -class VBAN { - static instream:="" - , outstream:="" - - enable{ - set{ - return VBVMR.SetParameterFloat("vban", "Enable", value) - } - get{ - return VBVMR.GetParameterFloat("vban", "Enable") - } - } - - init(){ - VMR.VBAN.instream:= Array() - VMR.VBAN.outstream:= Array() - loop % VBVMR.VBANINCOUNT - VMR.VBAN.instream.Push(new VMR.VBAN.Stream("in", A_Index)) - loop % VBVMR.VBANOUTCOUNT - VMR.VBAN.outstream.Push(new VMR.VBAN.Stream("out", A_Index)) - } - - class Stream{ - static initiated:= 0 - __New(p_type,p_index){ - this.PARAM_PREFIX:= Format("vban.{}stream[{}]", p_type, p_index) - } - __Set(p_name,p_value){ - if(VMR.VBAN.stream.initiated) { - if p_name contains name, ip - return VBVMR.SetParameterString(this.PARAM_PREFIX, p_name, p_value) - return VBVMR.SetParameterFloat(this.PARAM_PREFIX, p_name, p_value) - } - - } - __Get(p_name){ - if(VMR.VBAN.stream.initiated){ - if p_name contains name, ip - return VBVMR.GetParameterString(this.PARAM_PREFIX, p_name) - return VBVMR.GetParameterFloat(this.PARAM_PREFIX, p_name) - } - } - } -} \ No newline at end of file diff --git a/src/VBVMR.ahk b/src/VBVMR.ahk index 101ea8c..9eccd0b 100644 --- a/src/VBVMR.ahk +++ b/src/VBVMR.ahk @@ -1,205 +1,516 @@ -class VBVMR { - static DLL - , DLL_PATH - , DLL_FILE - , VM_TYPE - , BUSCOUNT - , STRIPCOUNT - , STR_TYPE - , VBANINCOUNT - , VBANOUTCOUNT - , FUNC_ADDR:={ Login:0 - , Logout:0 - , SetParameterFloat:0 - , SetParameterStringW:0 - , SetParameterStringA:0 - , GetParameterFloat:0 - , GetParameterStringW:0 - , GetParameterStringA:0 - , GetVoicemeeterType:0 - , GetLevel:0 - , Output_GetDeviceNumber:0 - , Output_GetDeviceDescW:0 - , Output_GetDeviceDescA:0 - , Input_GetDeviceNumber:0 - , Input_GetDeviceDescW:0 - , Input_GetDeviceDescA:0 - , IsParametersDirty:0 - , MacroButton_IsDirty:0 - , MacroButton_GetStatus:0 - , MacroButton_SetStatus:0 - , GetMidiMessage:0 - , SetParameters:0 - , SetParametersW:0} - - Login(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.Login) - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Login returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - return errLevel - } - - Logout(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.Logout) - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Logout returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - return errLevel - } - - SetParameterFloat(p_prefix, p_parameter, p_value){ - errLevel := DllCall(VBVMR.FUNC_ADDR.SetParameterFloat, "AStr" , p_prefix . "." . p_parameter , "Float" , p_value, "Int") - if (errLevel<0) - Throw, Exception(Format("`nVBVMR_SetParameterFloat returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - return p_value +#Requires AutoHotkey >=2.0 + +#Include VMRError.ahk +#Include VMRConsts.ahk +#Include VMRDevice.ahk + +/** + * 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 := "" - SetParameterString(p_prefix, p_parameter, p_value){ - errLevel := DllCall(VBVMR.FUNC_ADDR["SetParameterString" . VBVMR.STR_TYPE], "AStr", p_prefix . "." . p_parameter , VBVMR.STR_TYPE . "Str" , p_value , "Int") - if (errLevel<0) - Throw, Exception(Format("`nVBVMR_SetParameterString returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - return p_value + /** + * 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") + } } - GetParameterFloat(p_prefix, p_parameter){ - local value - VarSetCapacity(value, 4) - errLevel := DllCall(VBVMR.FUNC_ADDR.GetParameterFloat, "AStr" , p_prefix . "." . p_parameter , "Ptr" , &value, "Int") - if (errLevel<0) - Throw, Exception(Format("`nVBVMR_GetParameterFloat returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - value := NumGet(&value, 0, "Float") - return value + /** + * @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 } - GetParameterString(p_prefix, p_parameter){ - local value - VarSetCapacity(value, A_IsUnicode? 1024 : 512) - errLevel := DllCall(VBVMR.FUNC_ADDR["GetParameterString" . VBVMR.STR_TYPE], "AStr" , p_prefix . "." . p_parameter , "Ptr" , &value , "Int") - if (errLevel<0) - Throw, Exception(Format("`nVBVMR_GetParameterString returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - value := StrGet(&value,512) + /** + * 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 } - GetLevel(p_type, p_channel){ - local level - VarSetCapacity(level,4) - errLevel := DllCall(VBVMR.FUNC_ADDR.GetLevel, "Int", p_type, "Int", p_channel, "Ptr", &level) - if(errLevel<0){ - SetTimer,, Off - Throw, Exception(Format("`nVBVMR_GetLevel returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - } - level := NumGet(&level, 0, "Float") - return level - } - - GetVoicemeeterType(){ - local vtype - VarSetCapacity(vtype, 4) - errLevel := DllCall(VBVMR.FUNC_ADDR.GetVoicemeeterType, "Ptr", &vtype, "Int") - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_GetVoicemeeterType returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - vtype:= NumGet(&vtype, 0, "Int") - return vtype - } - - Output_GetDeviceNumber(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.Output_GetDeviceNumber,"Int") - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Output_GetDeviceNumber returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - else - return errLevel - } - - Output_GetDeviceDesc(p_index){ - local name, driver, device := {} - VarSetCapacity(name, A_IsUnicode? 1024 : 512) - VarSetCapacity(driver, 4) - errLevel := DllCall(VBVMR.FUNC_ADDR["Output_GetDeviceDesc" . VBVMR.STR_TYPE], "Int", p_index, "Ptr" , &driver , "Ptr", &name, "Ptr", 0, "Int") - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Output_GetDeviceDesc returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - driver := NumGet(&driver, 0, "UInt") - name := StrGet(&name,512) - device.name := name - device.driver := (driver=3 ? "wdm" : (driver=4 ? "ks" : (driver=5 ? "asio" : "mme"))) - return device - } - - Input_GetDeviceNumber(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.Input_GetDeviceNumber,"Int") - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Input_GetDeviceNumber returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - else - return errLevel - } - - Input_GetDeviceDesc(p_index){ - local name, driver, device := {} - VarSetCapacity(name, A_IsUnicode? 1024 : 512) - VarSetCapacity(driver, 4) - errLevel := DllCall(VBVMR.FUNC_ADDR["Input_GetDeviceDesc" . VBVMR.STR_TYPE], "Int", p_index, "Ptr" , &driver , "Ptr", &name, "Ptr", 0, "Int") - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_Input_GetDeviceDesc returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - driver := NumGet(&driver, 0, "UInt") - name := StrGet(&name,512) - device.name := name - device.driver := (driver=3 ? "wdm" : (driver=4 ? "ks" : (driver=5 ? "asio" : "mme"))) - return device - } - - IsParametersDirty(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.IsParametersDirty) - if(errLevel<0) - Throw, Exception(Format("`nVBVMR_IsParametersDirty returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - else - return errLevel - } - - MacroButton_GetStatus(nuLogicalButton, bitMode){ - local pValue - VarSetCapacity(pValue, 4) - errLevel := DllCall(VBVMR.FUNC_ADDR.MacroButton_GetStatus, "Int" , nuLogicalButton , "Ptr", &pValue, "Int", bitMode, "Int") - if (errLevel<0) - Throw, Exception("VBVMR_MacroButton_GetStatus returned " . errLevel . "`n DllCall returned " . ErrorLevel, -1) - pValue := NumGet(&pValue, 0, "Float") - return pValue - } - - MacroButton_SetStatus(nuLogicalButton, fValue, bitMode){ - errLevel := DllCall(VBVMR.FUNC_ADDR.MacroButton_SetStatus, "Int" , nuLogicalButton , "Float" , fValue, "Int", bitMode, "Int") - if (errLevel<0) - Throw, Exception("VBVMR_MacroButton_SetStatus returned " . errLevel, -1) - return fValue - } - - MacroButton_IsDirty(){ - errLevel := DllCall(VBVMR.FUNC_ADDR.MacroButton_IsDirty) - if(errLevel<0) - Throw, Exception("VBVMR_MacroButton_IsParametersDirty returned " . errLevel, -1) - else - return errLevel - } - - GetMidiMessage(){ - local nBytes:= 1024, dBuffer:="", tempArr:= Array() - VarSetCapacity(dBuffer, nBytes) - errLevel := DllCall(VBVMR.FUNC_ADDR.GetMidiMessage, "Ptr", &dBuffer, "Int", nBytes) - if errLevel between -1 and -2 - Throw, Exception("VBVMR_GetMidiMessage returned " . errLevel, -1) - loop %errLevel% { - tempArr[A_Index]:= Format("0x{:X}",NumGet(&dBuffer, A_Index - 1, "UChar")) - } - return tempArr.Length()? tempArr : "" + /** + * 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") } - SetParameters(script){ - errLevel := DllCall(VBVMR.FUNC_ADDR["SetParameters" . VBVMR.STR_TYPE], VBVMR.STR_TYPE . "Str" , script , "Int") - if (errLevel<0) - Throw, Exception(Format("`nVBVMR_SetParameters returned {}`nDllCall returned {}", errLevel, ErrorLevel)) - return errLevel + /** + * 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) } - __getAddresses(){ - for fName in VBVMR.FUNC_ADDR - (VBVMR.FUNC_ADDR)[fName]:= DllCall("GetProcAddress", "Ptr", VBVMR.DLL, "AStr", "VBVMR_" . fName, "Ptr") + /** + * 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 } -} \ No newline at end of file +} diff --git a/src/VMR.ahk b/src/VMR.ahk index 60608a5..ed5582b 100644 --- a/src/VMR.ahk +++ b/src/VMR.ahk @@ -1,226 +1,486 @@ -; VMR-header -class VMR{ - bus:="" - , strip:="" - , recorder:="" - , option:="" - , patch:="" - , fx:="" - , onUpdateLevels:="" - , onUpdateParameters:="" - , onUpdateMacrobuttons:="" - , onMidiMessage:="" - - __New(p_path:=""){ - VBVMR.DLL_PATH := p_path? p_path : this.__getDLLPath() - VBVMR.DLL_FILE := A_PtrSize = 8 ? "VoicemeeterRemote64.dll" : "VoicemeeterRemote.dll" - if(!FileExist(VBVMR.DLL_PATH . "\" . VBVMR.DLL_FILE)) - Throw, Exception(Format("Voicemeeter is not installed in the path :`n{}", VBVMR.DLL_PATH)) - VBVMR.STR_TYPE := A_IsUnicode? "W" : "A" - VBVMR.DLL := DllCall("LoadLibrary", "Str", VBVMR.DLL_PATH . "\" . VBVMR.DLL_FILE, "Ptr") - VBVMR.__getAddresses() +/** + * VMR.ahk - A wrapper for Voicemeeter's Remote API + * - Version + * - Build timestamp + * - Repository: {@link https://github.com/SaifAqqad/VMR.ahk GitHub} + * - Documentation: {@link https://saifaqqad.github.io/VMR.ahk VMR Docs} + */ + +#Requires AutoHotkey >=2.0 + +#Include VMRUtils.ahk +#Include VMRError.ahk +#Include VMRConsts.ahk +#Include VMRDevice.ahk +#Include VBVMR.ahk +#Include VMRAudioIO.ahk +#Include VMRBus.ahk +#Include VMRStrip.ahk +#Include VMRCommands.ahk +#Include VMRControllerBase.ahk +#Include VMRMacroButton.ahk +#Include VMRRecorder.ahk +#Include VMRVBAN.ahk + +/** + * 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] := [] } - login(){ - if(VBVMR.Login()){ - this.runVoicemeeter() - WinWait, ahk_class VBCABLE0Voicemeeter0MainWindow0 - sleep, 2000 - } - OnExit(ObjBindMethod(this, "__onExit")) - syncWithDLL := ObjBindMethod(this, "__syncWithDLL") - SetTimer, %syncWithDLL%, 20 - this.getType() - this.__init_arrays() - this.__init_obj() - this.__syncWithDLL() + /** + * 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 } - - - getType(){ - if(!VBVMR.VM_TYPE){ - VBVMR.VM_TYPE:= VBVMR.GetVoicemeeterType() - Switch VBVMR.VM_TYPE { - case 1: - VBVMR.BUSCOUNT:= 2 - VBVMR.STRIPCOUNT:= 3 - VBVMR.VBANINCOUNT:= 4 - VBVMR.VBANOUTCOUNT:= 4 - case 2: - VBVMR.BUSCOUNT:= 5 - VBVMR.STRIPCOUNT:= 5 - VBVMR.VBANINCOUNT:= 8 - VBVMR.VBANOUTCOUNT:= 8 - case 3: - VBVMR.BUSCOUNT:= 8 - VBVMR.STRIPCOUNT:= 8 - VBVMR.VBANINCOUNT:= 8 - VBVMR.VBANOUTCOUNT:= 8 - } + + /** + * 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 } - return VBVMR.VM_TYPE - } - runVoicemeeter(p_type := ""){ - if(p_type){ - Run, % VBVMR.DLL_PATH "\" this.__getTypeExecutable(p_type) , % VBVMR.DLL_PATH, UseErrorLevel Hide - }else{ - loop 3 { - Run, % VBVMR.DLL_PATH "\" this.__getTypeExecutable(4-A_Index) , % VBVMR.DLL_PATH, UseErrorLevel Hide - if(!ErrorLevel) - return + local vmTypeCount := VMR.Types.Count + loop vmTypeCount { + try { + vmPID := this.RunVoicemeeter((vmTypeCount + 1) - A_Index) + return vmPID } } - if(ErrorLevel) - Throw, Exception("Could not run Voicemeeter") - } - updateDevices(){ - VMR.BusStrip.BusDevices:= Array() - VMR.BusStrip.StripDevices:= Array() - loop % VBVMR.Output_GetDeviceNumber() - VMR.BusStrip.BusDevices.Push(VBVMR.Output_GetDeviceDesc(A_Index-1)) - loop % VBVMR.Input_GetDeviceNumber() - VMR.BusStrip.StripDevices.Push(VBVMR.Input_GetDeviceDesc(A_Index-1)) + throw VMRError("Failed to launch Voicemeeter", this.RunVoicemeeter.Name) } - getBusDevices(){ - return VMR.BusStrip.BusDevices - } + /** + * 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) - getStripDevices(){ - return VMR.BusStrip.StripDevices + 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) } - exec(script){ - Try errLn:= VBVMR.SetParameters(script) - if(errLn != 0) - Throw, Exception("exec:`nScript error at line " . errLn) - return errLn + /** + * 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 } - __getDLLPath(){ - vmkey := "VB:Voicemeeter {17359A74-1236-5467}" - key := "HKLM\Software{}\Microsoft\Windows\CurrentVersion\Uninstall\{}" - Try RegRead, value, % Format(key, A_Is64bitOS?"\WOW6432Node":"", vmkey), UninstallString - catch { - Throw, Exception("Voicemeeter is not installed") + /** + * 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 } - SplitPath, value,, dir - return dir } - - __init_obj(){ - this.option:= new this.OptionBase - this.vban.init() - this.vban.stream.initiated:=1 - if(this.getType() >= 2){ - this.patch:= new this.PatchBase - this.recorder:= new this.RecorderBase - } - if(this.getType() >= 3) - this.fx := new this.FXBase + + /** + * 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) } - __init_arrays(){ - this.bus:= Array() - this.strip:= Array() - loop % VBVMR.BUSCOUNT { - this.bus.Push(new this.BusStrip("Bus")) + ToString() { + local value := "VMR:`n" + + if (this.Type) { + value .= "Logged into " . this.Type.name . " in (" . VBVMR.DLL_PATH . ")" } - loop % VBVMR.STRIPCOUNT { - this.strip.Push(new this.BusStrip("Strip")) + else { + value .= "Not logged in" } - this.updateDevices() - VMR.BusStrip.initiated:=1 + + return value } - __getTypeExecutable(p_type){ - switch (p_type) { - case 1: return "voicemeeter.exe" - case 2: return "voicemeeterpro.exe" - case 3: return Format("voicemeeter8{}.exe", A_Is64bitOS? "x64":"") + /** + * @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) } - __syncWithDLL(){ - static ignore_msg:=0 - try { - ;sync vmr parameters - isParametersDirty:= VBVMR.IsParametersDirty() - - ;sync macro buttons states - isMacroButtonsDirty:= VBVMR.MacroButton_IsDirty() - - ;sync bus/strip level arrays - loop % VBVMR.BUSCOUNT { - this.bus[A_Index].__updateLevel() - } - loop % VBVMR.STRIPCOUNT { - this.strip[A_Index].__updateLevel() - } + /** + * @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)) + } - ;sync successful - ignore_msg:=0 + this.Strip := Array() + loop this.Type.stripCount { + this.Strip.Push(VMRStrip(A_Index - 1, this.Type.id)) + } - ;level callback - if(this.onUpdateLevels){ - this.onUpdateLevels.Call() - } + if (this.Type.Id > 1) + this.Recorder := VMRRecorder(this.Type) - ;parameter callback - if(isParametersDirty && this.onUpdateParameters){ - this.onUpdateParameters.Call() - } + if (this.Type.Id == 3) + this.Fx := VMRControllerBase("Fx", (*) => false) - ;macrobutton callback - if(isMacroButtonsDirty && this.onUpdateMacrobuttons){ - this.onUpdateMacrobuttons.Call() - } + this.VBAN := VMRVBAN(this.Type) + this.UpdateDevices() + VMRAudioIO.IS_CLASS_INIT := true + } - ;midi callback - if(this.onMidiMessage && midiMessages:= VBVMR.GetMidiMessage()){ - this.onMidiMessage.Call(midiMessages) - } - return isParametersDirty || isMacroButtonsDirty || midiMessages - } catch e { - if(!ignore_msg){ - MsgBox, 52, VMR.ahk, % Format("An error occurred during synchronization: {}`nAttempt to restart VoiceMeeter?", e.Message), 10 - IfMsgBox Yes - this.runVoicemeeter(VBVMR.VM_TYPE) - IfMsgBox, No - ignore_msg:=1 - IfMsgBox, Timeout - ignore_msg:=1 - sleep, 1000 - } + /** + * @private - Internal method + * @description Updates the levels of all buses/strips. + */ + _UpdateLevels() { + local bus, strip + for (bus in this.Bus) { + bus._UpdateLevels() } - } - __onExit(){ - while(this.__syncWithDLL()){ + for (strip in this.Strip) { + strip._UpdateLevels() } - Sleep, 100 ; to make sure all commands are executed before exiting - VBVMR.Logout() - DllCall("FreeLibrary", "Ptr", VBVMR.DLL) + + this._DispatchEvent(VMRConsts.Events.LevelsUpdated) } - - ; VMR-include './BusStrip.ahk' - ; VMR-include './Fx.ahk' + /** + * @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 := "" - ; VMR-include './Command.ahk' - - ; VMR-include './VBAN.ahk' + if (this._syncTimer) + SetTimer(this._syncTimer, 0) - ; VMR-include './MacroButton.ahk' + if (this._levelsTimer) + SetTimer(this._levelsTimer, 0) - ; VMR-include './Patch.ahk' + if (this._updateDevicesCallback) + OnMessage(VMRConsts.WM_DEVICE_CHANGE, this._updateDevicesCallback, 0) - ; VMR-include './Option.ahk' + while (this.Sync()) { + } - ; VMR-include './Recorder.ahk' -} + ; 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 + } -; VMR-include './VBVMR.ahk' \ No newline at end of file + /** + * 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 + } + } + } +} diff --git a/src/VMRAsyncOp.ahk b/src/VMRAsyncOp.ahk new file mode 100644 index 0000000..3511939 --- /dev/null +++ b/src/VMRAsyncOp.ahk @@ -0,0 +1,149 @@ +#Requires AutoHotkey >=2.0 + +#Include VMRError.ahk + +/** + * 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) + } + } + } +} diff --git a/src/VMRAudioIO.ahk b/src/VMRAudioIO.ahk new file mode 100644 index 0000000..c7e9627 --- /dev/null +++ b/src/VMRAudioIO.ahk @@ -0,0 +1,402 @@ +#Requires AutoHotkey >=2.0 + +#Include VBVMR.ahk +#Include VMRError.ahk +#Include VMRUtils.ahk +#Include VMRConsts.ahk +#Include VMR.ahk +#Include VMRAsyncOp.ahk + +/** + * 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 "" + } +} diff --git a/src/VMRBus.ahk b/src/VMRBus.ahk new file mode 100644 index 0000000..c05c152 --- /dev/null +++ b/src/VMRBus.ahk @@ -0,0 +1,77 @@ +#Requires AutoHotkey >=2.0 +#Include VBVMR.ahk +#Include VMRAudioIO.ahk +#Include VMRConsts.ahk +#Include VMRUtils.ahk + +/** + * 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) +} diff --git a/src/VMRCommands.ahk b/src/VMRCommands.ahk new file mode 100644 index 0000000..04b62e3 --- /dev/null +++ b/src/VMRCommands.ahk @@ -0,0 +1,160 @@ +#Requires AutoHotkey >=2.0 +#Include VBVMR.ahk + +/** + * 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 + } +} diff --git a/src/VMRConsts.ahk b/src/VMRConsts.ahk new file mode 100644 index 0000000..c9fa18f --- /dev/null +++ b/src/VMRConsts.ahk @@ -0,0 +1,86 @@ +#Requires AutoHotkey >=2.0 + +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 +} diff --git a/src/VMRControllerBase.ahk b/src/VMRControllerBase.ahk new file mode 100644 index 0000000..d96d171 --- /dev/null +++ b/src/VMRControllerBase.ahk @@ -0,0 +1,97 @@ +#Requires AutoHotkey >=2.0 +#Include VBVMR.ahk +#Include VMRAsyncOp.ahk + +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 + } +} diff --git a/src/VMRDevice.ahk b/src/VMRDevice.ahk new file mode 100644 index 0000000..4bdbf1f --- /dev/null +++ b/src/VMRDevice.ahk @@ -0,0 +1,27 @@ +#Requires AutoHotkey >=2.0 +#Include VMRUtils.ahk + +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 + } +} \ No newline at end of file diff --git a/src/VMRError.ahk b/src/VMRError.ahk new file mode 100644 index 0000000..4fb2158 --- /dev/null +++ b/src/VMRError.ahk @@ -0,0 +1,48 @@ +#Requires AutoHotkey >=2.0 +#Include VMRUtils.ahk + +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 ")" + } + } +} diff --git a/src/VMRMacroButton.ahk b/src/VMRMacroButton.ahk new file mode 100644 index 0000000..18a4077 --- /dev/null +++ b/src/VMRMacroButton.ahk @@ -0,0 +1,62 @@ +#Requires AutoHotkey >=2.0 + +#Include VBVMR.ahk + +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) +} diff --git a/src/VMRRecorder.ahk b/src/VMRRecorder.ahk new file mode 100644 index 0000000..15cf11d --- /dev/null +++ b/src/VMRRecorder.ahk @@ -0,0 +1,77 @@ +#Requires AutoHotkey >=2.0 + +#Include VMRControllerBase.ahk +#Include VMRUtils.ahk + +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) +} diff --git a/src/VMRStrip.ahk b/src/VMRStrip.ahk new file mode 100644 index 0000000..081d18d --- /dev/null +++ b/src/VMRStrip.ahk @@ -0,0 +1,115 @@ +#Requires AutoHotkey >=2.0 + +#Include VBVMR.ahk +#Include VMRAudioIO.ahk +#Include VMRConsts.ahk +#Include VMRUtils.ahk + +/** + * 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) +} diff --git a/src/VMRUtils.ahk b/src/VMRUtils.ahk new file mode 100644 index 0000000..e80aa54 --- /dev/null +++ b/src/VMRUtils.ahk @@ -0,0 +1,100 @@ +#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) + } +} diff --git a/src/VMRVBAN.ahk b/src/VMRVBAN.ahk new file mode 100644 index 0000000..0264e92 --- /dev/null +++ b/src/VMRVBAN.ahk @@ -0,0 +1,43 @@ +#Requires AutoHotkey >=2.0 + +#Include VMRControllerBase.ahk + +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 }) + } +}