diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be3e0f..d10b5b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ Before any major/minor/patch is released all unit tests will be run to verify th ## [Unreleased] These changes have not been added to PSGallery yet +### Added + +- New Remote methods for device enumeration: + - GetInputCount() + - GetOutputCount() + - GetInputDevice($index) + - GetOutputDevice($index) + +- New IODevice property `driver` to get the driver type of the current device (e.g. 'wdm', 'mme', etc.) + +- New IODevice methods to get, set, or clear the current device for a strip or bus: + - Get(): returns a PSObject with properties Driver, Name, HardwareId, and IsOutput + - Set($device): accepts a PSObject with properties Driver, Name, and IsOutput + - Clear() ## [4.1.0] - 2025-12-23 diff --git a/README.md b/README.md index 861374f..f4a6a87 100644 --- a/README.md +++ b/README.md @@ -368,20 +368,32 @@ $vmr.bus[0].FadeBy(-10, 500) The following Strip.device | Bus.device properties are available: - name: string +- driver: string - sr: int - wdm: string - ks: string - mme: string - asio: string +The following Strip.device | Bus.device methods are available: + +- Set($device) : PSObject, where device is a PSObject with properties Driver, Name, and IsOutput +- Get() : PSObject, returns a PSObject with properties Driver, Name, HardwareId, and IsOutput +- Clear() : Clears the currently selected device + for example: ```powershell $vmr.strip[0].device.wdm = "Mic|Line|Instrument 1 (Audient EVO4)" $vmr.bus[0].device.name | Write-Host + +$device = $vmr.strip[3].device.Get() +$vmr.strip[1].device.Set($device) # moves the device selected for strip 4 to strip 2 + +$vmr.bus[2].device.Clear() ``` -name, sr are defined as read only. +name, driver, sr are defined as read only. wdm, ks, mme, asio are defined as write only. asio only defined for Bus[0].Device @@ -793,6 +805,21 @@ Access to lower level polling functions are provided with these functions: - `$vmr.PDirty`: Returns true if a parameter has been updated. - `$vmr.MDirty`: Returns true if a macrobutton has been updated. +Access to lower level device enumeration functions are provided with these functions: + +- `$vmr.GetInputCount()`: Returns the number of available input devices. +- `$vmr.GetOutputCount()`: Returns the number of available output devices. +- `$vmr.GetInputDevice($index)`: Returns a PSObject with properties Driver, Name, HardwareId, and IsOutput for the input device at the given index. +- `$vmr.GetOutputDevice($index)`: Returns a PSObject with properties Driver, Name, HardwareId, and IsOutput for the output device at the given index. + +```powershell +$count = $vmr.GetInputCount() +for ($i = 0; $i -lt $count; $i++) { + $device = $vmr.GetInputDevice($i) + Write-Host "Input Device $i: $($device.Driver) - $($device.Name)" +} +``` + ### Errors - `VMRemoteError`: Base custom error class. diff --git a/lib/Voicemeeter.psm1 b/lib/Voicemeeter.psm1 index d68e388..d132c4e 100644 --- a/lib/Voicemeeter.psm1 +++ b/lib/Voicemeeter.psm1 @@ -75,6 +75,22 @@ class Remote { [void] PDirty() { P_Dirty } [void] MDirty() { M_Dirty } + + [int] GetOutputCount() { + return Device_Count -IS_OUT $true + } + + [int] GetInputCount() { + return Device_Count + } + + [PSObject] GetOutputDevice([int]$index) { + return Device_Desc -INDEX $index -IS_OUT $true + } + + [PSObject] GetInputDevice([int]$index) { + return Device_Desc -INDEX $index + } } class RemoteBasic : Remote { diff --git a/lib/base.ps1 b/lib/base.ps1 index 331c8d5..8f79131 100644 --- a/lib/base.ps1 +++ b/lib/base.ps1 @@ -225,4 +225,59 @@ function Get_Level { throw [CAPIError]::new($retval, 'VBVMR_GetLevel') } [float]$ptr +} + +function Device_Count { + param( + [bool]$IS_OUT = $false + ) + if ($IS_OUT) { + $retval = [int][Voicemeeter.Remote]::VBVMR_Output_GetDeviceNumber() + if ($retval -lt 0) { + throw [CAPIError]::new($retval, 'VBVMR_Output_GetDeviceNumber') + } + } + else { + $retval = [int][Voicemeeter.Remote]::VBVMR_Input_GetDeviceNumber() + if ($retval -lt 0) { + throw [CAPIError]::new($retval, 'VBVMR_Input_GetDeviceNumber') + } + } + $retval +} + +function Device_Desc { + param( + [int]$INDEX, [bool]$IS_OUT = $false + ) + $driver = 0 + $name = [System.Byte[]]::new(512) + $hardwareid = [System.Byte[]]::new(512) + + if ($IS_OUT) { + $retval = [int][Voicemeeter.Remote]::VBVMR_Output_GetDeviceDescA($INDEX, [ref]$driver, $name, $hardwareid) + if ($retval -notin @(0)) { + throw [CAPIError]::new($retval, 'VBVMR_Output_GetDeviceDescA') + } + } + else { + $retval = [int][Voicemeeter.Remote]::VBVMR_Input_GetDeviceDescA($INDEX, [ref]$driver, $name, $hardwareid) + if ($retval -notin @(0)) { + throw [CAPIError]::new($retval, 'VBVMR_Input_GetDeviceDescA') + } + } + + $drivers = @{ + 1 = 'mme' + 3 = 'wdm' + 4 = 'ks' + 5 = 'asio' + } + + [PSCustomObject]@{ + Driver = $drivers[$driver] + Name = [System.Text.Encoding]::ASCII.GetString($name).Trim([char]0) + HardwareId = [System.Text.Encoding]::ASCII.GetString($hardwareid).Trim([char]0) + IsOutput = $IS_OUT + } } \ No newline at end of file diff --git a/lib/binding.ps1 b/lib/binding.ps1 index 201ff26..a5ff579 100644 --- a/lib/binding.ps1 +++ b/lib/binding.ps1 @@ -43,6 +43,15 @@ function Setup_DLL { [DllImport(@"$dll")] public static extern int VBVMR_GetLevel(Int64 mode, Int64 index, ref float ptr); + + [DllImport(@"$dll")] + public static extern int VBVMR_Output_GetDeviceNumber(); + [DllImport(@"$dll")] + public static extern int VBVMR_Input_GetDeviceNumber(); + [DllImport(@"$dll")] + public static extern int VBVMR_Output_GetDeviceDescA(Int64 index, ref int type, byte[] name, byte[] hardwareid); + [DllImport(@"$dll")] + public static extern int VBVMR_Input_GetDeviceDescA(Int64 index, ref int type, byte[] name, byte[] hardwareid); "@ Add-Type -MemberDefinition $Signature -Name Remote -Namespace Voicemeeter -PassThru | Out-Null diff --git a/lib/bus.ps1 b/lib/bus.ps1 index 38e32e4..9584089 100644 --- a/lib/bus.ps1 +++ b/lib/bus.ps1 @@ -97,7 +97,7 @@ class VirtualBus : Bus { } class BusDevice : IODevice { - BusDevice ([int]$index, [Object]$remote) : base ($index, $remote) { + BusDevice ([int]$index, [Object]$remote) : base ($index, $remote, 'Output') { if ($this.index -eq 0) { AddStringMembers -PARAMS @('asio') -WriteOnly } @@ -106,6 +106,14 @@ class BusDevice : IODevice { [string] identifier () { return 'Bus[' + $this.index + '].Device' } + + [int] EnumCount () { + return $this.remote.GetOutputCount() + } + + [PSObject] EnumDevice ([int]$eIndex) { + return $this.remote.GetOutputDevice($eIndex) + } } function Make_Buses ([Object]$remote) { diff --git a/lib/io.ps1 b/lib/io.ps1 index eebe18f..f0c0475 100644 --- a/lib/io.ps1 +++ b/lib/io.ps1 @@ -100,9 +100,123 @@ class EqCell : IRemote { } class IODevice : IRemote { - IODevice ([int]$index, [Object]$remote) : base ($index, $remote) { + [string]$kindOfDevice + [Hashtable]$drivers + + IODevice ([int]$index, [Object]$remote, [string]$kindOfDevice) : base ($index, $remote) { + $this.kindOfDevice = $kindOfDevice + AddStringMembers -WriteOnly -PARAMS @('wdm', 'ks', 'mme') AddStringMembers -ReadOnly -PARAMS @('name') AddIntMembers -ReadOnly -PARAMS @('sr') + + $this.drivers = @{ + '1' = 'mme' + '4' = 'wdm' + '8' = 'ks' + '256' = 'asio' + } } + + [int] EnumCount () { + throw [System.NotImplementedException]::new("$($this.GetType().Name) must override EnumCount()") + } + + [PSObject] EnumDevice ([int]$eIndex) { + throw [System.NotImplementedException]::new("$($this.GetType().Name) must override EnumDevice()") + } + + [PSObject] Get () { + $device = [PSCustomObject]@{ + Driver = $this.driver + Name = $this.name + HardwareId = '' + IsOutput = $this.kindOfDevice -eq 'Output' + } + if (-not [string]::IsNullOrEmpty($device.Name)) { + for ($i = 0; $i -lt $this.EnumCount(); $i++) { + $eDevice = $this.EnumDevice($i) + if ($eDevice.Name -eq $device.Name -and $eDevice.Driver -eq $device.Driver) { + $device = $eDevice + break + } + } + } + return $device + } + + [void] Set ([PSObject]$device) { + $required = 'IsOutput', 'Driver', 'Name' + $missing = $required | Where-Object { $null -eq $device.PSObject.Properties[$_] } + + if ($missing) { + throw [System.ArgumentException]::new(("Invalid device object. Missing member(s): {0}" -f ($missing -join ', ')), 'device') + } + + $expectsOutput = ($this.kindOfDevice -eq 'Output') + if ([bool]$device.IsOutput -ne $expectsOutput) { + throw [System.ArgumentException]::new(("Device direction mismatch. Expected IsOutput={0}." -f $expectsOutput), 'device') + } + + $d = $device.Driver + $n = $device.Name + + if (-not ($d -is [string])) { + throw [System.ArgumentException]::new('Invalid device object. Driver must be a string.', 'device') + } + if (-not ($n -is [string])) { + throw [System.ArgumentException]::new('Invalid device object. Name must be a string.', 'device') + } + + if ($d -eq '' -and $n -eq '') { $this.Clear(); return } + if ($d -notin $this.drivers.Values) { + throw [System.ArgumentOutOfRangeException]::new('device.Driver', $d, 'Invalid device driver provided to Set method.') + } + + $this.Setter($d, $n) + } + + [void] Clear () { + $this.Setter('mme', '') + } + + hidden $_driver = $($this | Add-Member ScriptProperty 'driver' ` + { + if ([string]::IsNullOrEmpty($this.name)) { return '' } + + $type = $null + try { + $tmp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "vmrtmp-$(New-Guid).xml") + $this.remote.Setter('Command.Save', $tmp) + + $timeout = New-TimeSpan -Seconds 2 + $sw = [Diagnostics.Stopwatch]::StartNew() + $line = $null + do { + if (Test-Path $tmp) { + try { + $line = Get-Content $tmp | Select-String -Pattern "<$($this.kindOfDevice)Dev index='$($this.index + 1)'" -List + if ($line) { break } + } + catch {} + } + Start-Sleep -Milliseconds 20 + } while ($sw.elapsed -lt $timeout) + if ($line -and $line.ToString() -match "type='(?\d+)'") { + $type = $matches['type'] + } + } + finally { + if (Test-Path $tmp) { + Remove-Item $tmp -Force + } + } + + if ($type -notin $this.drivers.Keys) { return 'unknown' } + return $this.drivers[$type] + } ` + { + Write-Warning ("ERROR: $($this.identifier()).driver is read only") + } + ) } \ No newline at end of file diff --git a/lib/strip.ps1 b/lib/strip.ps1 index 1ac1528..bbc65b3 100644 --- a/lib/strip.ps1 +++ b/lib/strip.ps1 @@ -155,12 +155,20 @@ class StripEq : IOEq { } class StripDevice : IODevice { - StripDevice ([int]$index, [Object]$remote) : base ($index, $remote) { + StripDevice ([int]$index, [Object]$remote) : base ($index, $remote, 'Input') { } [string] identifier () { return 'Strip[' + $this.index + '].Device' } + + [int] EnumCount () { + return $this.remote.GetInputCount() + } + + [PSObject] EnumDevice ([int]$eIndex) { + return $this.remote.GetInputDevice($eIndex) + } } class VirtualStrip : Strip { diff --git a/tests/higher.Tests.ps1 b/tests/higher.Tests.ps1 index 0d06d77..15d0b0c 100644 --- a/tests/higher.Tests.ps1 +++ b/tests/higher.Tests.ps1 @@ -850,24 +850,47 @@ Describe -Tag 'higher', -TestName 'All Higher Tests' { @{ Index = $phys_in } ) { Context 'Device' -ForEach @( - @{ Value = 'testInput' }, @{ Value = '' } + @{ Driver = 'mme'; Value = 'testMme'; Expected = 'mme' } + @{ Driver = 'wdm'; Value = 'testWdm'; Expected = 'wdm' } + @{ Driver = 'ks'; Value = 'testKs'; Expected = 'ks' } + @{ Driver = 'mme'; Value = ''; Expected = '' } ) { - It "Should set Strip[$index].Device.wdm" { - $vmr.strip[$index].device.wdm = $value + BeforeEach { + $vmr.strip[$index].device.Clear() + Start-Sleep -Milliseconds 800 + } + + It "Should set Strip[$index].Device.$($driver)" { + $vmr.strip[$index].device.name | Should -Be '' + $vmr.strip[$index].device.driver | Should -Be '' + + $vmr.strip[$index].device.$($driver) = $value Start-Sleep -Milliseconds 800 $vmr.strip[$index].device.name | Should -Be $value + $vmr.strip[$index].device.driver | Should -Be $expected } - It "Should set Strip[$index].Device.ks" { - $vmr.strip[$index].device.ks = $value + It "Should set Strip[$index].Device" -ForEach @( + @{ + Clear = [PSCustomObject]@{ Driver = ''; Name = ''; HardwareId = ''; IsOutput = $false } + Device = [PSCustomObject]@{ Driver = $expected; Name = $value; HardwareId = ''; IsOutput = $false } + } + ) { + $initial = $vmr.strip[$index].device.Get() + + $initial.Driver | Should -Be $clear.Driver + $initial.Name | Should -Be $clear.Name + $initial.HardwareId | Should -Be $clear.HardwareId + $initial.IsOutput | Should -Be $clear.IsOutput + + $vmr.strip[$index].device.Set($device) Start-Sleep -Milliseconds 800 - $vmr.strip[$index].device.name | Should -Be $value - } - - It "Should set Strip[$index].Device.mme" { - $vmr.strip[$index].device.mme = $value - Start-Sleep -Milliseconds 800 - $vmr.strip[$index].device.name | Should -Be $value + $result = $vmr.strip[$index].device.Get() + + $result.Driver | Should -Be $device.Driver + $result.Name | Should -Be $device.Name + $result.HardwareId | Should -Be $device.HardwareId + $result.IsOutput | Should -Be $device.IsOutput } } @@ -983,24 +1006,47 @@ Describe -Tag 'higher', -TestName 'All Higher Tests' { @{ Index = $phys_out } ) { Context 'Device' -ForEach @( - @{ Value = 'testOutput' }, @{ Value = '' } + @{ Driver = 'mme'; Value = 'testMme'; Expected = 'mme' } + @{ Driver = 'wdm'; Value = 'testWdm'; Expected = 'wdm' } + @{ Driver = 'ks'; Value = 'testKs'; Expected = 'ks' } + @{ Driver = 'mme'; Value = ''; Expected = '' } ) { - It "Should set Bus[$index].Device.wdm" { - $vmr.bus[$index].device.wdm = $value + BeforeEach { + $vmr.bus[$index].device.Clear() + Start-Sleep -Milliseconds 800 + } + + It "Should set Bus[$index].Device.$($driver)" { + $vmr.bus[$index].device.name | Should -Be '' + $vmr.bus[$index].device.driver | Should -Be '' + + $vmr.bus[$index].device.$($driver) = $value Start-Sleep -Milliseconds 800 $vmr.bus[$index].device.name | Should -Be $value + $vmr.bus[$index].device.driver | Should -Be $expected } - It "Should set Bus[$index].Device.ks" { - $vmr.bus[$index].device.ks = $value + It "Should set Bus[$index].Device" -ForEach @( + @{ + Clear = [PSCustomObject]@{ Driver = ''; Name = ''; HardwareId = ''; IsOutput = $true } + Device = [PSCustomObject]@{ Driver = $expected; Name = $value; HardwareId = ''; IsOutput = $true } + } + ) { + $initial = $vmr.bus[$index].device.Get() + + $initial.Driver | Should -Be $clear.Driver + $initial.Name | Should -Be $clear.Name + $initial.HardwareId | Should -Be $clear.HardwareId + $initial.IsOutput | Should -Be $clear.IsOutput + + $vmr.bus[$index].device.Set($device) Start-Sleep -Milliseconds 800 - $vmr.bus[$index].device.name | Should -Be $value - } - - It "Should set Bus[$index].Device.mme" { - $vmr.bus[$index].device.mme = $value - Start-Sleep -Milliseconds 800 - $vmr.bus[$index].device.name | Should -Be $value + $result = $vmr.bus[$index].device.Get() + + $result.Driver | Should -Be $device.Driver + $result.Name | Should -Be $device.Name + $result.HardwareId | Should -Be $device.HardwareId + $result.IsOutput | Should -Be $device.IsOutput } } } @@ -1009,24 +1055,47 @@ Describe -Tag 'higher', -TestName 'All Higher Tests' { @{ Index = $virt_out } ) { Context 'Device' -Skip:$ifNotBasic -ForEach @( - @{ Value = 'testOutput' }, @{ Value = '' } + @{ Driver = 'mme'; Value = 'testMme'; Expected = 'mme' } + @{ Driver = 'wdm'; Value = 'testWdm'; Expected = 'wdm' } + @{ Driver = 'ks'; Value = 'testKs'; Expected = 'ks' } + @{ Driver = 'mme'; Value = ''; Expected = '' } ) { - It "Should set Bus[$index].Device.wdm" { - $vmr.bus[$index].device.wdm = $value + BeforeEach { + $vmr.bus[$index].device.Clear() + Start-Sleep -Milliseconds 800 + } + + It "Should set Bus[$index].Device.$($driver)" { + $vmr.bus[$index].device.name | Should -Be '' + $vmr.bus[$index].device.driver | Should -Be '' + + $vmr.bus[$index].device.$($driver) = $value Start-Sleep -Milliseconds 800 $vmr.bus[$index].device.name | Should -Be $value + $vmr.bus[$index].device.driver | Should -Be $expected } - It "Should set Bus[$index].Device.ks" { - $vmr.bus[$index].device.ks = $value + It "Should set Bus[$index].Device" -ForEach @( + @{ + Clear = [PSCustomObject]@{ Driver = ''; Name = ''; HardwareId = ''; IsOutput = $true } + Device = [PSCustomObject]@{ Driver = $expected; Name = $value; HardwareId = ''; IsOutput = $true } + } + ) { + $initial = $vmr.bus[$index].device.Get() + + $initial.Driver | Should -Be $clear.Driver + $initial.Name | Should -Be $clear.Name + $initial.HardwareId | Should -Be $clear.HardwareId + $initial.IsOutput | Should -Be $clear.IsOutput + + $vmr.bus[$index].device.Set($device) Start-Sleep -Milliseconds 800 - $vmr.bus[$index].device.name | Should -Be $value - } - - It "Should set Bus[$index].Device.mme" { - $vmr.bus[$index].device.mme = $value - Start-Sleep -Milliseconds 800 - $vmr.bus[$index].device.name | Should -Be $value + $result = $vmr.bus[$index].device.Get() + + $result.Driver | Should -Be $device.Driver + $result.Name | Should -Be $device.Name + $result.HardwareId | Should -Be $device.HardwareId + $result.IsOutput | Should -Be $device.IsOutput } } }