From abd792acd5785ae85e101e34edc6684762a76cd7 Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Sat, 28 Feb 2026 03:13:59 -0500 Subject: [PATCH 01/14] add device enumeration bindings, base functions, and Remote methods implemented initial manual tests for potato pass --- lib/Voicemeeter.psm1 | 16 +++++++++++++ lib/base.ps1 | 55 ++++++++++++++++++++++++++++++++++++++++++++ lib/binding.ps1 | 9 ++++++++ 3 files changed, 80 insertions(+) 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..84a4da5 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 From 2f2d4af8485edcccfd114aedd2ac7c6f19527a32 Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:35:12 -0500 Subject: [PATCH 02/14] IODevice.driver initial pester tests pass for all kinds --- lib/Voicemeeter.psm1 | 4 ++++ lib/bus.ps1 | 2 +- lib/io.ps1 | 50 +++++++++++++++++++++++++++++++++++++++++- lib/strip.ps1 | 2 +- tests/higher.Tests.ps1 | 44 ++++++++++++------------------------- 5 files changed, 69 insertions(+), 33 deletions(-) diff --git a/lib/Voicemeeter.psm1 b/lib/Voicemeeter.psm1 index d132c4e..4d5519f 100644 --- a/lib/Voicemeeter.psm1 +++ b/lib/Voicemeeter.psm1 @@ -20,11 +20,15 @@ class Remote { [String]$vmpath [Hashtable]$kind [Object]$profiles + [String]$userpath + [String]$workingconfig Remote ([String]$kindId) { $this.vmpath = Setup_DLL $this.kind = GetKind($kindId) $this.profiles = Get_Profiles($this.kind.name) + $this.userpath = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'Voicemeeter' + $this.workingconfig = Join-Path $this.userpath ('vm' + $this.kind.name + '_working.xml') } [string] ToString() { diff --git a/lib/bus.ps1 b/lib/bus.ps1 index 38e32e4..787e950 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 } diff --git a/lib/io.ps1 b/lib/io.ps1 index eebe18f..f767900 100644 --- a/lib/io.ps1 +++ b/lib/io.ps1 @@ -100,9 +100,57 @@ class EqCell : IRemote { } class IODevice : IRemote { - IODevice ([int]$index, [Object]$remote) : base ($index, $remote) { + [string]$kindOfDevice + + 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') } + + hidden $_driver = $($this | Add-Member ScriptProperty 'driver' ` + { + $path = $this.remote.workingconfig + $oldTime = if (Test-Path $path) { (Get-Item $path).LastWriteTime } else { [DateTime]::MinValue } + + $this.remote.Setter('Command.Save', $path) + + $timeout = New-TimeSpan -Seconds 2 + $sw = [Diagnostics.Stopwatch]::StartNew() + $line = $null + do { + if (Test-Path $path) { + $newTime = (Get-Item $path).LastWriteTime + if ($newTime -gt $oldTime) { + try { + $line = Get-Content $path | Select-String -Pattern "<$($this.kindOfDevice)Dev index='$($this.index + 1)'" + if ($line) { break } + } + catch {} + } + } + Start-Sleep -Milliseconds 20 + } while ($sw.elapsed -lt $timeout) + + if (-not $line) { return 'unknown' } + + $type = $null + if ($line.ToString() -match "type='(?\d+)'") { + $type = $matches['type'] + } + + switch ($type) { + '1' { return 'mme' } + '4' { return 'wdm' } + '8' { return 'ks' } + '256' { return 'asio' } + default { return 'none' } + } + } ` + { + 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..c7d920e 100644 --- a/lib/strip.ps1 +++ b/lib/strip.ps1 @@ -155,7 +155,7 @@ 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 () { diff --git a/tests/higher.Tests.ps1 b/tests/higher.Tests.ps1 index 0d06d77..dc128d9 100644 --- a/tests/higher.Tests.ps1 +++ b/tests/higher.Tests.ps1 @@ -983,24 +983,16 @@ 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 = 'none' } ) { - It "Should set Bus[$index].Device.wdm" { - $vmr.bus[$index].device.wdm = $value - Start-Sleep -Milliseconds 800 - $vmr.bus[$index].device.name | Should -Be $value - } - - It "Should set Bus[$index].Device.ks" { - $vmr.bus[$index].device.ks = $value - 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 + It "Should set Bus[$index].Device.$($driver)" { + $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 } } } @@ -1009,24 +1001,16 @@ 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 = 'none' } ) { - It "Should set Bus[$index].Device.wdm" { - $vmr.bus[$index].device.wdm = $value - Start-Sleep -Milliseconds 800 - $vmr.bus[$index].device.name | Should -Be $value - } - - It "Should set Bus[$index].Device.ks" { - $vmr.bus[$index].device.ks = $value - 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 + It "Should set Bus[$index].Device.$($driver)" { + $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 } } } From defb2b68c02b2e0e3cc5a5560daddc79ff60da2e Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Mon, 2 Mar 2026 05:24:30 -0500 Subject: [PATCH 03/14] driver type capitalization --- lib/base.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/base.ps1 b/lib/base.ps1 index 84a4da5..0f1c1a5 100644 --- a/lib/base.ps1 +++ b/lib/base.ps1 @@ -268,10 +268,10 @@ function Device_Desc { } $drivers = @{ - 1 = 'MME' - 3 = 'WDM' - 4 = 'KS' - 5 = 'ASIO' + 1 = 'mme' + 3 = 'wdm' + 4 = 'ks' + 5 = 'asio' } [PSCustomObject]@{ From 1f5b52b439b444e7e8bb3c507579796395aa26cf Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Mon, 2 Mar 2026 06:55:55 -0500 Subject: [PATCH 04/14] improve speed of Select-String device config is at the very top of the xml, so this should be much faster --- lib/io.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/io.ps1 b/lib/io.ps1 index f767900..5592c94 100644 --- a/lib/io.ps1 +++ b/lib/io.ps1 @@ -125,7 +125,7 @@ class IODevice : IRemote { $newTime = (Get-Item $path).LastWriteTime if ($newTime -gt $oldTime) { try { - $line = Get-Content $path | Select-String -Pattern "<$($this.kindOfDevice)Dev index='$($this.index + 1)'" + $line = Get-Content $path | Select-String -Pattern "<$($this.kindOfDevice)Dev index='$($this.index + 1)'" -List if ($line) { break } } catch {} From 8d267078ff4e96461e69b33340af0bb44d75fc55 Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:31:30 -0500 Subject: [PATCH 05/14] drivers switch -> hashtable --- lib/io.ps1 | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/io.ps1 b/lib/io.ps1 index 5592c94..52c261c 100644 --- a/lib/io.ps1 +++ b/lib/io.ps1 @@ -101,6 +101,7 @@ class EqCell : IRemote { class IODevice : IRemote { [string]$kindOfDevice + [Hashtable]$drivers IODevice ([int]$index, [Object]$remote, [string]$kindOfDevice) : base ($index, $remote) { $this.kindOfDevice = $kindOfDevice @@ -108,6 +109,13 @@ class IODevice : IRemote { AddStringMembers -WriteOnly -PARAMS @('wdm', 'ks', 'mme') AddStringMembers -ReadOnly -PARAMS @('name') AddIntMembers -ReadOnly -PARAMS @('sr') + + $this.drivers = @{ + '1' = 'mme' + '4' = 'wdm' + '8' = 'ks' + '256' = 'asio' + } } hidden $_driver = $($this | Add-Member ScriptProperty 'driver' ` @@ -141,13 +149,9 @@ class IODevice : IRemote { $type = $matches['type'] } - switch ($type) { - '1' { return 'mme' } - '4' { return 'wdm' } - '8' { return 'ks' } - '256' { return 'asio' } - default { return 'none' } - } + if ($null -eq $type) { return 'none' } + if ($type -notin $this.drivers.Keys) { return 'unknown' } + return $this.drivers[$type] } ` { Write-Warning ("ERROR: $($this.identifier()).driver is read only") From 6b2031de9942fcfc9da02b69be1bd07c21e19cdf Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:54:39 -0500 Subject: [PATCH 06/14] clean up IODevice.driver 'none' -> '' early return if name is empty --- lib/io.ps1 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/io.ps1 b/lib/io.ps1 index 52c261c..3f75ab8 100644 --- a/lib/io.ps1 +++ b/lib/io.ps1 @@ -120,6 +120,8 @@ class IODevice : IRemote { hidden $_driver = $($this | Add-Member ScriptProperty 'driver' ` { + if ([string]::IsNullOrEmpty($this.name)) { return '' } + $path = $this.remote.workingconfig $oldTime = if (Test-Path $path) { (Get-Item $path).LastWriteTime } else { [DateTime]::MinValue } @@ -141,15 +143,12 @@ class IODevice : IRemote { } Start-Sleep -Milliseconds 20 } while ($sw.elapsed -lt $timeout) - - if (-not $line) { return 'unknown' } $type = $null - if ($line.ToString() -match "type='(?\d+)'") { + if ($line -and $line.ToString() -match "type='(?\d+)'") { $type = $matches['type'] } - if ($null -eq $type) { return 'none' } if ($type -notin $this.drivers.Keys) { return 'unknown' } return $this.drivers[$type] } ` From 4ea371af2f44a4a61efb639ef697288cc26ee7f1 Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Wed, 4 Mar 2026 04:08:08 -0500 Subject: [PATCH 07/14] more IODevice.driver tweaks use temp file instead of persistent manual and pester tests pass --- lib/Voicemeeter.psm1 | 4 ---- lib/io.ps1 | 23 ++++++++++++----------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/Voicemeeter.psm1 b/lib/Voicemeeter.psm1 index 4d5519f..d132c4e 100644 --- a/lib/Voicemeeter.psm1 +++ b/lib/Voicemeeter.psm1 @@ -20,15 +20,11 @@ class Remote { [String]$vmpath [Hashtable]$kind [Object]$profiles - [String]$userpath - [String]$workingconfig Remote ([String]$kindId) { $this.vmpath = Setup_DLL $this.kind = GetKind($kindId) $this.profiles = Get_Profiles($this.kind.name) - $this.userpath = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'Voicemeeter' - $this.workingconfig = Join-Path $this.userpath ('vm' + $this.kind.name + '_working.xml') } [string] ToString() { diff --git a/lib/io.ps1 b/lib/io.ps1 index 3f75ab8..49d6f27 100644 --- a/lib/io.ps1 +++ b/lib/io.ps1 @@ -122,31 +122,32 @@ class IODevice : IRemote { { if ([string]::IsNullOrEmpty($this.name)) { return '' } - $path = $this.remote.workingconfig - $oldTime = if (Test-Path $path) { (Get-Item $path).LastWriteTime } else { [DateTime]::MinValue } - - $this.remote.Setter('Command.Save', $path) + $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 $path) { - $newTime = (Get-Item $path).LastWriteTime - if ($newTime -gt $oldTime) { + if (Test-Path $tmp) { try { - $line = Get-Content $path | Select-String -Pattern "<$($this.kindOfDevice)Dev index='$($this.index + 1)'" -List + $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) - - $type = $null 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' } From 7d9615d760eaf242d22f86b88d7f68aeb7eae0fe Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Wed, 4 Mar 2026 04:23:10 -0500 Subject: [PATCH 08/14] Get(), Set($device), Clear() methods added to IODevice manual and pester tests pass for all kinds --- lib/bus.ps1 | 8 +++ lib/io.ps1 | 69 +++++++++++++++++++++---- lib/strip.ps1 | 8 +++ tests/higher.Tests.ps1 | 113 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 174 insertions(+), 24 deletions(-) diff --git a/lib/bus.ps1 b/lib/bus.ps1 index 787e950..9584089 100644 --- a/lib/bus.ps1 +++ b/lib/bus.ps1 @@ -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 49d6f27..912f73e 100644 --- a/lib/io.ps1 +++ b/lib/io.ps1 @@ -118,30 +118,79 @@ class IODevice : IRemote { } } + [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) { + $v = $device.IsOutput -eq ($this.kindOfDevice -eq 'Output') + $d = $device.Driver + $n = $device.Name + + if ($v -and $d -is [string] -and $n -is [string]) { + if ($d -eq '' -and $n -eq '') { + $this.Clear() + return + } + if ($d -in $this.drivers.Values) { + $this.Setter($d, $n) + return + } + } + Write-Warning "Invalid device object provided to Set method." + } + + [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 { + $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'] + } + Start-Sleep -Milliseconds 20 + } while ($sw.elapsed -lt $timeout) + if ($line -and $line.ToString() -match "type='(?\d+)'") { + $type = $matches['type'] } } finally { diff --git a/lib/strip.ps1 b/lib/strip.ps1 index c7d920e..bbc65b3 100644 --- a/lib/strip.ps1 +++ b/lib/strip.ps1 @@ -161,6 +161,14 @@ class StripDevice : IODevice { [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 dc128d9..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 } } @@ -986,14 +1009,45 @@ Describe -Tag 'higher', -TestName 'All Higher Tests' { @{ Driver = 'mme'; Value = 'testMme'; Expected = 'mme' } @{ Driver = 'wdm'; Value = 'testWdm'; Expected = 'wdm' } @{ Driver = 'ks'; Value = 'testKs'; Expected = 'ks' } - @{ Driver = 'mme'; Value = ''; Expected = 'none' } + @{ Driver = 'mme'; Value = ''; Expected = '' } ) { + 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" -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 + $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 + } } } @@ -1004,14 +1058,45 @@ Describe -Tag 'higher', -TestName 'All Higher Tests' { @{ Driver = 'mme'; Value = 'testMme'; Expected = 'mme' } @{ Driver = 'wdm'; Value = 'testWdm'; Expected = 'wdm' } @{ Driver = 'ks'; Value = 'testKs'; Expected = 'ks' } - @{ Driver = 'mme'; Value = ''; Expected = 'none' } + @{ Driver = 'mme'; Value = ''; Expected = '' } ) { + 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" -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 + $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 + } } } From 55ade960f2f3dc135f5e819b675b994ae176c899 Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:02:18 -0500 Subject: [PATCH 09/14] update docs --- CHANGELOG.md | 14 ++++++++++++++ README.md | 29 ++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be3e0f..c5bdd2e 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..96ba1c0 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 and Name +- 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. From 33dcc98c8fc5c03063329935d25538fb4af94819 Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:39:57 -0500 Subject: [PATCH 10/14] small docs corrections --- CHANGELOG.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5bdd2e..d10b5b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Before any major/minor/patch is released all unit tests will be run to verify th - GetInputDevice($index) - GetOutputDevice($index) -- New IODevice property `driver` to get the driver type of the current device (e.g. 'WDM', 'MME', etc.) +- 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 diff --git a/README.md b/README.md index 96ba1c0..f4a6a87 100644 --- a/README.md +++ b/README.md @@ -377,7 +377,7 @@ The following Strip.device | Bus.device properties are available: The following Strip.device | Bus.device methods are available: -- Set($device) : PSObject, where device is a PSObject with properties Driver and Name +- 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 From ed3b7be9044746aa0d8c393e0f9b32925af15f6b Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:43:48 -0500 Subject: [PATCH 11/14] HardwareId changed capitalization for consistency --- lib/base.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/base.ps1 b/lib/base.ps1 index 0f1c1a5..8f79131 100644 --- a/lib/base.ps1 +++ b/lib/base.ps1 @@ -277,7 +277,7 @@ function Device_Desc { [PSCustomObject]@{ Driver = $drivers[$driver] Name = [System.Text.Encoding]::ASCII.GetString($name).Trim([char]0) - HardwareID = [System.Text.Encoding]::ASCII.GetString($hardwareid).Trim([char]0) + HardwareId = [System.Text.Encoding]::ASCII.GetString($hardwareid).Trim([char]0) IsOutput = $IS_OUT } } \ No newline at end of file From d1dfe2de52542025a0bb677d139146b654be273e Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:15:46 -0500 Subject: [PATCH 12/14] revert to user folder should be faster this way, and it wasn't actually causing the problems i thought it was causing pester tests pass for all kinds --- lib/Voicemeeter.psm1 | 4 ++++ lib/io.ps1 | 38 ++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/lib/Voicemeeter.psm1 b/lib/Voicemeeter.psm1 index d132c4e..4d5519f 100644 --- a/lib/Voicemeeter.psm1 +++ b/lib/Voicemeeter.psm1 @@ -20,11 +20,15 @@ class Remote { [String]$vmpath [Hashtable]$kind [Object]$profiles + [String]$userpath + [String]$workingconfig Remote ([String]$kindId) { $this.vmpath = Setup_DLL $this.kind = GetKind($kindId) $this.profiles = Get_Profiles($this.kind.name) + $this.userpath = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'Voicemeeter' + $this.workingconfig = Join-Path $this.userpath ('vm' + $this.kind.name + '_working.xml') } [string] ToString() { diff --git a/lib/io.ps1 b/lib/io.ps1 index 912f73e..23dfc4a 100644 --- a/lib/io.ps1 +++ b/lib/io.ps1 @@ -170,33 +170,31 @@ class IODevice : IRemote { 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) { + $path = $this.remote.workingconfig + $oldTime = if (Test-Path $path) { (Get-Item $path).LastWriteTime } else { [DateTime]::MinValue } + $this.remote.Setter('Command.Save', $path) + + $timeout = New-TimeSpan -Seconds 2 + $sw = [Diagnostics.Stopwatch]::StartNew() + $line = $null + do { + if (Test-Path $path) { + $newTime = (Get-Item $path).LastWriteTime + if ($newTime -gt $oldTime) { try { - $line = Get-Content $tmp | Select-String -Pattern "<$($this.kindOfDevice)Dev index='$($this.index + 1)'" -List + $line = Get-Content $path | 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 } + Start-Sleep -Milliseconds 20 + } while ($sw.elapsed -lt $timeout) + + $type = $null + if ($line -and $line.ToString() -match "type='(?\d+)'") { + $type = $matches['type'] } if ($type -notin $this.drivers.Keys) { return 'unknown' } From 91e798caa12146b38dc4ec0a1e907f7e2eb3b97d Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:32:11 -0400 Subject: [PATCH 13/14] Revert "revert to user folder" This reverts commit d1dfe2de52542025a0bb677d139146b654be273e. --- lib/Voicemeeter.psm1 | 4 ---- lib/io.ps1 | 38 ++++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/lib/Voicemeeter.psm1 b/lib/Voicemeeter.psm1 index 4d5519f..d132c4e 100644 --- a/lib/Voicemeeter.psm1 +++ b/lib/Voicemeeter.psm1 @@ -20,15 +20,11 @@ class Remote { [String]$vmpath [Hashtable]$kind [Object]$profiles - [String]$userpath - [String]$workingconfig Remote ([String]$kindId) { $this.vmpath = Setup_DLL $this.kind = GetKind($kindId) $this.profiles = Get_Profiles($this.kind.name) - $this.userpath = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'Voicemeeter' - $this.workingconfig = Join-Path $this.userpath ('vm' + $this.kind.name + '_working.xml') } [string] ToString() { diff --git a/lib/io.ps1 b/lib/io.ps1 index 23dfc4a..912f73e 100644 --- a/lib/io.ps1 +++ b/lib/io.ps1 @@ -170,31 +170,33 @@ class IODevice : IRemote { 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) - $path = $this.remote.workingconfig - $oldTime = if (Test-Path $path) { (Get-Item $path).LastWriteTime } else { [DateTime]::MinValue } - $this.remote.Setter('Command.Save', $path) - - $timeout = New-TimeSpan -Seconds 2 - $sw = [Diagnostics.Stopwatch]::StartNew() - $line = $null - do { - if (Test-Path $path) { - $newTime = (Get-Item $path).LastWriteTime - if ($newTime -gt $oldTime) { + $timeout = New-TimeSpan -Seconds 2 + $sw = [Diagnostics.Stopwatch]::StartNew() + $line = $null + do { + if (Test-Path $tmp) { try { - $line = Get-Content $path | Select-String -Pattern "<$($this.kindOfDevice)Dev index='$($this.index + 1)'" -List + $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 } - Start-Sleep -Milliseconds 20 - } while ($sw.elapsed -lt $timeout) - - $type = $null - if ($line -and $line.ToString() -match "type='(?\d+)'") { - $type = $matches['type'] } if ($type -notin $this.drivers.Keys) { return 'unknown' } From 68cf0cef37ceefdb718e2e83234a3cf51ed04875 Mon Sep 17 00:00:00 2001 From: pblivingston <71585805+pblivingston@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:52:46 -0400 Subject: [PATCH 14/14] IODevice.Set readability manual and pester tests pass --- lib/io.ps1 | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/io.ps1 b/lib/io.ps1 index 912f73e..f0c0475 100644 --- a/lib/io.ps1 +++ b/lib/io.ps1 @@ -146,21 +146,34 @@ class IODevice : IRemote { } [void] Set ([PSObject]$device) { - $v = $device.IsOutput -eq ($this.kindOfDevice -eq 'Output') + $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 ($v -and $d -is [string] -and $n -is [string]) { - if ($d -eq '' -and $n -eq '') { - $this.Clear() - return - } - if ($d -in $this.drivers.Values) { - $this.Setter($d, $n) - return - } + if (-not ($d -is [string])) { + throw [System.ArgumentException]::new('Invalid device object. Driver must be a string.', 'device') } - Write-Warning "Invalid device object provided to Set method." + 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 () {