16 Commits

Author SHA1 Message Date
f6c750fe56 add 4.2.0 to CHANGELOG 2026-03-15 02:00:05 +00:00
685854c35a Merge pull request #36 from pblivingston/devices-enumerator
Device enumeration
2026-03-15 01:28:27 +00:00
pblivingston
68cf0cef37 IODevice.Set readability
manual and pester tests pass
2026-03-14 19:52:46 -04:00
pblivingston
91e798caa1 Revert "revert to user folder"
This reverts commit d1dfe2de52.
2026-03-14 18:32:11 -04:00
pblivingston
d1dfe2de52 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
2026-03-04 21:15:46 -05:00
pblivingston
ed3b7be904 HardwareId
changed capitalization for consistency
2026-03-04 20:43:48 -05:00
pblivingston
33dcc98c8f small docs corrections 2026-03-04 20:39:57 -05:00
pblivingston
55ade960f2 update docs 2026-03-04 20:02:18 -05:00
pblivingston
7d9615d760 Get(), Set($device), Clear()
methods added to IODevice

manual and pester tests pass for all kinds
2026-03-04 04:23:10 -05:00
pblivingston
4ea371af2f more IODevice.driver tweaks
use temp file instead of persistent

manual and pester tests pass
2026-03-04 04:08:08 -05:00
pblivingston
6b2031de99 clean up IODevice.driver
'none' -> ''
early return if name is empty
2026-03-03 19:54:39 -05:00
pblivingston
8d267078ff drivers
switch -> hashtable
2026-03-02 11:31:30 -05:00
pblivingston
1f5b52b439 improve speed of Select-String
device config is at the very top of the xml, so this should be much faster
2026-03-02 06:55:55 -05:00
pblivingston
defb2b68c0 driver type capitalization 2026-03-02 05:24:30 -05:00
pblivingston
2f2d4af848 IODevice.driver
initial pester tests pass for all kinds
2026-02-28 20:35:12 -05:00
pblivingston
abd792acd5 add device enumeration
bindings, base functions, and Remote methods implemented

initial manual tests for potato pass
2026-02-28 03:13:59 -05:00
9 changed files with 362 additions and 40 deletions

View File

@@ -9,6 +9,22 @@ 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 ## [Unreleased] These changes have not been added to PSGallery yet
## [4.2.0] - 2026-03-15
### 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 ## [4.1.0] - 2025-12-23

View File

@@ -368,20 +368,32 @@ $vmr.bus[0].FadeBy(-10, 500)
The following Strip.device | Bus.device properties are available: The following Strip.device | Bus.device properties are available:
- name: string - name: string
- driver: string
- sr: int - sr: int
- wdm: string - wdm: string
- ks: string - ks: string
- mme: string - mme: string
- asio: 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: for example:
```powershell ```powershell
$vmr.strip[0].device.wdm = "Mic|Line|Instrument 1 (Audient EVO4)" $vmr.strip[0].device.wdm = "Mic|Line|Instrument 1 (Audient EVO4)"
$vmr.bus[0].device.name | Write-Host $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. wdm, ks, mme, asio are defined as write only.
asio only defined for Bus[0].Device 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.PDirty`: Returns true if a parameter has been updated.
- `$vmr.MDirty`: Returns true if a macrobutton 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 ### Errors
- `VMRemoteError`: Base custom error class. - `VMRemoteError`: Base custom error class.

View File

@@ -75,6 +75,22 @@ class Remote {
[void] PDirty() { P_Dirty } [void] PDirty() { P_Dirty }
[void] MDirty() { M_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 { class RemoteBasic : Remote {

View File

@@ -226,3 +226,58 @@ function Get_Level {
} }
[float]$ptr [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
}
}

View File

@@ -43,6 +43,15 @@ function Setup_DLL {
[DllImport(@"$dll")] [DllImport(@"$dll")]
public static extern int VBVMR_GetLevel(Int64 mode, Int64 index, ref float ptr); 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 Add-Type -MemberDefinition $Signature -Name Remote -Namespace Voicemeeter -PassThru | Out-Null

View File

@@ -97,7 +97,7 @@ class VirtualBus : Bus {
} }
class BusDevice : IODevice { 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) { if ($this.index -eq 0) {
AddStringMembers -PARAMS @('asio') -WriteOnly AddStringMembers -PARAMS @('asio') -WriteOnly
} }
@@ -106,6 +106,14 @@ class BusDevice : IODevice {
[string] identifier () { [string] identifier () {
return 'Bus[' + $this.index + '].Device' 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) { function Make_Buses ([Object]$remote) {

View File

@@ -100,9 +100,123 @@ class EqCell : IRemote {
} }
class IODevice : 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 -WriteOnly -PARAMS @('wdm', 'ks', 'mme')
AddStringMembers -ReadOnly -PARAMS @('name') AddStringMembers -ReadOnly -PARAMS @('name')
AddIntMembers -ReadOnly -PARAMS @('sr') 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='(?<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")
}
)
} }

View File

@@ -155,12 +155,20 @@ class StripEq : IOEq {
} }
class StripDevice : IODevice { class StripDevice : IODevice {
StripDevice ([int]$index, [Object]$remote) : base ($index, $remote) { StripDevice ([int]$index, [Object]$remote) : base ($index, $remote, 'Input') {
} }
[string] identifier () { [string] identifier () {
return 'Strip[' + $this.index + '].Device' return 'Strip[' + $this.index + '].Device'
} }
[int] EnumCount () {
return $this.remote.GetInputCount()
}
[PSObject] EnumDevice ([int]$eIndex) {
return $this.remote.GetInputDevice($eIndex)
}
} }
class VirtualStrip : Strip { class VirtualStrip : Strip {

View File

@@ -850,24 +850,47 @@ Describe -Tag 'higher', -TestName 'All Higher Tests' {
@{ Index = $phys_in } @{ Index = $phys_in }
) { ) {
Context 'Device' -ForEach @( 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" { BeforeEach {
$vmr.strip[$index].device.wdm = $value $vmr.strip[$index].device.Clear()
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
$vmr.strip[$index].device.name | Should -Be $value
} }
It "Should set Strip[$index].Device.ks" { It "Should set Strip[$index].Device.$($driver)" {
$vmr.strip[$index].device.ks = $value $vmr.strip[$index].device.name | Should -Be ''
$vmr.strip[$index].device.driver | Should -Be ''
$vmr.strip[$index].device.$($driver) = $value
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
$vmr.strip[$index].device.name | Should -Be $value $vmr.strip[$index].device.name | Should -Be $value
$vmr.strip[$index].device.driver | Should -Be $expected
} }
It "Should set Strip[$index].Device.mme" { It "Should set Strip[$index].Device" -ForEach @(
$vmr.strip[$index].device.mme = $value @{
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 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 } @{ Index = $phys_out }
) { ) {
Context 'Device' -ForEach @( 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" { BeforeEach {
$vmr.bus[$index].device.wdm = $value $vmr.bus[$index].device.Clear()
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
$vmr.bus[$index].device.name | Should -Be $value
} }
It "Should set Bus[$index].Device.ks" { It "Should set Bus[$index].Device.$($driver)" {
$vmr.bus[$index].device.ks = $value $vmr.bus[$index].device.name | Should -Be ''
$vmr.bus[$index].device.driver | Should -Be ''
$vmr.bus[$index].device.$($driver) = $value
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
$vmr.bus[$index].device.name | Should -Be $value $vmr.bus[$index].device.name | Should -Be $value
$vmr.bus[$index].device.driver | Should -Be $expected
} }
It "Should set Bus[$index].Device.mme" { It "Should set Bus[$index].Device" -ForEach @(
$vmr.bus[$index].device.mme = $value @{
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 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 } @{ Index = $virt_out }
) { ) {
Context 'Device' -Skip:$ifNotBasic -ForEach @( 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" { BeforeEach {
$vmr.bus[$index].device.wdm = $value $vmr.bus[$index].device.Clear()
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
$vmr.bus[$index].device.name | Should -Be $value
} }
It "Should set Bus[$index].Device.ks" { It "Should set Bus[$index].Device.$($driver)" {
$vmr.bus[$index].device.ks = $value $vmr.bus[$index].device.name | Should -Be ''
$vmr.bus[$index].device.driver | Should -Be ''
$vmr.bus[$index].device.$($driver) = $value
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
$vmr.bus[$index].device.name | Should -Be $value $vmr.bus[$index].device.name | Should -Be $value
$vmr.bus[$index].device.driver | Should -Be $expected
} }
It "Should set Bus[$index].Device.mme" { It "Should set Bus[$index].Device" -ForEach @(
$vmr.bus[$index].device.mme = $value @{
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 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
} }
} }
} }