30 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
b41001f4a9 add bump task 2026-02-19 09:17:38 +00:00
fc75bc2020 update obs vm sync example 2026-01-20 19:53:15 +00:00
d51ffacfaf rename nextbus script 2026-01-20 19:47:35 +00:00
fe540895b3 add cli examples to script docstring 2026-01-20 19:47:14 +00:00
7dd4c9db24 add -kind validation 2026-01-20 19:16:44 +00:00
3119b1080e make help output a console block 2026-01-20 15:33:46 +00:00
a45f7a93af add -help option to help message 2026-01-20 15:31:03 +00:00
0e552873f0 rename input variable 2026-01-20 13:35:53 +00:00
3d87f5c03f fix interactive prompt 2026-01-20 13:20:43 +00:00
aaa4672cbc fix 2026-01-20 13:12:38 +00:00
f5dead51df fix 2026-01-20 13:12:20 +00:00
259c7309dc update CLI example 2026-01-20 13:08:43 +00:00
6542473394 fix method name 2026-01-19 22:18:26 +00:00
448aa01ad2 nextbus example 2026-01-19 22:12:00 +00:00
16 changed files with 869 additions and 220 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
## [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

View File

@@ -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.

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}}

View File

@@ -1,68 +1,129 @@
<#
.SYNOPSIS
Command Line Interface for Voicemeeter control.
.DESCRIPTION
This script provides a command line interface to interact with Voicemeeter. It supports both interactive mode and scripted commands.
Users can specify the type of Voicemeeter (banana or potato) and execute commands to get or set parameters.
.PARAMETER help
Displays help information.
.PARAMETER interactive
Starts the CLI in interactive mode.
.PARAMETER kind
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 = @()
)
Import-Module ..\..\lib\Voicemeeter.psm1
function get-value {
param([object]$vmr, [string]$line)
if ($help -or ($script.Count -eq 0 -and -not $interactive)) {
Write-Host 'Voicemeeter CLI'
Write-Host ''
Write-Host 'Usage:'
Write-Host ' CLI.ps1 [-interactive] [-kind <basic|banana|potato>] [-script <command1>, <command2>, ...]'
Write-Host ''
Write-Host 'Options:'
Write-Host ' -help Display this help message.'
Write-Host ' -interactive Start in interactive mode.'
Write-Host ' -kind <type> Specify the Voicemeeter type (basic, banana or potato). Default is banana.'
Write-Host ' -script <commands> Provide a list of commands to execute in sequence.'
Write-Host ''
Write-Host 'Commands can be of the form:'
Write-Host ' Parameter=Value Set a parameter to a specific value.'
Write-Host ' !Parameter Toggle a boolean parameter.'
Write-Host ' Parameter Get the current value of a parameter.'
exit 0
}
function Get-ParameterValue {
param(
[object]$vmr,
[string]$Parameter
)
try {
$retval = $vmr.Getter($line)
$retval = $vmr.Getter($Parameter)
}
catch {
$retval = $vmr.Getter_String($line)
$retval = $vmr.Getter_String($Parameter)
}
$retval
}
function msgHandler {
param([object]$vmr, [string]$line)
$line + ' passed to handler' | Write-Debug
if ($line[0] -eq '!') {
'Toggling ' + $line.substring(1) | Write-Debug
$retval = get-value -vmr $vmr -line $line.substring(1)
$vmr.Setter($line.substring(1), 1 - $retval)
function Invoke-VoicemeeterCLICommand {
param(
[object]$vmr,
[string]$Command
)
# Toggle command
if ($Command[0] -eq '!') {
$parameter = $Command.Substring(1).Trim()
$currentValue = Get-ParameterValue -vmr $vmr -Parameter $parameter
if ($currentValue -ne 0 -and $currentValue -ne 1) {
throw "Cannot toggle non-boolean parameter '$parameter' with value '$currentValue'"
}
$newValue = 1 - $currentValue
$vmr.SendText("$parameter=$newValue")
Write-Host "Toggled $parameter to $newValue"
}
elseif ($line.Contains('=')) {
"Setting $line" | Write-Debug
$vmr.SendText($line)
# Set command
elseif ($Command.Contains('=')) {
$vmr.SendText("$Command")
Write-Host "Set $Command"
}
# Get command
else {
$parameter = $Command.Trim()
$value = Get-ParameterValue -vmr $vmr -Parameter $parameter
Write-Host "$parameter = $value"
}
}
function Start-VoicemeeterCLI {
param(
[object]$vmr
)
Write-Host "Connected to Voicemeeter. Type 'Q' to quit." -ForegroundColor Green
while (($CommandFromInput = Read-Host 'command') -ne 'Q') {
try {
Invoke-VoicemeeterCLICommand -vmr $vmr -Command $CommandFromInput
}
catch {
Write-Host "Error: $_" -ForegroundColor Red
}
}
}
try {
$vmr = Connect-Voicemeeter -Kind $kind
if ($interactive) {
Start-VoicemeeterCLI -vmr $vmr
}
else {
"Getting $line" | Write-Debug
$retval = get-value -vmr $vmr -line $line
$line + ' = ' + $retval | Write-Host
}
}
function read-hostuntilempty {
param([object]$vmr)
while (($line = Read-Host) -cne [string]::Empty) { msgHandler -vmr $vmr -line $line }
}
function main {
[object]$vmr
try {
$vmr = Connect-Voicemeeter -Kind $kind
if ($interactive) {
'Press <Enter> to exit' | Write-Host
read-hostuntilempty -vmr $vmr
return
}
if ($script.Count -eq 0) {
'No script provided, exiting' | Write-Host
return
}
$script | ForEach-Object {
msgHandler -vmr $vmr -line $_
foreach ($command in $script) {
Invoke-VoicemeeterCLICommand -vmr $vmr -Command $command
}
}
finally { Disconnect-Voicemeeter }
}
main
finally { Disconnect-Voicemeeter }

View File

@@ -1,34 +1,39 @@
## About
A simple voicemeeter-cli script. Offers ability to toggle, get and set parameters.
A basic command-line interface
## Use
Toggle with `!` prefix, get by excluding `=` and set by including `=`. Mix and match arguments.
```console
Voicemeeter CLI
You may pass the following optional flags:
Usage:
CLI.ps1 [-interactive] [-kind <basic|banana|potato>] [-script <command1>, <command2>, ...]
- -o: (-output) to toggle console output.
- -i: (-interactive) to toggle interactive mode.
- -k: (-kind) to set the kind of Voicemeeter. Defaults to banana.
- -s: (script) a string array, one string for each command.
Options:
-help Display this help message.
-interactive Start in interactive mode.
-kind <type> Specify the Voicemeeter type (basic, banana or potato). Default is banana.
-script <commands> Provide a list of commands to execute in sequence.
Commands can be of the form:
Parameter=Value Set a parameter to a specific value.
!Parameter Toggle a boolean parameter.
Parameter Get the current value of a parameter.
```
for example:
```powershell
.\CLI.ps1 -o -k "banana" -s "strip[0].mute", "!strip[0].mute", "strip[0].mute", "bus[2].eq.on=1", "command.lock=1"
.\CLI.ps1 -script strip[0].mute, !strip[0].mute, strip[0].mute, bus[2].eq.on=1, command.lock=1
```
Expected output:
should produce the output:
```powershell
Getting strip[0].mute
strip[0].mute = 0
Toggling strip[0].mute
Getting strip[0].mute
```console
strip[0].mute = 1
Setting bus[2].eq.on=1
Setting command.lock=1
```
If running in interactive mode press `<Enter>` to exit.
Toggled strip[0].mute to 0
strip[0].mute = 0
Set bus[2].eq.on=1
Set command.lock=1
```

View File

@@ -1,47 +0,0 @@
<#
1) Loop through an array of bus objects.
2) Mute first unmuted bus
3) If next bus in array exists, unmute it, otherwise clear unmuted variable.
4) If every bus in array is muted, unmute the first bus specified in array.
Credits go to @bobsupercow
#>
[cmdletbinding()]
param()
Import-Module ..\..\lib\Voicemeeter.psm1
try {
$vmr = Connect-Voicemeeter -Kind 'potato'
$buses = @($vmr.bus[1], $vmr.bus[2], $vmr.bus[4], $vmr.bus[6])
"Buses in selection: $($buses)"
$unmutedIndex = $null
# 1)
'Cycling through bus selection to check for first unmuted Bus...' | Write-Host
foreach ($bus in $buses) {
# 2)
if (-not $bus.mute) {
"Bus $($bus.index) is unmuted... muting it" | Write-Host
$unmutedIndex = $buses.IndexOf($bus)
$bus.mute = $true
# 3)
if ($buses[++$unmutedIndex]) {
"Unmuting Bus $($buses[$unmutedIndex].index)" | Write-Host
$buses[$unmutedIndex].mute = $false
break
}
else { Clear-Variable unmutedIndex }
}
}
# 4)
if ($null -eq $unmutedIndex) {
$buses[0].mute = $false
"Unmuting Bus $($buses[0].index)" | Write-Host
}
}
finally { Disconnect-Voicemeeter }

View File

@@ -0,0 +1,70 @@
<#
.SYNOPSIS
Rotates through specified Voicemeeter buses, unmuting one at a time.
.DESCRIPTION
This script connects to Voicemeeter Potato and allows the user to rotate through a set
of buses (1, 2, 4, and 6). When the user presses Enter, the next bus in the sequence is unmuted,
while all other specified buses are muted. The user can exit the rotation by typing 'Q'.
#>
[cmdletbinding()]
param()
Import-Module ..\..\lib\Voicemeeter.psm1
class BusRotator {
<#
.SYNOPSIS
Class to manage rotating through Voicemeeter buses.
#>
[object]$vmr = $null
[int]$CurrentIndex = -1
[object[]]$Buses
BusRotator([object]$vmr, [object[]]$buses) {
$this.vmr = $vmr
$this.Buses = $buses
}
hidden [object] GetNextBus() {
# Mute all buses in the list
foreach ($bus in $this.Buses) {
$bus.mute = $true
}
# Determine the next bus to unmute
$this.CurrentIndex = ($this.CurrentIndex + 1) % $this.Buses.Count
return $this.Buses[$this.CurrentIndex]
}
[object] UnmuteNextBus() {
$nextBus = $this.GetNextBus()
$nextBus.mute = $false
return $nextBus
}
}
try {
$vmr = Connect-Voicemeeter -Kind 'potato'
# Mute all buses initially
foreach ($bus in $vmr.bus) {
$bus.mute = $true
}
$busesToRotate = @(
$vmr.bus[1],
$vmr.bus[2],
$vmr.bus[4],
$vmr.bus[6]
)
$rotator = [BusRotator]::new($vmr, $busesToRotate)
while ((Read-Host "Press Enter to rotate buses or type 'Q' to quit.") -ne 'Q') {
$nextBus = $rotator.UnmuteNextBus()
Write-Host "Bus $nextBus is now unmuted."
}
}
finally { Disconnect-Voicemeeter }

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

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

View File

@@ -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 {

View File

@@ -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
}
}

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

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) {

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")
}
)
}

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 {

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
}
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
}
}
}