31 Commits
Author SHA1 Message Date
pblivingston 90c09092b6 semicolon in example 2026-08-16 11:59:48 -04:00
pblivingston 0fb26b9e72 add break in example switch 2026-08-08 07:54:19 -04:00
pblivingston c3aa47e7d0 update docs
manual tests pass for
- Get-Remote
- Command.Launch()
- Remote.GetVoicemeeter($kindId)
- Remote.RunVoicemeeter($kindId)
- Remote.RunApplication($appId)
2026-08-07 13:36:20 -04:00
pblivingston 21fce98eef fix Command.Launch() 2026-08-07 13:13:47 -04:00
pblivingston 36e4e16843 GetVoicemeeter 2026-08-07 12:35:03 -04:00
pblivingston 7a6150bf6e Get-Remote 2026-08-06 19:43:15 -04:00
pblivingston d05b46b899 Command.Launch 2026-08-06 19:42:35 -04:00
pblivingston b978f5aa3f RunVoicemeeter, RunApplication 2026-08-06 19:40:54 -04:00
pblivingston 5c5932e7d3 base functions
- Login
- WaitForConnection
- Run
- RunVm
- RunApp
2026-08-06 19:37:46 -04:00
pblivingston 3e5a61eff3 kind 'none' 2026-08-06 19:30:15 -04:00
onyx_online f6c750fe56 add 4.2.0 to CHANGELOG 2026-03-15 02:00:05 +00:00
onyx_online 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
onyx_online b41001f4a9 add bump task 2026-02-19 09:17:38 +00:00
onyx_online fc75bc2020 update obs vm sync example 2026-01-20 19:53:15 +00:00
onyx_online d51ffacfaf rename nextbus script 2026-01-20 19:47:35 +00:00
onyx_online fe540895b3 add cli examples to script docstring 2026-01-20 19:47:14 +00:00
onyx_online 7dd4c9db24 add -kind validation 2026-01-20 19:16:44 +00:00
16 changed files with 853 additions and 120 deletions
+25
View File
@@ -9,6 +9,31 @@ 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
- Get-Remote factory function to log in and return a base Remote instance without launching Voicemeeter
- Command.Launch() method to launch Voicemeeter based on `$this.kind.name`, useful for re-launch after Command.Shutdown()
- Remote.GetVoicemeeter($kindId) to confirm a connection to Voicemeeter and return the requested Remote instance
- Remote.RunVoicemeeter($kindId) to launch Voicemeeter and return an appropriate Remote instance
- Remote.RunApplication($appId) to launch VB-Audio applications
- Remote.GetType(), Remote.GetVersion() to README
## [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
+102 -1
View File
@@ -78,6 +78,7 @@ Voicemeeter factory function can be:
- Get-RemoteBasic
- Get-RemoteBanana
- Get-RemotePotato
- Get-Remote : if only lower-level Remote methods are needed
Added in `v3` you may also use the following entry/exit points:
@@ -368,20 +369,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
@@ -539,6 +552,7 @@ The following Command methods are available:
- HideVBANChat()
- Restart()
- Shutdown()
- Launch() : launches the current Voicemeeter kind
- Reset() : Reset all config
- Save($filepath) : string
- Load($filepath) : string
@@ -563,6 +577,21 @@ $vmr.command.StorePreset() # same as StorePreset(''), overwrite
$vmr.command.RecallPreset('example')
$vmr.command.RecallPreset(63)
$vmr.command.RecallPreset() # same as RecallPreset(''), recalls last recalled
try {
$vmr = Connect-Voicemeeter -Kind 'potato'
if ($vmr.GetType() -ne 'potato') {
$vmr.command.Shutdown() # shuts down whatever was running
Start-Sleep -Seconds 3
$vmr.command.Launch() # launches Voicemeeter Potato
}
# perform operations with Voicemeeter Potato
}
finally {
Disconnect-Voicemeeter
}
```
StorePreset('') and RecallPreset('') interact with the 'selected' preset. This is highlighted green in the GUI. Recalling a preset selects it. Storing a preset via GUI also selects it. Storing a preset with StorePreset does not select it.
@@ -769,6 +798,63 @@ will load a config file at profiles/banana/config.psd1 for Voicemeeter Banana.
### Remote class
`$vmr.GetType()` : returns current Voicemeeter kind ('basic', 'banana', 'potato')
`$vmr.GetVersion()` : returns current Voicemeeter version string (e.g. '2.1.2.2')
`$vmr.GetVoicemeeter($kindId)` : string, ('basic', 'banana', 'potato'), confirms connection and returns requested Remote instance
`$vmr.RunVoicemeeter($kindId)` : string, ('basic', 'banana', 'potato'), launches Voicemeeter and returns appropriate Remote instance
example:
```powershell
try {
$vmr = Get-Remote
$kind = 'none'
try {
$kind = $vmr.GetType()
$vmr = $vmr.GetVoicemeeter($kind)
}
catch { }
switch ($kind) {
'potato' {
break
}
'basic' {
# perform final operations with Voicemeeter Basic
}
'banana' {
# perform final operations with Voicemeeter Banana
}
{ $_ -in 'basic', 'banana'} {
$vmr.Command.Shutdown()
Start-Sleep -Seconds 3
}
{ $_ -ne 'potato' } {
$vmr = $vmr.RunVoicemeeter('potato')
}
}
# perform operations with Voicemeeter Potato
}
finally {
Disconnect-Voicemeeter
}
```
`$vmr.RunApplication($appId)` : string, launches VB-Audio applications
- 'devicecheck'
- 'macrobuttons'
- 'streamerview'
- 'busmatrix8'
- 'busgeq15'
- 'vban2midi'
- 'cablecontrolpanel'
- 'auxcontrolpanel'
- 'vaio3controlpanel'
- 'vaiocontrolpanel'
Access to lower level Getters and Setters are provided with these functions:
- `$vmr.Getter(param)`: For getting the value of a parameter expected to return a value other than string.
@@ -793,6 +879,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.
+13
View File
@@ -9,3 +9,16 @@ tasks:
cmds:
- echo "Running tests..."
- pwsh -c "tests\run.ps1 {{.CLI_ARGS}}"
bump:
desc: 'Bump the module version in the .psd1 file. Usage: "task bump -- show" or "task bump -- [patch|minor|major]".'
preconditions:
- sh: 'pwsh -c "if (Get-Command bump) { exit 0 } else { exit 1 }"'
msg: "The 'bump' command is not available. Please install the required tools to use this command."
cmds:
- |
{{if eq .CLI_ARGS "show"}}
pwsh -c "bump show -f lib/Voicemeeter.psd1 -p \"ModuleVersion\s*=\s'(\d+\.\d+\.\d+)'\""
{{else}}
pwsh -c "bump {{.CLI_ARGS}} -w -f lib/Voicemeeter.psd1 -p \"ModuleVersion\s*=\s'(\d+\.\d+\.\d+)'\""
{{end}}
+7
View File
@@ -12,12 +12,19 @@
Specifies the type of Voicemeeter to connect to (banana or potato). Default is 'banana'.
.PARAMETER script
A list of commands to execute in sequence.
.EXAMPLE
.\CLI.ps1 -interactive -kind banana
Starts the CLI in interactive mode for Voicemeeter Banana.
.EXAMPLE
.\CLI.ps1 -script "Strip[0].Gain=3", "!Bus[1].Mute", "Bus[0].Gain"
Executes a series of commands: sets Strip 0 Gain to 3, toggles Bus 1 Mute, and retrieves Bus 0 Gain.
#>
[cmdletbinding()]
param(
[switch]$help,
[switch]$interactive,
[ValidateSet('basic', 'banana', 'potato')]
[String]$kind = 'banana',
[String[]]$script = @()
)
@@ -12,10 +12,11 @@ param()
Import-Module ..\..\lib\Voicemeeter.psm1
<#
A class that accepts a list of Voicemeeter buses and unmutes them one at a time in a round-robin fashion
#>
class BusRotator {
<#
.SYNOPSIS
Class to manage rotating through Voicemeeter buses.
#>
[object]$vmr = $null
[int]$CurrentIndex = -1
[object[]]$Buses
+293
View File
@@ -0,0 +1,293 @@
<#
.SYNOPSIS
Synchronizes OBS Studio scene changes with Voicemeeter audio settings.
.DESCRIPTION
This script monitors OBS Studio for scene changes via WebSocket connection and
automatically adjusts Voicemeeter audio settings based on the active scene.
.PARAMETER ConfigPath
Path to the configuration file. Defaults to 'config.psd1' in the script directory.
.PARAMETER VoicemeeterKind
Type of Voicemeeter to connect to. Defaults to 'basic'.
.EXAMPLE
.\Vm-Obs-Sync.ps1
.EXAMPLE
.\Vm-Obs-Sync.ps1 -ConfigPath "C:\myconfig.psd1" -VoicemeeterKind "banana"
#>
[CmdletBinding()]
param(
[string]$ConfigPath = (Join-Path $PSScriptRoot 'config.psd1'),
[ValidateSet('basic', 'banana', 'potato')]
[string]$VoicemeeterKind = 'basic'
)
#Requires -Modules obs-powershell
# Import required modules
try {
Import-Module ..\..\lib\Voicemeeter.psm1
Import-Module obs-powershell
}
catch {
Write-Error "Failed to import required modules: $($_.Exception.Message)"
exit 1
}
# Script-level variables
$script:vmr = $null
$script:obsJob = $null
$script:shouldExit = $false
#region Helper Functions
function Write-Log {
<#
.SYNOPSIS
Writes timestamped log messages to the console.
#>
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string]$Message,
[ValidateSet('Info', 'Warning', 'Error')]
[string]$Level = 'Info'
)
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$logMessage = "[$timestamp] [$Level] $Message"
switch ($Level) {
'Info' { Write-Information $logMessage -InformationAction Continue }
'Warning' { Write-Warning $logMessage }
'Error' { Write-Error $logMessage }
}
}
function Get-ConnectionConfig {
<#
.SYNOPSIS
Loads OBS connection configuration from file.
#>
param([string]$Path = $ConfigPath)
try {
if (-not (Test-Path $Path)) {
throw "Configuration file not found: $Path"
}
$config = Import-PowerShellDataFile -Path $Path -ErrorAction Stop
# Validate required properties
$requiredProperties = @('host', 'port', 'password')
foreach ($prop in $requiredProperties) {
if (-not $config.ContainsKey($prop)) {
throw "Missing required configuration property: $prop"
}
}
Write-Log "Configuration loaded successfully from: $Path"
return $config
}
catch {
Write-Log "Failed to load configuration: $($_.Exception.Message)" -Level Error
throw
}
}
function Initialize-Connections {
<#
.SYNOPSIS
Initializes connections to Voicemeeter and OBS.
#>
try {
$script:vmr = Connect-Voicemeeter -Kind $VoicemeeterKind -ErrorAction Stop
Write-Log 'Voicemeeter connection established'
$obsConfig = Get-ConnectionConfig
$webSocketUri = "ws://$($obsConfig.host):$($obsConfig.port)"
$script:obsJob = Watch-OBS -WebSocketURI $webSocketUri -WebSocketToken $obsConfig.password -ErrorAction Stop
Write-Log "OBS connection at $webSocketUri established"
}
catch {
Write-Log "Failed to initialize connections: $($_.Exception.Message)" -Level Error
throw
}
}
function Disconnect-All {
<#
.SYNOPSIS
Safely disconnects from all services.
#>
Write-Log 'Cleaning up connections...'
try {
if ($script:obsJob) {
Remove-Job -Job $script:obsJob -Force -ErrorAction SilentlyContinue
Disconnect-OBS -ErrorAction SilentlyContinue
}
}
catch {
Write-Log "Error disconnecting from OBS: $($_.Exception.Message)" -Level Warning
}
try {
if ($script:vmr) {
Disconnect-Voicemeeter -ErrorAction SilentlyContinue
}
}
catch {
Write-Log "Error disconnecting from Voicemeeter: $($_.Exception.Message)" -Level Warning
}
Write-Log 'Cleanup completed'
}
#endregion
#region Event Handlers
function Invoke-CurrentProgramSceneChanged {
<#
.SYNOPSIS
Handles OBS scene change events.
#>
param(
[Parameter(Mandatory)]
[System.Object]$EventData
)
if (-not $EventData.sceneName) {
Write-Log 'Scene change event received but no scene name provided' -Level Warning
return
}
Write-Log "Scene changed to: $($EventData.sceneName)"
try {
switch ($EventData.sceneName) {
'START' {
Write-Log 'Toggling mute for strip 0'
$script:vmr.strip[0].mute = !$script:vmr.strip[0].mute
}
'BRB' {
Write-Log 'Setting gain to -8.3dB for strip 0'
$script:vmr.strip[0].gain = -8.3
}
'END' {
Write-Log 'Enabling mono for strip 0'
$script:vmr.strip[0].mono = $true
}
'LIVE' {
Write-Log 'Setting color_x to 0.3 for strip 0'
$script:vmr.strip[0].color_x = 0.3
}
default {
Write-Log "Unknown scene '$($EventData.sceneName)'. Expected: START, BRB, END, or LIVE" -Level Warning
}
}
}
catch {
Write-Log "Error processing scene change: $($_.Exception.Message)" -Level Error
}
}
function Invoke-ExitStarted {
<#
.SYNOPSIS
Handles OBS exit events.
#>
param([System.Object]$EventData)
Write-Log 'OBS shutdown detected - initiating graceful exit'
$script:shouldExit = $true
}
function Invoke-EventDispatcher {
<#
.SYNOPSIS
Dispatches OBS events to appropriate handlers.
#>
param(
[Parameter(Mandatory)]
[System.Object]$EventData
)
if (-not $EventData.eventType) {
Write-Log 'Event received without eventType property' -Level Warning
return
}
$handlerName = "Invoke-$($EventData.eventType)"
if (Get-Command $handlerName -ErrorAction SilentlyContinue) {
try {
& $handlerName -EventData $EventData.eventData
}
catch {
Write-Log "Error in event handler '$handlerName': $($_.Exception.Message)" -Level Error
}
}
else {
Write-Log "No handler found for event type: $($EventData.eventType)" -Level Warning
}
}
#endregion
#region Main Execution
function Start-VoicemeeterObsSync {
<#
.SYNOPSIS
Main execution function for the sync process.
#>
Write-Log 'Starting Voicemeeter-OBS synchronization service'
try {
Initialize-Connections
Write-Log 'Monitoring OBS events... Press Ctrl+C to stop'
while (-not $script:shouldExit) {
try {
$obsEvents = Receive-Job -Job $script:obsJob -ErrorAction SilentlyContinue
foreach ($obsEvent in $obsEvents) {
if ($obsEvent.MessageData.op -eq 5) {
Invoke-EventDispatcher -EventData $obsEvent.MessageData.d
}
}
Start-Sleep -Milliseconds 100
}
catch {
Write-Log "Error processing OBS events: $($_.Exception.Message)" -Level Error
}
}
}
catch {
Write-Log "Fatal error: $($_.Exception.Message)" -Level Error
exit 1
}
finally {
Disconnect-All
}
Write-Log 'Voicemeeter-OBS synchronization service stopped'
}
# Handle Ctrl+C gracefully
$null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
$script:shouldExit = $true
}
Start-VoicemeeterObsSync
#endregion
-68
View File
@@ -1,68 +0,0 @@
[cmdletbinding()]
param()
Import-Module ..\..\lib\Voicemeeter.psm1
Import-Module obs-powershell
function CurrentProgramSceneChanged {
param([System.Object]$data)
Write-Host 'Switched to scene', $data.sceneName
switch ($data.sceneName) {
'START' {
$vmr.strip[0].mute = !$vmr.strip[0].mute
}
'BRB' {
$vmr.strip[0].gain = -8.3
}
'END' {
$vmr.strip[0].mono = $true
}
'LIVE' {
$vmr.strip[0].color_x = 0.3
}
default { 'Expected START, BRB, END or LIVE scene' | Write-Warning; return }
}
}
function ExitStarted {
param([System.Object]$data)
'OBS shutdown has begun!' | Write-Host
break
}
function eventHandler($data) {
if (Get-Command $data.eventType -ErrorAction SilentlyContinue) {
& $data.eventType -data $data.eventData
}
}
function ConnFromFile {
$configpath = Join-Path $PSScriptRoot 'config.psd1'
return Import-PowerShellDataFile -Path $configpath
}
function main {
$vmr = Connect-Voicemeeter -Kind 'basic'
$conn = ConnFromFile
$job = Watch-OBS -WebSocketURI "ws://$($conn.host):$($conn.port)" -WebSocketToken $conn.password
try {
while ($true) {
Receive-Job -Job $job | ForEach-Object {
$data = $_.MessageData
if ($data.op -eq 5) {
eventHandler($data.d)
}
}
}
}
finally {
Disconnect-OBS
Disconnect-Voicemeeter
}
}
main
+55 -1
View File
@@ -40,6 +40,20 @@ class Remote {
Logout
}
[Remote] GetVoicemeeter([String]$kindId) {
WaitForConnection
return Make_Remote -kindId $kindId
}
[Remote] RunVoicemeeter([String]$kindId) {
RunVm -kindId $kindId
return Make_Remote -kindId $kindId
}
[void] RunApplication([String]$appId) {
RunApp -appId $appId
}
[string] GetType() {
return VmType
}
@@ -75,6 +89,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 {
@@ -143,6 +173,30 @@ class RemotePotato : Remote {
}
}
function Make_Remote {
param(
[String]$kindId
)
switch ($kindId) {
'basic' {
[RemoteBasic]::new()
}
'banana' {
[RemoteBanana]::new()
}
'potato' {
[RemotePotato]::new()
}
default {
throw [VMRemoteError]::new("Unknown Voicemeeter kind `"$kindId`"")
}
}
}
Function Get-Remote {
[Remote]::new('none').Login()
}
Function Get-RemoteBasic {
[RemoteBasic]::new().Login()
}
@@ -177,4 +231,4 @@ Function Disconnect-Voicemeeter {
Logout
}
Export-ModuleMember -Function Get-RemoteBasic, Get-RemoteBanana, Get-RemotePotato, Connect-Voicemeeter, Disconnect-Voicemeeter
Export-ModuleMember -Function Get-Remote, Get-RemoteBasic, Get-RemoteBanana, Get-RemotePotato, Connect-Voicemeeter, Disconnect-Voicemeeter
+99 -8
View File
@@ -11,22 +11,32 @@ function Login {
}
switch ($retval) {
0 {
WaitForConnection
}
1 {
'Voicemeeter Engine running but GUI not launched. Launching GUI now.' | Write-Verbose
RunVoicemeeter -kindId $kindId
if ($kindId -eq 'none') {
'Logged in to Voicemeeter Remote.' | Write-Verbose
}
else {
'Logged in to Voicemeeter Remote but audio engine and GUI not launched. Launching engine now.' | Write-Verbose
RunVm -kindId $kindId
}
}
-2 {
throw [LoginError]::new('Login may only be called once per session.')
}
}
}
function WaitForConnection {
$timeout = New-TimeSpan -Seconds 2
$sw = [diagnostics.stopwatch]::StartNew()
$exception = $null
do {
Start-Sleep -m 100
try {
'Successfully logged into Voicemeeter [' + $(VmType).ToUpper() + '] Version ' + $(VmVersion) | Write-Verbose
'Successfully connected to Voicemeeter [' + $(VmType).ToUpper() + '] Version ' + $(VmVersion) | Write-Verbose
$exception = $null
break
}
@@ -37,7 +47,7 @@ function Login {
} while ($sw.elapsed -lt $timeout)
if ($null -ne $exception) {
throw [VMRemoteError]::new('Timeout logging into the API.')
throw [VMRemoteError]::new('Timeout waiting to connect to Voicemeeter.')
}
while (P_Dirty -or M_Dirty) { Start-Sleep -m 1 }
@@ -52,7 +62,17 @@ function Logout {
if ($retval -eq 0) { 'Sucessfully logged out' | Write-Verbose }
}
function RunVoicemeeter {
function Run {
param(
[int]$id
)
$retval = [int][Voicemeeter.Remote]::VBVMR_RunVoicemeeter([Int64]$id)
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_RunVoicemeeter')
}
}
function RunVm {
param(
[string]$kindId
)
@@ -61,11 +81,27 @@ function RunVoicemeeter {
'banana' = $(if ([Environment]::Is64BitOperatingSystem) { 5 } else { 2 })
'potato' = $(if ([Environment]::Is64BitOperatingSystem) { 6 } else { 3 })
}
Run -id $kinds[$kindId]
WaitForConnection
}
$retval = [int][Voicemeeter.Remote]::VBVMR_RunVoicemeeter([int64]$kinds[$kindId])
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_RunVoicemeeter')
function RunApp {
param(
[string]$appId
)
$apps = @{
'devicecheck' = 10
'macrobuttons' = 11
'streamerview' = 12
'busmatrix8' = 13
'busgeq15' = 14
'vban2midi' = 15
'cablecontrolpanel' = 20
'auxcontrolpanel' = 21
'vaio3controlpanel' = 22
'vaiocontrolpanel' = 23
}
Run -id $apps[$appId]
}
function P_Dirty {
@@ -226,3 +262,58 @@ function Get_Level {
}
[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
}
}
+9
View File
@@ -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
+9 -1
View File
@@ -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) {
+5
View File
@@ -7,6 +7,11 @@ class Special : IRemote {
return 'Command'
}
[void] Launch() {
'Launching Voicemeeter audio engine and GUI' | Write-Verbose
RunVm -kindId $this.remote.kind.name
}
[void] RunMacrobuttons() {
'Launching the MacroButtons app' | Write-Verbose
Start-Process -FilePath $(Join-Path -Path $this.remote.vmpath -ChildPath 'VoicemeeterMacroButtons.exe')
+115 -1
View File
@@ -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='(?<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")
}
)
}
+3
View File
@@ -1,4 +1,7 @@
$KindMap = @{
'none' = @{
'name' = 'none'
}
'basic' = @{
'name' = 'basic'
'p_in' = 2
+9 -1
View File
@@ -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 {
+96 -27
View File
@@ -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
$vmr.strip[$index].device.name | Should -Be $value
}
It "Should set Strip[$index].Device.ks" {
$vmr.strip[$index].device.ks = $value
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.mme" {
$vmr.strip[$index].device.mme = $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
$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
$vmr.bus[$index].device.name | Should -Be $value
}
It "Should set Bus[$index].Device.ks" {
$vmr.bus[$index].device.ks = $value
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.mme" {
$vmr.bus[$index].device.mme = $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
$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
$vmr.bus[$index].device.name | Should -Be $value
}
It "Should set Bus[$index].Device.ks" {
$vmr.bus[$index].device.ks = $value
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.mme" {
$vmr.bus[$index].device.mme = $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
$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
}
}
}