191 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
dfa044d853 add 4.1.0 to CHANGELOG 2025-12-23 11:41:37 +00:00
21eab2e528 pass parent object references to EqChannel and EqCell 2025-12-23 02:27:30 +00:00
8679b4199f Merge pull request #34 from pblivingston/voicemeeter-x-1-2-x
Voicemeeter x.1.2.x
2025-12-22 21:44:07 +00:00
pblivingston
c64d81c36f bus trim, delay
- now pass Eq object reference to Channel
- get remote from Eq
- added kindOfEq check for trim and delay

for safety, float pester tests for potato pass
2025-12-22 11:44:40 -05:00
pblivingston
797ab2e597 Update CHANGELOG.md
updated with x.1.2.x changes
2025-12-19 20:04:15 -05:00
pblivingston
55eb851729 recorder tests, tests pass
more reliable way to locate the recording

pester tests pass for all kinds
manual tests pass for all kinds
- video vban.outstream.route
- eq.channel.cell.gain range
2025-12-19 19:44:05 -05:00
pblivingston
ef1a583351 video, midi outstream routes
- VMR bug: video outstream route is write-only; test commented
- midi outstream route test was improperly int; moved to string

prelim tests for potato pass
2025-12-19 19:44:05 -05:00
pblivingston
0d303e20be vban x.1.2.0
changes staged
- on, name, ip partial write-only resolved
- simple ranges of consecutive integers moved to AddIntMembers
- audio instream/outstream divergent behavior separated
- midi outstream route, string
- new video outstream
2025-12-19 19:44:05 -05:00
pblivingston
c446ad8c93 eq x.1.2.0
changes staged
- bus channel trim
- bus channel delay
- cell gain range
2025-12-19 19:44:04 -05:00
f5bdeb6d57 add recorder.loop removal to breaking changes. 2025-12-16 20:29:04 +00:00
be58d6a9e5 if not in interactive, check a script was passed 2025-12-16 16:45:15 +00:00
eb3af982da add 4.0.0 to CHANGELOG 2025-12-16 15:50:49 +00:00
83c81384be remove comments, they are redundant. 2025-12-14 22:05:30 +00:00
e848037b30 Merge pull request #33 from pblivingston/read-write-only
Meta functions read/write only
2025-12-14 22:03:28 +00:00
pblivingston
66b3fb355c Update CHANGELOG.md
pester tests pass for all kinds

manual tests pass for all kinds
- bus.device.asio
2025-12-13 16:39:01 -05:00
pblivingston
30da69469b asio, recorder.prefix
string pester tests for potato pass

manual test passes
- bus.device.asio
2025-12-13 16:39:01 -05:00
pblivingston
59f3168436 device properties
implemented here first because string pester tests can confirm the behavior works

string pester tests for potato pass
manual tests to confirm error behavior pass
2025-12-13 16:39:01 -05:00
pblivingston
b273aa7a51 ReadOnly, WriteOnly
added ReadOnly and WriteOnly params to meta functions that will override the setter and getter, respectively

prelim pester tests for potato for safety pass
2025-12-13 16:39:01 -05:00
b128ee0625 Merge pull request #32 from pblivingston/io-control
IO Classes
2025-12-13 21:23:59 +00:00
pblivingston
9d3f58f6f2 knob threshold, audibility
move threshold back to comp/gate/denoiser so audibility can be derived from StripKnob

pester tests for safety pass
2025-12-13 11:42:23 -05:00
pblivingston
ea6192ba5f io classes, stripknob to CHANGELOG
pester tests pass for all kinds
2025-12-12 04:35:21 -05:00
pblivingston
ea780f6595 stripknob
centralizes threshold, knob
- stripcomp
- stripgate
- stripdenoiser

prelim pester tests for potato pass
2025-12-12 04:35:21 -05:00
pblivingston
126e6172cb implement io classes
prelim pester tests for potato pass
2025-12-12 04:35:20 -05:00
pblivingston
0c60c5e6c5 Create io.ps1
- IOControl
- IOLevels
- IOEq
- IODevice
2025-12-12 04:35:20 -05:00
837211424f Merge pull request #31 from pblivingston/update-docs
Update docs
2025-12-12 09:02:32 +00:00
88901aa6ee Merge pull request #30 from pblivingston/strip-commands
Strip commands
2025-12-12 08:37:05 +00:00
pblivingston
64ebc86f21 bpsidechain README 2025-12-10 19:54:39 -05:00
pblivingston
8855092438 organize unreleased
organize unreleased changes for readability
2025-12-09 13:20:09 -05:00
pblivingston
865d094450 update bus.levels.convert in CHANGELOG 2025-12-09 05:53:47 -05:00
pblivingston
1cdbf9e272 Update README.md
- more consistent style, organization, terminology
- 'level' -> 'levels'
- examples
- macrobutton index range
2025-12-09 05:51:23 -05:00
pblivingston
23b86fecb9 Update CHANGELOG.md 2025-12-09 05:51:23 -05:00
pblivingston
61b3ecd3d3 Update CHANGELOG.md 2025-12-09 05:49:02 -05:00
pblivingston
a2b75fa21b levels.convert
- hidden, float -> single
2025-12-09 05:47:11 -05:00
pblivingston
618f4a8462 Update README.md
forgot to include pitch examples
2025-12-08 22:40:19 -05:00
pblivingston
a22dccf18f Update README.md 2025-12-08 21:12:36 -05:00
pblivingston
64e6874a75 pan_y, examples
- added tests to demonstrate pan_y, though this is easily seen in the GUI
2025-12-08 15:01:55 -05:00
pblivingston
ac3e36838e gainlayers
gainlayers are now FloatArrayMember objects - this is a breaking change

prelim pester tests for potato pass
2025-12-08 14:49:15 -05:00
pblivingston
a5bade4fbb pitch class
- pitch class added to physical strips

prelim pester tests for potato pass
manual test for potato passes
- recallpreset()
2025-12-07 14:52:20 -05:00
pblivingston
2cf265b3b6 vaio, knobs
- vaio bool on physical strips
- denoiser.threshold
- StripAudibility class with knob float
- knob getters/setters types

prelim pester tests for potato and basic pass
2025-12-07 13:55:42 -05:00
pblivingston
0bdfb1c38f eqgain
- eqgain float members added to virtual strips
- bass/low aliases for eqgain1
- mid/med aliases for eqgain2
- treble/high aliases for eqgain3

prelim pester tests for potato pass
2025-12-07 11:50:48 -05:00
pblivingston
4189ac7721 appgain, appmute
- missing closing parenthesis in AppMute value string
- AppGain, AppMute overloads for index

prelim manual tests for potato pass
2025-12-07 10:41:59 -05:00
pblivingston
6d511d8aa6 limit, alias members
- limit [int] -> [float]
- moved mono bool member to physicalstrip
- added AddAliasMembers meta function
- mono and karaoke aliases added to virtualstrip

prelim pester tests for potato pass
2025-12-07 02:27:29 -05:00
46584236d4 upd floats change in CHANGELOG 2025-12-05 23:37:04 +00:00
a8e66113f7 Merge pull request #29 from pblivingston/float-decimals
Float decimals
2025-12-05 23:31:49 +00:00
pblivingston
daa1fa6c13 docs updated
pester tests pass for all kinds
2025-12-05 07:59:47 -05:00
pblivingston
bc136d870b Update higher.Tests.ps1
prelim pester tests for potato pass
2025-12-05 07:30:25 -05:00
pblivingston
8f49403555 addfloatmembers
- added '-decimals' param, default 2

prelim manual testing passes
2025-12-04 20:30:15 -05:00
pblivingston
dd38cf4bc2 arraymember
- default decimals to 2
- realized an overload is needed

prelim manual testing passes
2025-12-04 20:27:09 -05:00
771238b3b6 update tested against in README 2025-12-04 19:38:15 +00:00
1ad91b455a Merge pull request #28 from pblivingston/special-commands
Special commands
2025-12-04 10:38:13 +00:00
pblivingston
1310ca25ef storepreset, recallpreset
pester tests pass for all kinds
manual tests pass for all kinds
- show/hide
- lock/unlock
- showvbanchat/hidevbanchat
2025-12-04 01:33:25 -05:00
pblivingston
cfa7de9b11 reset, save
- removed 'lock' test, corrected README example
- can now test 'save', 'reset', 'load'

prelim test for potato passes
2025-12-04 01:33:25 -05:00
pblivingston
b5546aa56c existing to methods
prelim manual testing passes for potato
2025-12-04 01:33:25 -05:00
77a8792377 Merge pull request #27 from pblivingston/recorder-commands
Recorder commands
2025-12-04 05:45:40 +00:00
pblivingston
df86ad2175 prefix, filetype
changed to write-only properties
pester tests pass for all kinds
2025-12-04 00:12:37 -05:00
pblivingston
1d41be7396 channel, types
- correct channel values
- add 'gain' to README
- cast getters to [int]
- add some int tests for safety
- skip recording test if basic

pester tests pass for all kinds
manual tests for safety pass
- channel
2025-12-03 04:18:25 -05:00
pblivingston
ab4baa5c44 remove loop, cleanup
- removed deprecated recorder.loop
- placed methods before hidden properties for readability
- added a couple mode tests for good measure
2025-12-03 03:31:30 -05:00
pblivingston
e42862c32d add string test
- can now test recorder.load($filename)
- prelim test passes for potato
2025-12-03 02:58:56 -05:00
pblivingston
0564dce7b6 recorder.state
- prelim tests pass for potato
2025-12-03 02:24:08 -05:00
pblivingston
8c3217b9a8 eject
prelim test passes for potato
'Command.Eject'
2025-12-02 18:12:15 -05:00
pblivingston
d7cb1d610d prerectime, prefix
prelim testing passes for potato
- prefix is currently write-only, so added as a method like FileType
2025-12-02 18:06:02 -05:00
pblivingston
58652b5a3f update docs
arming
2025-12-02 12:42:00 -05:00
pblivingston
9209bbbc65 armstrip, armbus, armedbus
- armstrip, armbus -> boolarraymembers
- armedbus

prelim tests pass for potato
2025-12-02 12:23:06 -05:00
f40e0afb0d Merge pull request #26 from pblivingston/meta-array-device
AddActionMembers, types
2025-12-02 05:21:53 +00:00
pblivingston
bef4e64c9e update docs
- pester tests for safety pass for all kinds
- all manual tests pass
2025-12-01 21:34:26 -05:00
pblivingston
9b1b78100c $arg types
correct/explicit $arg types for consistency
2025-12-01 20:46:34 -05:00
pblivingston
dd31bfcc55 Update arraymember.ps1
remove explicit type casting where implicit coercion already occurs
2025-12-01 20:36:20 -05:00
pblivingston
0fbd41ac0b AddActionMembers
changed AddActionMembers to add ScriptMethod members; this is a breaking change
2025-12-01 20:23:38 -05:00
df2d1bb156 Merge pull request #25 from pblivingston/option-monitoringbus
Option.MonitoringBus
2025-12-01 22:57:22 +00:00
pblivingston
6e74db2751 Update README.md 2025-12-01 17:52:29 -05:00
pblivingston
5f064de010 Update higher.Tests.ps1
pester tests pass for all types
manual tests for safety pass
- buffer.asio
2025-12-01 17:52:29 -05:00
pblivingston
dd404ae337 monitoringBus, types
- add monitoringBus for convenience for Bus[i].Monitor since only one will be true
- cast getters to [int] for type consistency
2025-12-01 17:52:29 -05:00
68f582512a Merge pull request #24 from pblivingston/bus-params
Bus params
2025-12-01 22:47:23 +00:00
pblivingston
8f5536f139 update docs 2025-12-01 17:08:47 -05:00
pblivingston
7eb82c41a2 hide Convert, add mode.set
tests pass for all kinds
2025-12-01 17:05:58 -05:00
pblivingston
ec05790312 Update higher.Tests.ps1
pester tests pass for all kinds
manual tests for safety pass:
- bus.levels.all()
- bus.device.asio
2025-12-01 13:22:07 -05:00
pblivingston
2def27573d update docs 2025-12-01 12:06:51 -05:00
pblivingston
36d4e5f6c4 Update bus.ps1
- sel
- monitor
- vaio
- mono to int
- levels.convert [float] -> [single]
- device.asio $arg -> [string]$arg
2025-12-01 11:17:58 -05:00
e944dc46e6 Merge pull request #19 from pblivingston/vban-fixes
Vban fixes
2025-11-29 01:27:06 +00:00
pblivingston
d87cdbd444 'stream' -> 'audio'
VbanStream -> VbanAudio for clarity
2025-11-28 19:44:49 -05:00
pblivingston
06942a234d Update CHANGELOG.md 2025-11-28 19:25:58 -05:00
pblivingston
17e601a8d6 midi/text streams
- added midi and text streams with:
  - on
  - name
  - ip
had to manually test as these are currently write-only, but I suspect this is a bug/will change in the future
2025-11-28 19:22:56 -05:00
pblivingston
72185d14b3 update docs 2025-11-28 03:47:11 -05:00
pblivingston
81764f0e43 Update higher.Tests.ps1
pester tests pass for all kinds
- increased sleeps after restarts to 2s
- added tags to test types
2025-11-28 03:21:47 -05:00
pblivingston
90e9dcd06c Update vban.ps1
- vban.port
- on -> AddBoolMembers
- name, ip -> AddStringMembers
- cast getters to types
- correct read-only/write-only
- correct route range
2025-11-28 03:18:44 -05:00
b92a2422a7 Merge pull request #17 from pblivingston/fix-test-run
Update run.ps1
2025-11-28 02:32:40 +00:00
pblivingston
2eecdd7def Update run.ps1 2025-11-27 21:23:16 -05:00
5c623711f7 Merge pull request #16 from pblivingston/fx-params
Fx params
2025-11-28 02:17:05 +00:00
pblivingston
8d97df2d92 update docs 2025-11-27 20:56:06 -05:00
pblivingston
438fa525da Update higher.Tests.ps1
pester tests pass for all kinds
2025-11-27 20:55:12 -05:00
pblivingston
aa2c2a24af Update Voicemeeter.psm1 2025-11-27 20:55:12 -05:00
pblivingston
d3e9ad2bf4 Create fx.ps1 2025-11-27 20:55:12 -05:00
abdf2dbf5d Merge pull request #15 from pblivingston/eq-params
Eq params
2025-11-28 01:51:12 +00:00
pblivingston
02bc174746 forgot to save kinds 2025-11-27 20:33:03 -05:00
pblivingston
8038fc24ce identifier, kindOfEq
- move identifier back to BusEq and StripEq for clarity and looser coupling
- adjust kindmap so we can get channel count with kindOfEq
2025-11-27 20:21:43 -05:00
pblivingston
d13b08eff4 update docs 2025-11-27 12:09:06 -05:00
pblivingston
dedb4201be update tests
pester tests pass for all kinds
2025-11-27 11:54:54 -05:00
pblivingston
60d97a89b4 stripeq, buseq
- replace iremote with eq for stripeq and buseq
- move identifier to eq
- avoid passing entire parent objects
2025-11-27 09:49:46 -05:00
pblivingston
6154af7ad7 Create eq.ps1
- eq
- eq channel
- eq cell
2025-11-27 08:40:55 -05:00
07028478cc Merge pull request #13 from pblivingston/decouple-device
Decouple device
2025-11-27 12:31:51 +00:00
pblivingston
3f7bef56c1 strip, bus device
move stripdevice and busdevice back to strip & bus
2025-11-27 06:09:55 -05:00
pblivingston
820b897e84 update docs
Manual tests pass
- device.asio
2025-11-26 18:04:37 -05:00
pblivingston
1e4a2da821 update tests
pester tests pass for all kinds
- cast device.sr to int
2025-11-26 17:54:06 -05:00
pblivingston
ee85d5ffd8 decouple device
- basic A2 device supported
- asio only added to bus[0].device
2025-11-26 17:23:01 -05:00
b20f62f17c Merge pull request #12 from pblivingston/option-params
Option class
2025-11-26 18:52:24 +00:00
pblivingston
1587b2ea6a basic a2 delay 2025-11-26 13:33:42 -05:00
pblivingston
88468d4e52 accepted buffers
removed 896 from wdm and ks
2025-11-26 13:33:14 -05:00
pblivingston
a69902ec49 formatting 2025-11-26 13:32:06 -05:00
pblivingston
10c85cead5 remove mode
- mode.exclusif
- mode.swift
are not available without registry edits
2025-11-26 12:16:43 -05:00
pblivingston
d81cd32392 update docs
manual tests pass
- option.asiosr
- option.buffer.asio
2025-11-26 11:37:51 -05:00
pblivingston
e887e15168 Update higher.Tests.ps1
pester tests pass
2025-11-26 11:17:42 -05:00
pblivingston
c5a8813e9a option.ps1 2025-11-26 10:40:20 -05:00
16dd73231e Merge pull request #9 from pblivingston/patch-arraymembers
Patch and ArrayMember classes
2025-11-26 15:34:42 +00:00
pblivingston
15a977834d add a2 for basic
patch.outa2[i]
2025-11-26 10:01:32 -05:00
pblivingston
f3ed1de557 Update README.md
- correct postfxinsert
- correct examples
- better readability for patch arraymembers
2025-11-26 09:30:44 -05:00
pblivingston
54319924d0 update docs
manual tests all pass:
- asio[i].set($val)
- asio[i].get()
- outa2[i]-outa5[i].set($val)
- outa2[i]-outa5[i].get()

these require an asio device
2025-11-25 21:57:15 -05:00
4d54e0a15f Merge pull request #7 from pblivingston/iremote
Add IRemote abstract base class.
2025-11-26 02:33:58 +00:00
pblivingston
dce6f37bf1 Merge branch 'iremote' into patch-arraymembers 2025-11-25 21:32:31 -05:00
pblivingston
5fc5680c75 fix ToString 2025-11-25 21:29:45 -05:00
pblivingston
80869d4306 tests
pester tests pass
2025-11-25 21:25:13 -05:00
pblivingston
e0b01288ff Update kinds.ps1 2025-11-25 20:43:31 -05:00
pblivingston
3a5c7286f6 Patch
Patch class with:
- Patch.asio[i]
- Patch.OutA2[i]-OutA5[i]
- Patch.composite[i]
- Patch.insert[i]
- Patch.postFaderComposite
- Patch.postFxInsert
2025-11-25 20:38:57 -05:00
pblivingston
c086f58ade ArrayMember classes 2025-11-25 18:54:36 -05:00
pblivingston
e5137b842b Update CHANGELOG.md 2025-11-25 16:44:16 -05:00
pblivingston
78f7fc80d4 vban
implement iremote
2025-11-25 16:39:44 -05:00
pblivingston
62d9e89b5f strip
implement iremote
2025-11-25 16:33:04 -05:00
pblivingston
a6f7d8efe0 recorder
implement iremote
2025-11-25 16:05:13 -05:00
pblivingston
b372cf8087 Update command.ps1
forgot to pass to base
2025-11-25 15:59:39 -05:00
pblivingston
eeb30925fa command
implement iremote
2025-11-25 15:40:15 -05:00
pblivingston
09d8bd48eb bus
implement iremote
2025-11-25 15:35:00 -05:00
pblivingston
b0a6bf7b63 nullable index
make index nullable so ToString can append the index for indexed objects
2025-11-25 15:32:27 -05:00
pblivingston
9a2529c617 module path in tests
change module path so we can run from /tests/
2025-11-25 15:25:40 -05:00
pblivingston
2404bfb50f create iremote.ps1 2025-11-25 14:39:10 -05:00
bd0779add2 add Taskfile
upd tasks in launch.json

add with Task to Run tests in README
2025-06-06 13:50:16 +01:00
a0a2c72634 run through formatter
rename pre-commit to run

remove num and log parameters
2025-06-06 13:49:35 +01:00
0f68a2373d run through formatter 2025-06-06 13:48:24 +01:00
2d6437d37b run through formatter 2025-06-06 13:48:11 +01:00
f199fa587f add Voicemeeter + OBS button example 2025-06-05 20:19:16 +01:00
b6c9c65390 update requirements with note about different scriptdeck versions 2025-06-05 20:10:19 +01:00
436b47a5db upd example 2025-06-05 16:43:30 +01:00
0cdd71600f reword 2025-06-05 01:58:02 +01:00
41529c0d58 add module installation note 2025-06-05 01:55:53 +01:00
b1a6ac68c1 add stream deck example README 2025-06-05 01:51:15 +01:00
fbfab5b4aa 3.3.0 section added to CHANGELOG
added dates for past versions
2024-06-29 10:03:32 +01:00
4d371a7582 Remove the 1 second wait from RunVoicemeeter
Write exception message to Debug
2024-06-29 07:13:11 +01:00
15b3b375bd md fix 2024-06-29 06:55:48 +01:00
c8abc6964a update RunVoicemeeter to launch x64 bit GUIs for all kinds
Keep testing login for up to 2 seconds.
If timeout exceeded throw VMRemoteError
2024-06-29 06:53:20 +01:00
907ee3e63b upd tested against 2024-06-28 11:12:01 +01:00
f3ed9c28c7 upd doc link 2024-01-03 09:38:22 +00:00
d305a4048d "\" -Join path parts 2023-08-17 15:02:03 +01:00
108731b4cf add RunMacrobuttons(), CloseMacrobuttons() 2023-08-17 03:19:05 +01:00
e7c648f1d0 fix function names 2023-08-17 03:05:32 +01:00
b21a71471b 3.2.0 section added to CHANGELOG 2023-08-17 02:57:24 +01:00
43367525c5 Errors section added to README 2023-08-17 02:56:38 +01:00
d0fbd6deef CAPIError properties renamed.
code and function better describe their meaning.
2023-08-17 02:54:30 +01:00
1df92afcfe check size of script 2023-08-17 02:53:01 +01:00
2ad8118f2c adjust the timings slightly 2023-08-17 00:14:12 +01:00
bc6162cf16 add cmdletbinding to examples for debug, verbose flags
add verbose,debug flags to launch scripts
2023-08-16 16:38:00 +01:00
9b3d9f2250 remove Write-Warning for CAPIErrors.
Allow them to bubble up.
(Might be worth adding a helper function to print stacktrace?)
2023-08-16 16:36:43 +01:00
844eaeabaa ErrorMessage removed from error classes 2023-08-16 15:13:30 +01:00
a78cdf9a99 RunVoicemeeter function added
All CAPIErrors are now logged and rethrown
2023-08-16 15:12:25 +01:00
35 changed files with 4001 additions and 1451 deletions

5
.gitignore vendored
View File

@@ -1,7 +1,6 @@
# quick test
quick.ps1
lib/*.psd1
**/*.log
config.psd1
test-*.ps1

28
.vscode/launch.json vendored
View File

@@ -16,7 +16,9 @@
"\"!strip[0].mute\",",
"\"strip[0].mute\",",
"\"bus[2].eq.on=1\",",
"\"command.lock=1\""
"\"command.lock=1\"",
"-Verbose",
"-Debug"
],
"createTemporaryIntegratedConsole": true
},
@@ -26,7 +28,10 @@
"request": "launch",
"cwd": "${workspaceRoot}/examples/nextbus",
"script": "${workspaceFolder}/examples/nextbus/GoTo-NextBus.ps1",
"args": [],
"args": [
"-Verbose",
"-Debug"
],
"createTemporaryIntegratedConsole": true
},
{
@@ -35,7 +40,10 @@
"request": "launch",
"cwd": "${workspaceRoot}/examples/obs",
"script": "${workspaceFolder}/examples/obs/Vm-Obs-Sync.ps1",
"args": [],
"args": [
"-Verbose",
"-Debug"
],
"createTemporaryIntegratedConsole": true
},
{
@@ -43,21 +51,9 @@
"type": "PowerShell",
"request": "launch",
"cwd": "${workspaceRoot}",
"script": "${workspaceFolder}/tests/pre-commit.ps1",
"script": "${workspaceFolder}/tests/run.ps1",
"args": [],
"createTemporaryIntegratedConsole": true
},
{
"name": "PowerShell: Launch Quick Test",
"type": "PowerShell",
"request": "launch",
"cwd": "${workspaceRoot}",
"script": "${workspaceFolder}/quick.ps1",
"args": [
"-Verbose",
"-Debug"
],
"createTemporaryIntegratedConsole": true
}
]
}

View File

@@ -5,13 +5,181 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Before any major/minor/patch is released all test units will be run to verify they pass.
Before any major/minor/patch is released all unit tests will be run to verify they pass.
## [Unreleased] These changes have not been added to PSGallery yet
- [x] Debug statements added to Getters, Setters in higher classes.
## [4.2.0] - 2026-03-15
## [3.1.0]
### 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
### Added
- Bus.EQ.Channel.Trim
- Bus.EQ.Channel.Delay
- MIDI Vban.Outstream.Route: 'none', 'midi_in', 'aux_in', 'vban_in', 'all_in', 'midi_out'
- Video Vban.Outstream
- Vban.Outstream.Vfps
- Vban.Outstream.Vformat: 'png', 'jpg'
- Vban.Outstream.Vquality
- Vban.Outstream.Vcursor
- Vban.Outstream.Route (VMR bug: currently write-only)
### Changed
- `on`, `name`, `ip` now read/write on all streams
- Simple ranges of consecutive integers moved to `AddIntMembers`
- Retained warning for `sr` and anything with atypical behavior, consistent with the rest of the module
- Retained warning for `port` because Voicemeeter will allow this to be set below 1024
- Audio instream/outstream divergent behavior via 'if' dropped in favor of explicit separation in derived classes
- Updated EQ.Channel.Cell.Gain range in README
## [4.0.0] - 2025-12-16
This release introduces some breaking changes, the affected classes are Command, Recorder and Strip.
### Breaking Changes
- Some of the Command properties have been converted to methods. See Command section in README for more details.
- Recorder.loop removed, it can be accessed through {Recorder}.{Mode}.loop.
- Recorder.FileType changed from method to write-only property.
- Strip Gainlayers are now defined by their own class and may be accessed through {Strip}.Gainlayer[i].
- The previous {Strip}.gainlayer{i} properties have been removed.
### Changed
- Meta functions can now take a -ReadOnly or -WriteOnly switch parameter
- Meta: AddBoolMembers, AddIntMembers $arg types for consistency
- Float getters/setters now default to two decimal places.
- Device: explicit $arg types for consistency
- Device members now added with meta functions
- some vban.instream | vban.outstream commands now added with meta functions
- on
- name
- ip
- cast vban getters to types for consistency
- Recorder.Armstrip|Armbus -> BoolArrayMember: now have .Get()
- Cast Recorder getters to types for consistency
- Recorder.Prefix now added with AddStringMembers
- Strip.Mono is now an alias for Strip.MC on virtual strips
- Strip.AppMute|AppGain can now take an app index; see README for details
- Strip Knob setters: explicit $arg types for consistency
### Added
- IRemote abstract base class serves as a launching point for the higher classes.
- ArrayMember classes offer a common interface for array-like properties
- Patch class
- Option class
- IO classes to centralize controls common to both Strip and Bus
- IOControl
- IOLevels
- IOEq
- IODevice
- FX class
- AddAliasMembers meta function takes a hashtable `-MAP` of `alias = property`
- Vban.port sets Vban.Instream[0].port
- Vban Midi and Command streams
- on, write-only
- name, write-only
- ip, write-only
- Recorder.Armedbus
- Recorder.PreRecTime
- Recorder.Prefix
- Recorder.State
- Recorder.Eject()
- Command.Reset()
- Command.Save($filepath)
- Command.StorePreset()
- Command.RecallPreset()
- Bus.Sel, Bus.Monitor, Bus.Vaio
- Bus.Mode.Set($mode)
- Strip.Karaoke alias for Strip.K
- Strip.EQGain1|EQGain2|EQGain3 with bass/low, mid/med, treble/high aliases, respectively
- StripKnob base class which defines a knob property.
- StripAudbility class which represents the audibility knob for basic kind, it subclasses StripKnob.
- Strip.Denoiser.Threshold
- Strip.VAIO.
- Strip.Pitch, StripPitch class
- on
- drywet
- pitchvalue
- loformant
- medformant
- hiformant
- recallpreset($presetIndex)
### Fixed
- some vban commands incorrectly read-only/write-only
- enable
- instream|outstream.quality
- instream|outstream.route
- vban.stream.port: [string]$arg -> [int]$arg
- vban route range (API documentation is incorrect)
- vban.stream.sr: $this._port -> $this._sr
- Recorder.channel values: 1..8 -> (2, 4, 6, 8)
- Bus.Mono -> [int]
- This gives the user access to set stereo reverse.
- Strip.Limit type [int] -> [float]
- Missing closing parenthesis in AppMute value string
- Strip Knob getters: `this.Getter_String('') -> [math]::Round($this.Getter(''), 2)`
## [3.3.0] - 2024-06-29
### Added
- Add a timeout (2s) to the login function. If timeout exceeded a VMRemoteError is thrown.
### Changed
- Launch x64 bit GUIs for all kinds if on 64 bit system.
## [3.2.0] - 2023-08-17
### Added
- Debug statements added to Getters, Setters in higher classes.
- RunVoicemeeter function added to base.ps1. Accepts kind name as parameter.
- Errors section to README.
### Fixed
- All CAPIErrors are now exposed to the consumer.
- The function name and error code can be retrieved using [CAPIError].function and [CAPIError].code
- Set_By_Script now throws [VMRemoteError] if script length exceeds 48kB.
- parameter range checks in Vban class.
## [3.1.0] - 2023-08-15
### Added
@@ -20,7 +188,7 @@ Before any major/minor/patch is released all test units will be run to verify th
- More Recorder commands implemented. See Recorder section in README.
- RunMacrobuttons, CloseMacrobuttons added to Special class
## [3.0.0]
## [3.0.0] - 2023-08-09
v3 introduces some breaking changes. They are as follows:

557
README.md
View File

@@ -8,9 +8,9 @@ For past/future changes to this project refer to: [CHANGELOG](CHANGELOG.md)
## Tested against
- Basic 1.0.8.8
- Banana 2.0.6.8
- Potato 3.0.2.8
- Basic 1.1.2.2
- Banana 2.1.2.2
- Potato 3.1.2.2
## Requirements
@@ -113,58 +113,76 @@ $vmr.Logout()
### Strip
The following strip commands are available:
The following Strip properties are available:
- mute: bool
- mono: bool
- mc: bool
- k: int, from 0 to 4
- k/karaoke: int, from 0 to 4
- solo: bool
- A1-A5: bool
- B1-B3: bool
- limit: int, from -40 to 12
- gain: float, from -60.0 to 12.0
- vaio: bool
- limit: float, from -40.00 to 12.00
- gain: float, from -60.00 to 12.00
- label: string
- reverb: float, from 0.0 to 10.0
- delay: float, from 0.0 to 10.0
- fx1: float, from 0.0 to 10.0
- fx2: float, from 0.0 to 10.0
- pan_x: float, from -0.5 to 0.5
- pan_y: float, from 0.0 to 1.0
- color_x: float, from -0.5 to 0.5
- color_y: float, from 0.0 to 1.0
- fx_x: float, from -0.5 to 0.5
- fx_y: float, from 0.0 to 1.0
- reverb: float, from 0.00 to 10.00
- delay: float, from 0.00 to 10.00
- fx1: float, from 0.00 to 10.00
- fx2: float, from 0.00 to 10.00
- pan_x: float, from -0.50 to 0.50
- pan_y: float, physical: from 0.00 to 1.00, virtual: from -0.50 to 0.50
- color_x: float, from -0.50 to 0.50
- color_y: float, from 0.00 to 1.00
- fx_x: float, from -0.50 to 0.50
- fx_y: float, from 0.00 to 1.00
- postreverb: bool
- postdelay: bool
- postfx1: bool
- postfx2: bool
- gainlayer0-gainlayer7: float
- eqgain1/bass/low: float, from -12.00 to 12.00
- eqgain2/mid/med: float, from -12.00 to 12.00
- eqgain3/treble/high: float, from -12.00 to 12.00
for example:
```powershell
$vmr.strip[5].gainlayer1 = -8.3
$vmr.strip[6].karaoke = 3
$vmr.strip[0].limit = 4.5
$vmr.strip[2].label = 'example'
$vmr.strip[7].pan_y = -0.38
$vmr.strip[5].treble = -2.43
```
A,B commands depend on Voicemeeter type.
The following Strip methods are available:
gainlayers defined for Potato version only.
- AppGain($appname or $appindex, $gain) : string or int, float, from 0.00 to 1.00
- AppMute($appname or $appindex, $mutestate) : string or int, bool
for example:
```powershell
$vmr.strip[5].AppGain("Spotify", 0.5)
$vmr.strip[5].AppMute("Spotify", $true)
$vmr.strip[7].AppGain(0, 0.28)
$vmr.strip[6].AppMute(2, $false)
```
A,B properties depend on Voicemeeter type.
mc, k for virtual strips only.
#### comp
The following strip.comp commands are available:
The following Strip.comp properties are available:
- knob: float, from 0.0 to 10.0
- gainin: float, from -24.0 to 24.0
- ratio: float, from 1.0 to 8.0
- threshold: float, from -40.0 to -3.0
- attack: float, from 0.0 to 200.0
- release: float, from 0.0 to 5000.0
- knee: float, 0.0 to 1.0
- gainout: float, from -24.0 to 24.0
- knob: float, from 0.00 to 10.00
- gainin: float, from -24.00 to 24.00
- ratio: float, from 1.00 to 8.00
- threshold: float, from -40.00 to -3.00
- attack: float, from 0.00 to 200.00
- release: float, from 0.00 to 5000.00
- knee: float, 0.00 to 1.00
- gainout: float, from -24.00 to 24.00
- makeup: bool
for example:
@@ -175,15 +193,15 @@ $vmr.strip[3].comp.attack = 8.5
#### gate
The following strip.gate commands are available:
The following Strip.gate properties are available:
- knob: float, from 0.0 to 10.0
- threshold: float, from -60.0 to -10.0
- damping: float, from -60.0 to -10.0
- bpsidechain: int, from 100 to 4000
- attack: float, from 0.0 to 1000.0
- hold: float, from 0.0 to 5000.0
- release: float, from 0.0 to 5000.0
- knob: float, from 0.00 to 10.00
- threshold: float, from -60.00 to -10.00
- damping: float, from -60.00 to -10.00
- bpsidechain: float, from 100.00 to 4000.00
- attack: float, from 0.00 to 1000.00
- hold: float, from 0.00 to 5000.00
- release: float, from 0.00 to 5000.00
for example:
@@ -193,9 +211,10 @@ $vmr.strip[3].gate.threshold = -40.5
#### denoiser
The following strip.denoiser commands are available:
The following Strip.denoiser properties are available:
- knob: float, from 0.0 to 10.0
- knob: float, from 0.00 to 10.00
- threshold: float, from 0.00 to 10.00
for example:
@@ -203,21 +222,57 @@ for example:
$vmr.strip[3].denoiser.knob = 5
```
#### AppGain | AppMute
#### pitch
- `AppGain(amount, gain)` : string, float
- `AppMute(amount, mutestate)` : string, bool
The following Strip.pitch properties are available:
- on: bool
- drywet: float, from -100.00 to 100.00
- pitchvalue: float, from -12.00 to 12.00
- loformant: float, from -12.00 to 12.00
- medformant: float, from -12.00 to 12.00
- hiformant: float, from -12.00 to 12.00
The following Strip.pitch methods are available:
- RecallPreset($presetIndex) : int, from 0 to 7
for example:
```powershell
$vmr.strip[5].AppGain("Spotify", 0.5)
$vmr.strip[5].AppMute("Spotify", $true)
$vmr.strip[2].pitch.recallpreset(4)
$vmr.strip[4].pitch.drywet = -22.86
$vmr.strip[1].pitch.medformant = 2.1
```
#### audibility
The following Strip.audibility properties are available:
- knob: float, from 0.00 to 10.00
for example:
```powershell
$vmr.strip[1].audibility.knob = 2.66
```
#### gainlayer[i]
The following Strip.gainlayer[i] methods are available:
- Set($val) : float, from -60.00 to 12.00
- Get()
for example:
```powershell
$vmr.strip[4].gainlayer[7].set(-26.81)
```
#### levels
The following strip.level commands are available:
The following Strip.levels methods are available:
- PreFader()
- PostFader()
@@ -231,17 +286,19 @@ $vmr.strip[2].levels.PreFader() -Join ', ' | Write-Host
### Bus
The following bus commands are available:
The following Bus properties are available:
- mute: bool
- mono: bool
- limit: int, from -40 to 12
- gain: float, from -60.0 to 12.0
- sel: bool
- monitor: bool
- vaio: bool
- mono: int, 0 off, 1 mono, 2 stereo reverse
- gain: float, from -60.00 to 12.00
- label: string
- returnreverb: float, from 0.0 to 10.0
- returndelay: float, from 0.0 to 10.0
- returnfx1: float, from 0.0 to 10.0
- returnfx2: float, from 0.0 to 10.0
- returnreverb: float, from 0.00 to 10.00
- returndelay: float, from 0.00 to 10.00
- returnfx1: float, from 0.00 to 10.00
- returnfx2: float, from 0.00 to 10.00
for example:
@@ -251,7 +308,7 @@ $vmr.bus[3].returnreverb = 5.7
#### modes
The following bus.mode members are available:
The following Bus.mode members are available:
- normal: bool
- amix: bool
@@ -266,21 +323,21 @@ The following bus.mode members are available:
- lfeonly: bool
- rearonly: bool
The following bus.mode commands are available:
The following Bus.mode methods are available:
- Get(): returns the current bus mode.
- Set($mode) : string, sets the current bus mode
- Get() : returns the current bus mode
for example:
```powershell
$vmr.bus[0].mode.centeronly = $true
$vmr.bus[0].mode.Get()
$vmr.bus[0].mode.Set('tvmix')
```
#### levels
The following strip.level commands are available:
The following Bus.levels methods are available:
- All()
@@ -292,34 +349,66 @@ $vmr.bus[2].levels.All() -Join ', ' | Write-Host
### Strip|Bus
The following Strip | Bus methods are available:
- FadeTo(amount, time) : float, int
- FadeBy(amount, time) : float, int
Modify gain to or by the selected amount in db over a time interval in ms.
for example:
```powershell
$vmr.strip[3].FadeTo(-18.75, 1000)
$vmr.bus[0].FadeBy(-10, 500)
```
#### device
The following strip.device | bus.device commands are available:
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
$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
#### eq
The following strip.eq | bus.eq commands are available:
The following Strip.eq | Bus.eq properties are available:
- on: bool
- ab: bool
The following Strip.eq | Bus.eq methods are available:
- Load($filepath) : string
- Save($filepath) : string
for example:
```powershell
@@ -327,48 +416,81 @@ $vmr.strip[0].eq.on = $true
$vmr.bus[0].eq.ab = $false
```
#### FadeTo | FadeBy
##### channel
- `FadeTo(amount, time)` : float, int
- `FadeBy(amount, time)` : float, int
The following bus.eq.channel.cell properties are available:
Modify gain to or by the selected amount in db over a time interval in ms.
- trim: float, from -24.00 to 24.00
- delay: float, from 0.00 to 500.00
###### cell
The following eq.channel.cell properties are available:
- on: bool
- type: int, from 0 to 6
- f: float, from 20.00 to 20000.00
- gain: float, from -36.00 to 18.00
- q: float, from 0.30 to 100.00
for example:
```powershell
$vmr.strip[3].FadeTo(-18.7, 1000)
$vmr.bus[0].FadeBy(-10, 500)
$vmr.strip[2].eq.channel[1].cell[4].type = 1
$vmr.bus[5].eq.channel[6].cell[3].on = $false
```
### Macrobuttons
Three modes defined: state, stateonly and trigger.
The following Button properties are available:
- State runs associated scripts
- Stateonly does not run associated scripts
- Index range (0, 69)
- state: bool, runs associated scripts
- stateonly: bool, does not run associated scripts
- trigger: bool
```powershell
$vmr.button[3].state = $true
$vmr.button[4].stateonly = $false
$vmr.button[5].trigger = $true
```
Index range (0, 79)
### VBAN
- vmr.vban.enable: Toggle VBAN on or off. Accepts a bool value.
The following Vban properties are available:
For each vban in/out stream the following parameters are defined:
- enable: bool
- port: int, from 1024 - 65535
for example:
```powershell
$vmr.vban.enable = $true
$vmr.vban.port = 6990
```
#### instream[i]|outstream[i]
The following Vban.instream | Vban.outstream properties are available:
- on: bool
- name: string
- ip: string
- port: int, from 1024 - 65535
- sr: in, (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
- channel: int from 1 to 8
for example:
```powershell
$vmr.vban.instream[0].on = $true
$vmr.vban.outstream[9].ip = '192.168.1.154'
```
##### audio
The following audio Vban.instream | Vban.outstream properties are available:
- sr: int, (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
- channel: int, from 1 to 8
- bit: int, 16 or 24
- quality: int, from 0 to 4
- route: int, from 0 to 8
@@ -376,97 +498,238 @@ For each vban in/out stream the following parameters are defined:
SR, channel and bit are defined as readonly for instreams. Attempting to write
to those parameters will throw an error. They are read and write for outstreams.
example:
for example:
```powershell
$vmr.vban.enable = $true
$vmr.vban.instream[0].on = $true
$vmr.vban.instream[2].port = 6990
$vmr.vban.instream[0].route = 4
$vmr.vban.outstream[3].bit = 16
```
##### midi
The following midi Vban.outstream properties are available:
- route: string, ('none', 'midi_in', 'aux_in', 'vban_in', 'all_in', 'midi_out')
for example:
```powershell
$vmr.vban.outstream[8].route = 'aux_in'
```
##### video
The following video Vban.outstream properties are available:
- vfps: int, from 1 to 30
- vformat: string, ('png', 'jpg')
- vquality: int, from 1 to 100
- vcursor: bool
- route: int, from 0 to 4
Route is currently write-only. This is a VMR bug.
for example:
```powershell
$vmr.vban.outstream[9].vformat = 'jpg'
$vmr.vban.outstream[9].vquality = 85
$vmr.vban.outstream[9].vcursor = $true
```
### Command
Certain 'special' commands are defined by the API as performing actions rather than setting values.
The following commands are available:
The following Command methods are available:
- show
- hide
- restart
- shutdown
- showvbanchat: bool, (write only)
- lock: bool, (write only)
- Show()
- Hide()
- Lock()
- Unlock()
- ShowVBANChat()
- HideVBANChat()
- Restart()
- Shutdown()
- Reset() : Reset all config
- Save($filepath) : string
- Load($filepath) : string
- StorePreset($index, $name) : (int, string)
- RecallPreset($index or $name) : (int or string)
- RunMacrobuttons() : Launches the macrobuttons app
- CloseMacrobuttons() : Closes the macrobuttons app
The following methods are available:
- Load($filepath): string
example:
for example:
```powershell
$vmr.command.show
$vmr.command.lock = $true
$vmr.command.Show()
$vmr.command.Lock()
$vmr.command.Load("path/to/filename.xml")
$vmr.command.RunMacrobuttons()
$vmr.command.StorePreset(63, 'example')
$vmr.command.StorePreset('example')
$vmr.command.StorePreset(63) # same as StorePreset(63, '')
$vmr.command.StorePreset() # same as StorePreset(''), overwrites last recalled
$vmr.command.RecallPreset('example')
$vmr.command.RecallPreset(63)
$vmr.command.RecallPreset() # same as RecallPreset(''), recalls last recalled
```
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.
### Fx
The following Fx properties are available:
- Reverb.on: bool
- Reverb.ab: bool
- Delay.on: bool
- Delay.ab: bool
for example:
```powershell
$vmr.fx.reverb.ab = $false
```
### Patch
The following Patch properties are available:
- postFaderComposite: bool
- postFxInsert: bool
The following Patch members have methods Set($val) | Get() available:
- asio[i]: int, from 0 to ASIO input channels
- OutA2[i]-OutA5[i]: int, from 0 to ASIO output channels
- composite[i]: int, from 0 to strip channels
- insert[i]: bool
for example:
```powershell
$vmr.patch.asio[3].set(2) # patches ASIO input channel 2 (2) to strip 2, channel 2 (3)
$vmr.patch.OutA3[0].set(24) # patches bus A3, channel 1 (0) to ASIO output channel 24
$vmr.patch.composite[5].set(0) # sets composite channel 6 (5) to default bus channel
$vmr.patch.insert[4].get()
```
### Option
The following Option properties are available:
- sr: int, (32000, 44100, 48000, 88200, 96000, 176400, 192000)
- asiosr: bool
- monitorOnSel: bool
- sliderMode: bool
- monitoringBus: int, from 0 to bus index
for example:
```powershell
$vmr.Option.sliderMode = $false # sets slider mode to absolute
```
#### buffers
The following Option.buffer properties are available:
- mme: int, (441, 480, 512, 576, 640, 704, 768, 896, 1024, 1536, 2048)
- wdm: int, (128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 441, 448, 480, 512, 576, 640, 704, 768, 1024, 1536, 2048)
- ks: int, (128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 441, 448, 480, 512, 576, 640, 704, 768, 1024, 1536, 2048)
- asio: int, (0, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 441, 448, 480, 512, 576, 640, 704, 768, 1024)
for example:
```powershell
$vmr.Option.buffer.wdm = 512
$vmr.Option.buffer.asio = 0 # to use default buffer size
```
#### delay[i]
The following Option.delay[i] methods are available:
- Set($val) : float, from 0.00 to 500.00
- Get()
for example:
```powershell
$vmr.Option.delay[2].set(30.26) # sets the delay for the third (2) bus
```
### Recorder
The following commands are available:
The following Recorder properties are available:
- play
- stop
- pause
- record
- ff
- rew
- A1 - A5: bool
- B1 - B3: bool
- gain: float, from -60.00 to 12.00
- armedbus: int, from 0 to bus index
- state: string, ('play', 'stop', 'record', 'pause')
- prerectime: int, from 0 to 20 seconds
- prefix: string, write-only
- filetype: string, write-only, ('wav', 'aiff', 'bwf', 'mp3')
- samplerate: int, (22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
- bitresolution: int, (8, 16, 24, 32)
- channel: int, from 1 to 8
- channel: int, (2, 4, 6, 8)
- kbps: int, (32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320)
The following methods are available:
- Load($filepath): string
- GoTo($timestring): string, must match the format 'hh:mm:ss'
- FileType($format): string, ('wav', 'aiff', 'bwf', 'mp3')
example:
for example:
```powershell
$vmr.recorder.play
$vmr.recorder.A1 = $true
$vmr.recorder.filetype = 'mp3'
$vmr.recorder.kbps = 256
```
The following Recorder methods are available:
- Play()
- Stop()
- Replay()
- FF()
- Rew()
- Record()
- Pause()
- Eject()
- Load($filepath) : string
- GoTo($timestring) : string, must match the format 'hh:mm:ss'
for example:
```powershell
$vmr.recorder.play()
$vmr.recorder.GoTo("00:01:15") # go to 1min 15sec into track
```
#### Mode
#### mode
The following commands are available:
The following Recorder.mode properties are available:
- recbus
- playonload
- loop
- multitrack
- recbus: bool
- playonload: bool
- loop: bool
- multitrack: bool
example:
for example:
```powershell
$vmr.recorder.mode.loop = $true
```
#### ArmStrip[i]|ArmBus[i]
#### armstrip[i]|armbus[i]
The following method is available:
The following Recorder.armstrip | Recorder.armbus methods are available:
- Set($val): bool
- Set($val) : bool
- Get()
example:
for example:
```powershell
$vmr.recorder.armstrip[0].Set($true)
@@ -542,19 +805,43 @@ 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.
- `LoginError`: Raised when a login error occurs.
- `CAPIError`: Raised when a C-API function returns an error code.
- The following class properties are available:
- `function`: The name of the C-API function that returned the error code.
- `code`: The error code.
### Run tests
Run tests using .\tests\pre-commit.ps1 which accepts the following parameters:
Parameters:
- `kind`: Run tests of this kind
- `tag`: Run tests tagged with this marker (currently `higher` or `lower`)
- `num`: Run this number of tests
- `log`: Write summary log file
Run tests from repository root in a subshell and write logs, like so:
*with Task*
`powershell .\tests\pre-commit.ps1 -k "potato" -t "higher" -log`
```console
task test -- -t "higher" -k "banana"
```
### Official Documentation
- [Voicemeeter Remote C API](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/update-docs/VoicemeeterRemoteAPI.pdf)
- [Voicemeeter Remote C API](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/main/VoicemeeterRemoteAPI.pdf)

24
Taskfile.yaml Normal file
View File

@@ -0,0 +1,24 @@
version: '3'
tasks:
test:
desc: 'Run tests'
preconditions:
- sh: 'pwsh -c "if ([System.Version](Get-InstalledModule Pester).Version.ToString() -gt [System.Version]"5.7.0") { exit 0 } else { exit 1 }"'
msg: 'Pester version must be greater than 5.7.0'
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,66 +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,
[switch]$output,
[String]$kind = "banana",
[ValidateSet('basic', 'banana', 'potato')]
[String]$kind = 'banana',
[String[]]$script = @()
)
Import-Module ..\..\lib\Voicemeeter.psm1
$VerbosePreference = "Continue"
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
)
function get-value {
param([object]$vmr, [string]$line)
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 "!") {
if ($output) { "Toggling " + $line.substring(1) | Write-Host }
$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("=")) {
if ($output) { "Setting $line" | Write-Host }
$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 {
if ($output) { "Getting $line" | Write-Host }
$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
}
$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,32 +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.exe .\CLI.ps1 -o -k "banana" -s "strip[0].mute", "!strip[0].mute", "strip[0].mute", "bus[2].eq.on=1", "command.lock=1"`
Expected output:
```powershell
.\CLI.ps1 -script strip[0].mute, !strip[0].mute, strip[0].mute, bus[2].eq.on=1, command.lock=1
```
Getting strip[0].mute
strip[0].mute = 0
Toggling strip[0].mute
Getting strip[0].mute
should produce the output:
```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,44 +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
#>
Import-Module ..\..\lib\Voicemeeter.psm1
$VerbosePreference = "Continue"
try {
$vmr = Connect-Voicemeeter -Kind "potato"
$buses = @($vmr.bus[1], $vmr.bus[2], $vmr.bus[4], $vmr.bus[6])
$unmutedIndex = $null
# 1)
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,71 +0,0 @@
Import-Module ..\..\lib\Voicemeeter.psm1
Import-Module obs-powershell
$VerbosePreference = "Continue"
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
"Toggling Strip 0 mute"
}
"BRB" {
$vmr.strip[0].gain = -8.3
"Setting Strip 0 gain to -8.3"
}
"END" {
$vmr.strip[0].mono = $true
"Setting Strip 0 mono to `$true"
}
"LIVE" {
$vmr.strip[0].color_x = 0.3
"Setting Strip 0 color_x to 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

@@ -0,0 +1,125 @@
# About
Thanks to the guys at [Start Automating](https://startautomating.com/) it's possible to use this module straight from your Stream Deck.
## Requirements
### ScriptDeck
*Windows Powershell*
- [Windows ScriptDeck](https://marketplace.elgato.com/product/windows-scriptdeck-857f01dd-8fd4-44d5-8ec7-67ac850b21d3)
*Powershell core*
- [ScriptDeck](https://marketplace.elgato.com/product/scriptdeck-927e59aa-b42d-4da7-84cc-8c78f4dd7e18)
Note, even though one of them is named Windows they both work on Windows for different powershell versions, see [this issue](https://github.com/StartAutomating/ScriptDeck/issues/120)
### Voicemeeter API Powershell
- Install it as a module, see [Installation](https://github.com/onyx-and-iris/voicemeeter-api-powershell?tab=readme-ov-file#installation)
## How
Once ScriptDeck is installed create a button using *Powershell Script*, then:
### On one button
Due to the design of Voicemeeter's API you may only login/logout once per session so in order to program multiple buttons you must do the following for just ONE button (it can be any button).
#### Button 1
*When Loaded*
```powershell
$global:vmr = Connect-Voicemeeter -Kind "banana"
```
*When Unloaded*
```powershell
Disconnect-Voicemeeter
```
*When Pressed*
```powershell
if ($vmr.strip[0].mute) {
$vmr.bus[0].mute=1
$vmr.bus[1].mute=1
} else {
$vmr.bus[0].mute=0
$vmr.bus[1].mute=0
}
```
### Other buttons
Then your other buttons can have any scripts using the `$vmr` object:
#### Button 2
*When Pressed*
```powershell
$vmr.strip[1].mute=1
$vmr.strip[2].mute=1
if (-not $vmr.strip[0].mute) {
$vmr.strip[0].mute=1
}
```
#### Button 3
*When Pressed*
```powershell
$vmr.strip[0].mute=$(-not $vmr.strip[0].mute)
$vmr.strip[1].mute=$(-not $vmr.strip[1].mute)
$vmr.strip[2].mute=$(-not $vmr.strip[2].mute)
```
---
Then let's say you have zillions of buttons you want to program, for each Stream Deck window configure ONE button as described above and the other buttons of the same window as described above.
If this explanation is unclear or you'd like me to add some screenshots just ask.
## Leveraging Powershell
Since we're now working with Powershell we can do some useful things, for example, lets create a button that interacts with Voicemeeter and OBS:
First make sure you've installed [obs-powershell](https://github.com/StartAutomating/obs-powershell).
Now let's create a button that only toggles some strip mutes if the current OBS scene is "LIVE".
#### Button
*When Loaded*
```powershell
$global:vmr = Connect-Voicemeeter -Kind "banana"
Connect-OBS -WebSocketToken <websocket token>
```
*When Unloaded*
```powershell
Disconnect-Voicemeeter
Disconnect-OBS
```
*When Pressed*
```powershell
$currentScene = $(Get-OBSCurrentProgramScene | Select-Object -ExpandProperty currentProgramSceneName)
if ($currentScene -eq "LIVE") {
$vmr.strip[0].mute=$(-not $vmr.strip[0].mute)
$vmr.strip[1].mute=$(-not $vmr.strip[1].mute)
$vmr.strip[2].mute=$(-not $vmr.strip[2].mute)
}
```

View File

@@ -2,12 +2,18 @@
. $PSScriptRoot\meta.ps1
. $PSScriptRoot\base.ps1
. $PSScriptRoot\kinds.ps1
. $PSScriptRoot\iremote.ps1
. $PSScriptRoot\arraymember.ps1
. $PSScriptRoot\io.ps1
. $PSScriptRoot\strip.ps1
. $PSScriptRoot\bus.ps1
. $PSScriptRoot\macrobuttons.ps1
. $PSScriptRoot\vban.ps1
. $PSScriptRoot\command.ps1
. $PSScriptRoot\recorder.ps1
. $PSScriptRoot\patch.ps1
. $PSScriptRoot\option.ps1
. $PSScriptRoot\fx.ps1
. $PSScriptRoot\profiles.ps1
class Remote {
@@ -22,7 +28,7 @@ class Remote {
}
[string] ToString() {
return "Voicemeeter " + $this.kind.name.substring(0, 1).toupper() + $this.kind.name.substring(1)
return 'Voicemeeter ' + $this.kind.name.substring(0, 1).toupper() + $this.kind.name.substring(1)
}
[Remote] Login() {
@@ -69,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 {
@@ -77,6 +99,8 @@ class RemoteBasic : Remote {
[System.Collections.ArrayList]$button
[PSCustomObject]$vban
[Object]$command
[Object]$patch
[Object]$option
RemoteBasic () : base ('basic') {
$this.strip = Make_Strips($this)
@@ -84,6 +108,8 @@ class RemoteBasic : Remote {
$this.button = Make_Buttons
$this.vban = Make_Vban($this)
$this.command = Make_Command($this)
$this.patch = Make_Patch($this)
$this.option = Make_Option($this)
}
}
@@ -93,6 +119,8 @@ class RemoteBanana : Remote {
[System.Collections.ArrayList]$button
[PSCustomObject]$vban
[Object]$command
[Object]$patch
[Object]$option
[Object]$recorder
RemoteBanana () : base ('banana') {
@@ -101,6 +129,8 @@ class RemoteBanana : Remote {
$this.button = Make_Buttons
$this.vban = Make_Vban($this)
$this.command = Make_Command($this)
$this.patch = Make_Patch($this)
$this.option = Make_Option($this)
$this.recorder = Make_Recorder($this)
}
}
@@ -111,6 +141,9 @@ class RemotePotato : Remote {
[System.Collections.ArrayList]$button
[PSCustomObject]$vban
[Object]$command
[Object]$fx
[Object]$patch
[Object]$option
[Object]$recorder
RemotePotato () : base ('potato') {
@@ -119,6 +152,9 @@ class RemotePotato : Remote {
$this.button = Make_Buttons
$this.vban = Make_Vban($this)
$this.command = Make_Command($this)
$this.fx = Make_Fx($this)
$this.patch = Make_Patch($this)
$this.option = Make_Option($this)
$this.recorder = Make_Recorder($this)
}
}
@@ -137,30 +173,20 @@ Function Get-RemotePotato {
Function Connect-Voicemeeter {
param([String]$Kind)
try {
switch ($Kind) {
"basic" {
return Get-RemoteBasic
}
"banana" {
return Get-RemoteBanana
}
"potato" {
return Get-RemotePotato
}
default { throw [LoginError]::new("Unknown Voicemeeter kind `"$Kind`"") }
}
}
catch [LoginError], [CAPIError] {
Write-Warning $_.Exception.ErrorMessage()
throw
}
catch [VMRemoteError] {
$_.Exception.ErrorMessage() | Write-Warning
if ($_.Exception.ErrorMessage() -eq "Couldn't get Voicemeeter path") {
Exit -1
switch ($Kind) {
'basic' {
return Get-RemoteBasic
}
}
'banana' {
return Get-RemoteBanana
}
'potato' {
return Get-RemotePotato
}
default {
throw [LoginError]::new("Unknown Voicemeeter kind `"$Kind`"")
}
}
}
Function Disconnect-Voicemeeter {

70
lib/arraymember.ps1 Normal file
View File

@@ -0,0 +1,70 @@
class ArrayMember : IRemote {
[string]$prefix
[Object]$parent
ArrayMember (
[int]$index, [string]$prefix, [Object]$parent
) : base ($index, $parent.remote) {
$this.prefix = $prefix
$this.parent = $parent
}
[string] identifier () {
$parentId = $this.parent.identifier()
return "{0}.{1}[{2}]" -f $parentId, $this.prefix, $this.index
}
[void] Set ($val) {
$this.Setter('', $val)
}
}
class BoolArrayMember : ArrayMember {
BoolArrayMember (
[int]$index, [string]$prefix, [Object]$parent
) : base ($index, $prefix, $parent) {}
[bool] Get () {
return $this.Getter('')
}
}
class IntArrayMember : ArrayMember {
IntArrayMember (
[int]$index, [string]$prefix, [Object]$parent
) : base ($index, $prefix, $parent) {}
[int] Get () {
return $this.Getter('')
}
}
class FloatArrayMember : ArrayMember {
[int]$decimals
FloatArrayMember (
[int]$index, [string]$prefix, [Object]$parent, [int]$decimals
) : base ($index, $prefix, $parent) {
$this.decimals = $decimals
}
FloatArrayMember (
[int]$index, [string]$prefix, [Object]$parent
) : base ($index, $prefix, $parent) {
$this.decimals = 2
}
[double] Get () {
return [math]::Round($this.Getter(''), $this.decimals)
}
}
class StringArrayMember : ArrayMember {
StringArrayMember (
[int]$index, [string]$prefix, [Object]$parent
) : base ($index, $prefix, $parent) {}
[string] Get () {
return $this.Getter_String('')
}
}

View File

@@ -5,73 +5,104 @@ function Login {
param(
[string]$kindId
)
try {
$retval = [int][Voicemeeter.Remote]::VBVMR_Login()
if ($retval -ne 0) {
switch ($retval) {
1 {
New-Variable -Name vmExe -Value 0
if ( $kindId -eq "basic" ) { $vmExe = 1 }
elseif ( $kindId -eq "banana" ) { $vmExe = 2 }
elseif ( $kindId -eq "potato" ) {
$vmExe = $(if ([Environment]::Is64BitOperatingSystem) { 6 } else { 3 })
}
$retval = [int][Voicemeeter.Remote]::VBVMR_RunVoicemeeter([int64]$vmExe)
if (-not $retval) {
"Voicemeeter Engine running but GUI not launched. Launching GUI now." | Write-Verbose
Start-Sleep -s 1
}
else {
throw [CAPIError]::new($retval, $MyInvocation.MyCommand)
}
}
-2 {
throw [LoginError]::new('Login may only be called once per session')
}
default { throw [CAPIError]::new($retval, $MyInvocation.MyCommand) }
}
}
$retval = [int][Voicemeeter.Remote]::VBVMR_Login()
if ($retval -notin @(0, 1, -2)) {
throw [CAPIError]::new($retval, 'VBVMR_Login')
}
catch [LoginError] {
"$($_.Exception.ErrorMessage()). Fatal error, exiting..." | Write-Warning
exit -2
switch ($retval) {
1 {
'Voicemeeter Engine running but GUI not launched. Launching GUI now.' | Write-Verbose
RunVoicemeeter -kindId $kindId
}
-2 {
throw [LoginError]::new('Login may only be called once per session.')
}
}
$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
$exception = $null
break
}
catch [CAPIError] {
$exception = $_
$exception | Write-Debug
}
} while ($sw.elapsed -lt $timeout)
if ($null -ne $exception) {
throw [VMRemoteError]::new('Timeout logging into the API.')
}
while (P_Dirty -or M_Dirty) { Start-Sleep -m 1 }
"Successfully logged into Voicemeeter [" + $(VmType).ToUpper() + "] Version " + $(VmVersion) | Write-Verbose
}
function Logout {
Start-Sleep -m 20
Start-Sleep -m 100
$retval = [int][Voicemeeter.Remote]::VBVMR_Logout()
if ($retval -eq 0) { "Sucessfully logged out" | Write-Verbose }
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_Logout')
}
if ($retval -eq 0) { 'Sucessfully logged out' | Write-Verbose }
}
function RunVoicemeeter {
param(
[string]$kindId
)
$kinds = @{
'basic' = $(if ([Environment]::Is64BitOperatingSystem) { 4 } else { 1 })
'banana' = $(if ([Environment]::Is64BitOperatingSystem) { 5 } else { 2 })
'potato' = $(if ([Environment]::Is64BitOperatingSystem) { 6 } else { 3 })
}
$retval = [int][Voicemeeter.Remote]::VBVMR_RunVoicemeeter([int64]$kinds[$kindId])
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_RunVoicemeeter')
}
}
function P_Dirty {
[bool][Voicemeeter.Remote]::VBVMR_IsParametersDirty()
$retval = [Voicemeeter.Remote]::VBVMR_IsParametersDirty()
if ($retval -notin @(0, 1)) {
throw [CAPIError]::new($retval, 'VBVMR_IsParametersDirty')
}
[bool]$retval
}
function M_Dirty {
[bool][Voicemeeter.Remote]::VBVMR_MacroButton_IsDirty()
$retval = [Voicemeeter.Remote]::VBVMR_MacroButton_IsDirty()
if ($retval -notin @(0, 1)) {
throw [CAPIError]::new($retval, 'VBVMR_MacroButton_IsDirty')
}
[bool]$retval
}
function VmType {
New-Variable -Name ptr -Value 0
$retval = [int][Voicemeeter.Remote]::VBVMR_GetVoicemeeterType([ref]$ptr)
if ($retval) { throw [CAPIError]::new($retval, $MyInvocation.MyCommand) }
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_GetVoicemeeterType')
}
switch ($ptr) {
1 { return "basic" }
2 { return "banana" }
3 { return "potato" }
1 { return 'basic' }
2 { return 'banana' }
3 { return 'potato' }
}
}
function VmVersion {
New-Variable -Name ptr -Value 0
$retval = [int][Voicemeeter.Remote]::VBVMR_GetVoicemeeterVersion([ref]$ptr)
if ($retval) { throw [CAPIError]::new($retval, $MyInvocation.MyCommand) }
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_GetVoicemeeterVersion')
}
$v1 = ($ptr -band 0xFF000000) -shr 24
$v2 = ($ptr -band 0x00FF0000) -shr 16
$v3 = ($ptr -band 0x0000FF00) -shr 8
@@ -84,28 +115,22 @@ function Param_Get {
param(
[string]$PARAM, [bool]$IS_STRING = $false
)
Start-Sleep -m 50
Start-Sleep -m 30
while (P_Dirty) { Start-Sleep -m 1 }
if ($IS_STRING) {
$BYTES = [System.Byte[]]::new(512)
try {
$retval = [int][Voicemeeter.Remote]::VBVMR_GetParameterStringA($PARAM, $BYTES)
if ($retval) { throw [CAPIError]::new($retval, $MyInvocation.MyCommand) }
}
catch [CAPIError] {
Write-Warning $_.Exception.ErrorMessage()
$retval = [int][Voicemeeter.Remote]::VBVMR_GetParameterStringA($PARAM, $BYTES)
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_GetParameterStringA')
}
[System.Text.Encoding]::ASCII.GetString($BYTES).Trim([char]0)
}
else {
New-Variable -Name ptr -Value 0.0
try {
$retval = [int][Voicemeeter.Remote]::VBVMR_GetParameterFloat($PARAM, [ref]$ptr)
if ($retval) { throw [CAPIError]::new($retval, $MyInvocation.MyCommand) }
}
catch [CAPIError] {
Write-Warning $_.Exception.ErrorMessage()
$retval = [int][Voicemeeter.Remote]::VBVMR_GetParameterFloat($PARAM, [ref]$ptr)
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_GetParameterFloat')
}
[single]$ptr
}
@@ -115,17 +140,17 @@ function Param_Set {
param(
[string]$PARAM, [Object]$VALUE
)
try {
if ($VALUE -is [string]) {
$retval = [int][Voicemeeter.Remote]::VBVMR_SetParameterStringA($PARAM, $VALUE)
if ($VALUE -is [string]) {
$retval = [int][Voicemeeter.Remote]::VBVMR_SetParameterStringA($PARAM, $VALUE)
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_SetParameterStringA')
}
else {
$retval = [int][Voicemeeter.Remote]::VBVMR_SetParameterFloat($PARAM, $VALUE)
}
if ($retval) { throw [CAPIError]::new($retval, $MyInvocation.MyCommand) }
}
catch [CAPIError] {
Write-Warning $_.Exception.ErrorMessage()
else {
$retval = [int][Voicemeeter.Remote]::VBVMR_SetParameterFloat($PARAM, $VALUE)
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_SetParameterFloat')
}
}
}
@@ -133,12 +158,9 @@ function MB_Set {
param(
[int64]$ID, [single]$SET, [int64]$MODE
)
try {
$retval = [int][Voicemeeter.Remote]::VBVMR_MacroButton_SetStatus($ID, $SET, $MODE)
if ($retval) { throw [CAPIError]::new($retval, $MyInvocation.MyCommand) }
}
catch [CAPIError] {
Write-Warning $_.Exception.ErrorMessage()
$retval = [int][Voicemeeter.Remote]::VBVMR_MacroButton_SetStatus($ID, $SET, $MODE)
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_MacroButton_SetStatus')
}
}
@@ -150,12 +172,9 @@ function MB_Get {
while (M_Dirty) { Start-Sleep -m 1 }
New-Variable -Name ptr -Value 0.0
try {
$retval = [int][Voicemeeter.Remote]::VBVMR_MacroButton_GetStatus($ID, [ref]$ptr, $MODE)
if ($retval) { throw [CAPIError]::new($retval, $MyInvocation.MyCommand) }
}
catch [CAPIError] {
Write-Warning $_.Exception.ErrorMessage()
$retval = [int][Voicemeeter.Remote]::VBVMR_MacroButton_GetStatus($ID, [ref]$ptr, $MODE)
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_MacroButton_GetStatus')
}
[int]$ptr
}
@@ -165,8 +184,8 @@ function Param_Set_Multi {
[hashtable]$HASH
)
foreach ($key in $HASH.keys) {
$classobj, $m2, $m3 = $key.Split("_")
if ($m2 -match "^\d+$") { $index = [int]$m2 } else { $index = [int]$m3 }
$classobj, $m2, $m3 = $key.Split('_')
if ($m2 -match '^\d+$') { $index = [int]$m2 } else { $index = [int]$m3 }
foreach ($h in $HASH[$key].GetEnumerator()) {
$property = $h.Name
@@ -187,12 +206,12 @@ function Set_By_Script {
param(
[string]$script
)
try {
$retval = [int][Voicemeeter.Remote]::VBVMR_SetParameters($script)
if ($retval) { throw [CAPIError]::new($retval, $MyInvocation.MyCommand) }
if ($script.Length -gt 48000) {
throw [VMRemoteError]::new('Script size cannot be larger than 48kB')
}
catch [CAPIError] {
Write-Warning $_.Exception.ErrorMessage()
$retval = [int][Voicemeeter.Remote]::VBVMR_SetParameters($script)
if ($retval -notin @(0)) {
throw [CAPIError]::new($retval, 'VBVMR_SetParameters')
}
}
@@ -201,12 +220,64 @@ function Get_Level {
[int64]$MODE, [int64]$INDEX
)
New-Variable -Name ptr -Value 0.0
try {
$retval = [int][Voicemeeter.Remote]::VBVMR_GetLevel($MODE, $INDEX, [ref]$ptr)
if ($retval) { throw [CAPIError]::new($retval, $MyInvocation.MyCommand) }
}
catch [CAPIError] {
Write-Warning $_.Exception.ErrorMessage()
$retval = [int][Voicemeeter.Remote]::VBVMR_GetLevel($MODE, $INDEX, [ref]$ptr)
if ($retval -notin @(0)) {
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

@@ -3,9 +3,9 @@
function Setup_DLL {
$VMPATH = Get_VMPath
$dll = Join-Path -Path $VMPATH -ChildPath ("VoicemeeterRemote" + `
(& { if ([Environment]::Is64BitOperatingSystem) { "64" } else { "" } }) + `
".dll")
$dll = Join-Path -Path $VMPATH -ChildPath ('VoicemeeterRemote' + `
(& { if ([Environment]::Is64BitOperatingSystem) { '64' } else { '' } }) + `
'.dll')
$Signature = @"
[DllImport(@"$dll")]
@@ -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

@@ -1,68 +1,24 @@
class IBus {
[int]$index
[Object]$remote
IBus ([int]$index, [Object]$remote) {
$this.index = $index
$this.remote = $remote
}
[string] identifier () {
return "Bus[" + $this.index + "]"
}
[single] Getter ($param) {
$this.ToString() + " Getter: $($this.Cmd($param))" | Write-Debug
return $this.remote.Getter($this.Cmd($param))
}
[string] Getter_String ($param) {
$this.ToString() + " Getter_String: $($this.Cmd($param))" | Write-Debug
return $this.remote.Getter_String($this.Cmd($param))
}
[void] Setter ($param, $val) {
$this.ToString() + " Setter: $($this.Cmd($param))=$val" | Write-Debug
$this.remote.Setter($this.Cmd($param), $val)
}
[string] Cmd ($param) {
if ([string]::IsNullOrEmpty($param)) {
return $this.identifier()
}
return "$($this.identifier()).$param"
}
}
class Bus : IBus {
class Bus : IOControl {
[Object]$mode
[Object]$eq
[Object]$levels
Bus ([int]$index, [Object]$remote) : base ($index, $remote) {
AddBoolMembers -PARAMS @('mono', 'mute')
AddStringMembers -PARAMS @('label')
AddFloatMembers -PARAMS @('gain', 'returnreverb', 'returndelay', 'returnfx1', 'returnfx2')
AddBoolMembers -PARAMS @('sel', 'monitor')
AddIntMembers -PARAMS @('mono')
AddFloatMembers -PARAMS @('returnreverb', 'returndelay', 'returnfx1', 'returnfx2')
$this.mode = [BusMode]::new($index, $remote)
$this.eq = [BusEq]::new($index, $remote)
$this.levels = [BusLevels]::new($index, $remote)
}
[string] ToString() {
return $this.GetType().Name + $this.index
}
[void] FadeTo ([single]$target, [int]$time) {
$this.Setter('FadeTo', "($target, $time)")
}
[void] FadeBy ([single]$target, [int]$time) {
$this.Setter('FadeBy', "($target, $time)")
[string] identifier () {
return 'Bus[' + $this.index + ']'
}
}
class BusLevels : IBus {
class BusLevels : IOLevels {
[int]$init
[int]$offset
@@ -71,29 +27,12 @@ class BusLevels : IBus {
$this.offset = 8
}
[float] Convert([float]$val) {
if ($val -gt 0) {
return [math]::Round(20 * [math]::Log10($val), 1)
}
else {
return - 200.0
}
}
[System.Collections.ArrayList] Getter([int]$mode) {
[System.Collections.ArrayList]$vals = @()
$this.init..$($this.init + $this.offset - 1) | ForEach-Object {
$vals.Add($this.Convert($(Get_Level -MODE $mode -INDEX $_)))
}
return $vals
}
[System.Collections.ArrayList] All() {
return $this.Getter(3)
}
}
class BusMode : IBus {
class BusMode : IRemote {
[System.Collections.ArrayList]$modes
BusMode ([int]$index, [Object]$remote) : base ($index, $remote) {
@@ -106,7 +45,7 @@ class BusMode : IBus {
}
[string] identifier () {
return "Bus[" + $this.index + "].mode"
return 'Bus[' + $this.index + '].mode'
}
[string] Get () {
@@ -117,15 +56,23 @@ class BusMode : IBus {
}
return $mode
}
[void] Set ([string]$mode) {
if ($this.modes.Contains($mode)) {
$this.Setter($mode, $true)
}
else {
throw [System.ArgumentException]::new("Invalid mode: $mode")
}
}
}
class BusEq : IBus {
BusEq ([int]$index, [Object]$remote) : base ($index, $remote) {
AddBoolMembers -PARAMS @('on', 'ab')
class BusEq : IOEq {
BusEq ([int]$index, [Object]$remote) : base ($index, $remote, 'Bus') {
}
[string] identifier () {
return "Bus[" + $this.index + "].EQ"
return 'Bus[' + $this.index + '].EQ'
}
}
@@ -134,78 +81,38 @@ class PhysicalBus : Bus {
PhysicalBus ([int]$index, [Object]$remote) : base ($index, $remote) {
$this.device = [BusDevice]::new($index, $remote)
AddBoolMembers -PARAMS @('vaio')
}
}
class BusDevice : IBus {
BusDevice ([int]$index, [Object]$remote) : base ($index, $remote) {
}
[string] identifier () {
return "Bus[" + $this.index + "].Device"
}
hidden $_name = $($this | Add-Member ScriptProperty 'name' `
{
$this.Getter_String('name')
} `
{
return Write-Warning ("ERROR: $($this.identifier()).name is read only")
}
)
hidden $_sr = $($this | Add-Member ScriptProperty 'sr' `
{
$this.Getter('sr')
} `
{
return Write-Warning ("ERROR: $($this.identifier()).sr is read only")
}
)
hidden $_wdm = $($this | Add-Member ScriptProperty 'wdm' `
{
return Write-Warning ("ERROR: $($this.identifier()).wdm is write only")
} `
{
param($arg)
return $this.Setter('wdm', $arg)
}
)
hidden $_ks = $($this | Add-Member ScriptProperty 'ks' `
{
return Write-Warning ("ERROR: $($this.identifier()).ks is write only")
} `
{
param($arg)
return $this.Setter('ks', $arg)
}
)
hidden $_mme = $($this | Add-Member ScriptProperty 'mme' `
{
return Write-Warning ("ERROR: $($this.identifier()).mme is write only")
} `
{
param($arg)
return $this.Setter('mme', $arg)
}
)
hidden $_asio = $($this | Add-Member ScriptProperty 'asio' `
{
return Write-Warning ("ERROR: $($this.identifier()).asio is write only")
} `
{
param($arg)
return $this.Setter('asio', $arg)
}
)
}
class VirtualBus : Bus {
[Object]$device
VirtualBus ([int]$index, [Object]$remote) : base ($index, $remote) {
if ($this.remote.kind.name -eq 'basic') {
$this.device = [BusDevice]::new($index, $remote)
}
}
}
class BusDevice : IODevice {
BusDevice ([int]$index, [Object]$remote) : base ($index, $remote, 'Output') {
if ($this.index -eq 0) {
AddStringMembers -PARAMS @('asio') -WriteOnly
}
}
[string] identifier () {
return 'Bus[' + $this.index + '].Device'
}
[int] EnumCount () {
return $this.remote.GetOutputCount()
}
[PSObject] EnumDevice ([int]$eIndex) {
return $this.remote.GetOutputDevice($eIndex)
}
}

View File

@@ -1,73 +1,73 @@
class Special {
[Object]$remote
Special ([Object]$remote) {
AddActionMembers -PARAMS @('restart', 'shutdown', 'show')
$this.remote = $remote
class Special : IRemote {
Special ([Object]$remote) : base ($remote) {
AddActionMembers -PARAMS @('restart', 'shutdown', 'show', 'lock', 'reset')
}
[string] identifier () {
return "Command"
}
[string] ToString() {
return $this.GetType().Name
}
[single] Getter ($param) {
return $this.remote.Getter("$($this.identifier()).$param")
}
[void] Setter ($param, $val) {
if ($val -is [Boolean]) {
$this.remote.Setter("$($this.identifier()).$param", $(if ($val) { 1 } else { 0 }))
}
else {
$this.remote.Setter("$($this.identifier()).$param", $val)
}
return 'Command'
}
[void] RunMacrobuttons() {
"Launching the MacroButtons app" | Write-Verbose
Start-Process -FilePath $(Join-Path -Path $this.remote.vmpath -ChildPath "VoicemeeterMacroButtons.exe")
'Launching the MacroButtons app' | Write-Verbose
Start-Process -FilePath $(Join-Path -Path $this.remote.vmpath -ChildPath 'VoicemeeterMacroButtons.exe')
}
[void] CloseMacrobuttons() {
"Closing the MacroButtons app" | Write-Verbose
Stop-Process -Name "VoicemeeterMacroButtons"
'Closing the MacroButtons app' | Write-Verbose
Stop-Process -Name 'VoicemeeterMacroButtons'
}
hidden $_hide = $($this | Add-Member ScriptProperty 'hide' `
{
$this._hide = $this.Setter('show', $false)
} `
{}
)
[void] Hide () {
$this.Setter('show', $false)
}
hidden $_showvbanchat = $($this | Add-Member ScriptProperty 'showvbanchat' `
{
$this.Getter('DialogShow.VBANCHAT')
} `
{
param([bool]$arg)
$this._showvbanchat = $this.Setter('DialogShow.VBANCHAT', $arg)
}
)
[void] Unlock () {
$this.Setter('lock', $false)
}
hidden $_lock = $($this | Add-Member ScriptProperty 'lock' `
{
$this._lock = $this.Getter('lock')
} `
{
param([bool]$arg)
$this._lock = $this.Setter('lock', $arg)
}
)
[void] ShowVBANChat () {
$this.Setter('DialogShow.VBANCHAT', $true)
}
[void] HideVBANChat () {
$this.Setter('DialogShow.VBANCHAT', $false)
}
[void] Load ([string]$filename) {
$this.Setter('load', $filename)
}
[void] Save ([string]$filename) {
$this.Setter('save', $filename)
}
[void] StorePreset () {
$this.Setter('updatepreset', '')
}
[void] StorePreset ([string]$name) {
$this.Setter('updatepreset', $name)
}
[void] StorePreset ([int]$index) {
$this.Setter('preset[{0}].store' -f $index, '')
}
[void] StorePreset ([int]$index, [string]$name) {
$this.Setter('preset[{0}].store' -f $index, $name)
}
[void] RecallPreset () {
$this.Setter('recallpreset', '')
}
[void] RecallPreset ([string]$name) {
$this.Setter('recallpreset', $name)
}
[void] RecallPreset ([int]$index) {
$this.Setter('preset[{0}].recall' -f $index, 1)
}
}
function Make_Command([Object]$remote) {

View File

@@ -1,30 +1,19 @@
class VMRemoteError : Exception {
[string]$msg
VMRemoteError ([string]$msg) {
$this.msg = $msg
}
[string] ErrorMessage () {
return $this.msg
VMRemoteError ([string]$msg) : base ($msg) {
}
}
class LoginError : VMRemoteError {
LoginError ([string]$msg) : base ([string]$msg) {
LoginError ([string]$msg) : base ($msg) {
}
}
class CAPIError : VMRemoteError {
[int]$retval
[string]$caller
[int]$code
[string]$function
CAPIError ([int]$retval, [string]$caller) {
$this.retval = $retval
$this.caller = $caller
CAPIError ([int]$code, [string]$function) : base ("$function returned $code") {
$this.code = $code
$this.function = $function
}
[string] ErrorMessage () {
return "CAPI return value: {0} in {1}" -f $this.retval, $this.caller
}
}
}

37
lib/fx.ps1 Normal file
View File

@@ -0,0 +1,37 @@
class Fx : IRemote {
[Object]$reverb
[Object]$delay
Fx ([Object]$remote) : base ($remote) {
$this.reverb = [FxReverb]::new($remote)
$this.delay = [FxDelay]::new($remote)
}
[string] identifier () {
return 'Fx'
}
}
class FxReverb : IRemote {
FxReverb ([Object]$remote) : base ($remote) {
AddBoolMembers -PARAMS @('on', 'ab')
}
[string] identifier () {
return 'Fx.Reverb'
}
}
class FxDelay : IRemote {
FxDelay ([Object]$remote) : base ($remote) {
AddBoolMembers -PARAMS @('on', 'ab')
}
[string] identifier () {
return 'Fx.Delay'
}
}
function Make_Fx ([Object]$remote) {
return [Fx]::new($remote)
}

View File

@@ -1,13 +1,19 @@
function Get_VMPath {
$REG_KEY = "Registry::HKEY_LOCAL_MACHINE\Software" + `
(& { if ([Environment]::Is64BitOperatingSystem) { "\WOW6432Node" } else { "" } }) + `
"\Microsoft\Windows\CurrentVersion\Uninstall"
$VM_KEY = "\VB:Voicemeeter {17359A74-1236-5467}\"
$REG_KEY = @(
'Registry::HKEY_LOCAL_MACHINE',
'Software',
(& { if ([Environment]::Is64BitOperatingSystem) { 'WOW6432Node' } else { '' } }),
'Microsoft',
'Windows',
'CurrentVersion',
'Uninstall'
).Where({ $_ -ne '' }) -Join '\'
$VM_KEY = 'VB:Voicemeeter {17359A74-1236-5467}'
try {
return $(Get-ItemPropertyValue -Path ($REG_KEY + $VM_KEY) -Name UninstallString | Split-Path -Parent)
return $(Get-ItemPropertyValue -Path (@($REG_KEY, $VM_KEY) -Join '\') -Name UninstallString | Split-Path -Parent)
}
catch {
throw [VMRemoteError]::new("Couldn't get Voicemeeter path")
throw [VMRemoteError]::new('Unable to fetch Voicemeeter path from the Registry.')
}
}

222
lib/io.ps1 Normal file
View File

@@ -0,0 +1,222 @@
class IOControl : IRemote {
IOControl ([int]$index, [Object]$remote) : base ($index, $remote) {
AddBoolMembers -PARAMS @('mute')
AddFloatMembers -PARAMS @('gain')
AddStringMembers -PARAMS @('label')
}
[void] FadeTo ([single]$target, [int]$time) {
$this.Setter('FadeTo', "($target, $time)")
}
[void] FadeBy ([single]$target, [int]$time) {
$this.Setter('FadeBy', "($target, $time)")
}
}
class IOLevels : IRemote {
IOLevels ([int]$index, [Object]$remote) : base ($index, $remote) {
}
hidden [single] Convert([single]$val) {
if ($val -gt 0) {
return [math]::Round(20 * [math]::Log10($val), 1)
}
else {
return -200.0
}
}
[System.Collections.ArrayList] Getter([int]$mode) {
[System.Collections.ArrayList]$vals = @()
$this.init..$($this.init + $this.offset - 1) | ForEach-Object {
$vals.Add($this.Convert($(Get_Level -MODE $mode -INDEX $_)))
}
return $vals
}
}
class IOEq : IRemote {
[System.Collections.ArrayList]$channel
[string]$kindOfEq
IOEq ([int]$index, [Object]$remote, [string]$kindOfEq) : base ($index, $remote) {
$this.kindOfEq = $kindOfEq
AddBoolMembers -PARAMS @('on', 'ab')
$this.channel = @()
for ($ch = 0; $ch -lt $remote.kind.eq_ch[$this.kindOfEq]; $ch++) {
$this.channel.Add([EqChannel]::new($ch, $this))
}
}
[void] Load ([string]$filename) {
$param = 'Command.Load{0}Eq[{1}]' -f $this.kindOfEq, $this.index
$this.remote.Setter($param, $filename)
}
[void] Save ([string]$filename) {
$param = 'Command.Save{0}Eq[{1}]' -f $this.kindOfEq, $this.index
$this.remote.Setter($param, $filename)
}
}
class EqChannel : IRemote {
[System.Collections.ArrayList]$cell
[Object]$eq
EqChannel ([int]$index, [Object]$eq) : base ($index, $eq.remote) {
$this.eq = $eq
if ($eq.kindOfEq -eq 'Bus') { AddFloatMembers -PARAMS @('trim', 'delay') }
$this.cell = @()
$cellCount = $this.remote.kind.cells
for ($c = 0; $c -lt $cellCount; $c++) {
$this.cell.Add([EqCell]::new($c, $this))
}
}
[string] identifier () {
return '{0}.Channel[{1}]' -f $this.eq.identifier(), $this.index
}
}
class EqCell : IRemote {
[Object]$channel
EqCell ([int]$index, [Object]$channel) : base ($index, $channel.remote) {
$this.channel = $channel
AddBoolMembers -PARAMS @('on')
AddIntMembers -PARAMS @('type')
AddFloatMembers -PARAMS @('f', 'gain', 'q')
}
[string] identifier () {
return '{0}.Cell[{1}]' -f $this.channel.identifier(), $this.index
}
}
class IODevice : IRemote {
[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")
}
)
}

52
lib/iremote.ps1 Normal file
View File

@@ -0,0 +1,52 @@
class IRemote {
[Nullable[int]]$index
[Object]$remote
IRemote ([Object]$remote) {
$this.remote = $remote
}
IRemote ([int]$index, [Object]$remote) {
$this.index = $index
$this.remote = $remote
}
[single] Getter ($param) {
$this.ToString() + " Getter: $($this.Cmd($param))" | Write-Debug
return $this.remote.Getter($this.Cmd($param))
}
[string] Getter_String ($param) {
$this.ToString() + " Getter_String: $($this.Cmd($param))" | Write-Debug
return $this.remote.Getter_String($this.Cmd($param))
}
[void] Setter ($param, $val) {
$this.ToString() + " Setter: $($this.Cmd($param))=$val" | Write-Debug
if ($val -is [Boolean]) {
$this.remote.Setter($this.Cmd($param), $(if ($val) { 1 } else { 0 }))
}
else {
$this.remote.Setter($this.Cmd($param), $val)
}
}
[string] Cmd ($param) {
if ([string]::IsNullOrEmpty($param)) {
return $this.identifier()
}
return "$($this.identifier()).$param"
}
# Must be overridden by derived classes
[string] identifier () {
throw [System.NotImplementedException]::new("$($this.GetType().Name) must override identifier()")
}
[string] ToString() {
if ($null -ne $this.index) {
return $this.GetType().Name + $this.index
}
return $this.GetType().Name
}
}

View File

@@ -1,33 +1,51 @@
$KindMap = @{
"basic" = @{
"name" = "basic"
"p_in" = 2
"v_in" = 1
"p_out" = 1
"v_out" = 1
"vban_in" = 4
"vban_out" = 4
'basic' = @{
'name' = 'basic'
'p_in' = 2
'v_in' = 1
'p_out' = 1
'v_out' = 1
'asio_in' = 4
'asio_out' = 8
'composite' = 0
'insert' = 0
'vban' = @{ 'in' = 4; 'out' = 4; 'midi' = 1; 'text' = 1; 'video' = 1 }
'eq_ch' = @{ 'strip' = 0; 'bus' = 0 }
'cells' = 0
'gainlayer' = 0
};
"banana" = @{
"name" = "banana"
"p_in" = 3
"v_in" = 2
"p_out" = 3
"v_out" = 2
"vban_in" = 8
"vban_out" = 8
'banana' = @{
'name' = 'banana'
'p_in' = 3
'v_in' = 2
'p_out' = 3
'v_out' = 2
'asio_in' = 6
'asio_out' = 8
'composite' = 8
'insert' = 22
'vban' = @{ 'in' = 8; 'out' = 8; 'midi' = 1; 'text' = 1; 'video' = 1 }
'eq_ch' = @{ 'strip' = 0; 'bus' = 8 }
'cells' = 6
'gainlayer' = 0
};
"potato" = @{
"name" = "potato"
"p_in" = 5
"v_in" = 3
"p_out" = 5
"v_out" = 3
"vban_in" = 8
"vban_out" = 8
'potato' = @{
'name' = 'potato'
'p_in' = 5
'v_in' = 3
'p_out' = 5
'v_out' = 3
'asio_in' = 10
'asio_out' = 8
'composite' = 8
'insert' = 34
'vban' = @{ 'in' = 8; 'out' = 8; 'midi' = 1; 'text' = 1; 'video' = 1 }
'eq_ch' = @{ 'strip' = 2; 'bus' = 8 }
'cells' = 6
'gainlayer' = 8
};
}
function GetKind ([string]$kindId) {
$KindMap[$kindId]
}
}

View File

@@ -1,64 +1,57 @@
function AddBoolMembers () {
param(
[String[]]$PARAMS
[String[]]$PARAMS, [Switch]$readOnly, [Switch]$writeOnly
)
[hashtable]$Signatures = @{}
foreach ($param in $PARAMS) {
# Define getter
$Signatures["Getter"] = "[bool]`$this.Getter('{0}')" -f $param
# Define setter
$Signatures["Setter"] = "param ( [Single]`$arg )`n`$this.Setter('{0}', `$arg)" `
$Signatures['Getter'] = "[bool]`$this.Getter('{0}')" -f $param
$Signatures['Setter'] = "param ( [bool]`$arg )`n`$this.Setter('{0}', `$arg)" `
-f $param
Addmember
Addmember -ReadOnly:$readOnly -WriteOnly:$writeOnly
}
}
function AddFloatMembers () {
param(
[String[]]$PARAMS
[String[]]$PARAMS, [Switch]$readOnly, [Switch]$writeOnly,
[int]$decimals = 2
)
[hashtable]$Signatures = @{}
foreach ($param in $PARAMS) {
# Define getter
$Signatures["Getter"] = "[math]::Round(`$this.Getter('{0}'), 1)" -f $param
# Define setter
$Signatures["Setter"] = "param ( [Single]`$arg )`n`$this.Setter('{0}', `$arg)" `
$Signatures['Getter'] = "[math]::Round(`$this.Getter('{0}'), {1})" -f $param, $decimals
$Signatures['Setter'] = "param ( [Single]`$arg )`n`$this.Setter('{0}', `$arg)" `
-f $param
Addmember
Addmember -ReadOnly:$readOnly -WriteOnly:$writeOnly
}
}
function AddIntMembers () {
param(
[String[]]$PARAMS
[String[]]$PARAMS, [Switch]$readOnly, [Switch]$writeOnly
)
[hashtable]$Signatures = @{}
foreach ($param in $PARAMS) {
# Define getter
$Signatures["Getter"] = "[Int]`$this.Getter('{0}')" -f $param
# Define setter
$Signatures["Setter"] = "param ( [Single]`$arg )`n`$this.Setter('{0}', `$arg)" `
$Signatures['Getter'] = "[Int]`$this.Getter('{0}')" -f $param
$Signatures['Setter'] = "param ( [Int]`$arg )`n`$this.Setter('{0}', `$arg)" `
-f $param
Addmember
Addmember -ReadOnly:$readOnly -WriteOnly:$writeOnly
}
}
function AddStringMembers () {
param(
[String[]]$PARAMS
[String[]]$PARAMS, [Switch]$readOnly, [Switch]$writeOnly
)
[hashtable]$Signatures = @{}
foreach ($param in $PARAMS) {
# Define getter
$Signatures["Getter"] = "[String]`$this.Getter_String('{0}')" -f $param
# Define setter
$Signatures["Setter"] = "param ( [String]`$arg )`n`$this.Setter('{0}', `$arg)" `
$Signatures['Getter'] = "[String]`$this.Getter_String('{0}')" -f $param
$Signatures['Setter'] = "param ( [String]`$arg )`n`$this.Setter('{0}', `$arg)" `
-f $param
Addmember
Addmember -ReadOnly:$readOnly -WriteOnly:$writeOnly
}
}
@@ -66,14 +59,20 @@ function AddActionMembers () {
param(
[String[]]$PARAMS
)
[hashtable]$Signatures = @{}
foreach ($param in $PARAMS) {
# Define getter
$Signatures["Getter"] = "`$this.Setter('{0}', `$true)" -f $param
# Define setter
$Signatures["Setter"] = ""
$this | Add-Member -MemberType ScriptMethod -Name $param `
-Value ([scriptblock]::Create("`$null = `$this.Setter('$param', 1)")) `
-Force
}
}
Addmember
function AddAliasMembers () {
param(
[hashtable]$MAP
)
foreach ($alias in $MAP.Keys) {
$this | Add-Member -MemberType AliasProperty -Name $alias `
-Value $MAP[$alias] -Force
}
}
@@ -83,33 +82,36 @@ function AddChannelMembers () {
[System.Collections.ArrayList]$channels = @()
1..$($num_A + $num_B) | ForEach-Object {
if ($_ -le $num_A) { $channels.Add("A{0}" -f $_) } else { $channels.Add("B{0}" -f $($_ - $num_A)) }
if ($_ -le $num_A) { $channels.Add('A{0}' -f $_) } else { $channels.Add('B{0}' -f $($_ - $num_A)) }
}
AddBoolMembers -PARAMS $channels
}
function AddGainlayerMembers () {
[hashtable]$Signatures = @{}
0..7 | ForEach-Object {
# Define getter
$Signatures["Getter"] = "`$this.Getter('gainlayer[{0}]')" -f $_
# Define setter
$Signatures["Setter"] = "param ( [Single]`$arg )`n`$this.Setter('gainlayer[{0}]', `$arg)" `
-f $_
$param = "gainlayer{0}" -f $_
$null = $param
Addmember
}
}
function Addmember {
param(
[Switch]$readOnly, [Switch]$writeOnly
)
if ($readOnly -and $writeOnly) {
throw "AddMember: cannot be both readOnly and writeOnly for '$param'"
}
# Override signatures based on mode
if ($readOnly) {
$Signatures['Setter'] = "return Write-Warning (`"ERROR: `$(`$this.identifier()).{0} is read only`")" `
-f $param
}
elseif ($writeOnly) {
$Signatures['Getter'] = "return Write-Warning (`"ERROR: `$(`$this.identifier()).{0} is write only`")" `
-f $param
}
$AddMemberParams = @{
Name = $param
MemberType = 'ScriptProperty'
Value = [scriptblock]::Create($Signatures["Getter"])
SecondValue = [scriptblock]::Create($Signatures["Setter"])
Value = [scriptblock]::Create($Signatures['Getter'])
SecondValue = [scriptblock]::Create($Signatures['Setter'])
}
$this | Add-Member @AddMemberParams
}

138
lib/option.ps1 Normal file
View File

@@ -0,0 +1,138 @@
class Option : IRemote {
[System.Collections.ArrayList]$delay
[Object]$buffer
Option ([Object]$remote) : base ($remote) {
AddBoolMembers -PARAMS @('asiosr', 'monitorOnSel', 'sliderMode')
$this.buffer = [OptionBuffer]::new($remote)
$num_A = $this.remote.kind.p_out
if ($this.remote.kind.name -eq 'basic') {
$num_A += $this.remote.kind.v_out
}
$this.delay = @()
for ($i = 0; $i -lt $num_A; $i++) {
$this.delay.Add([FloatArrayMember]::new($i, 'delay', $this))
}
}
[string] identifier () {
return 'Option'
}
hidden $_sr = $($this | Add-Member ScriptProperty 'sr' `
{
[int]$this.Getter('sr')
} `
{
param([int]$arg)
$opts = @(32000, 44100, 48000, 88200, 96000, 176400, 192000)
if ($opts.Contains($arg)) {
$this._sr = $this.Setter('sr', $arg)
}
else {
Write-Warning ('Expected one of', $opts)
}
}
)
hidden $_monitoringBus = $($this | Add-Member ScriptProperty 'monitoringBus' `
{
foreach ($bus in 0..$($this.remote.kind.p_out + $this.remote.kind.v_out - 1)) {
if ($this.remote.Getter("Bus[$bus].Monitor")) {
break
}
}
return $bus
} `
{
param([int]$arg)
$busMax = $this.remote.kind.p_out + $this.remote.kind.v_out - 1
if ($arg -ge 0 -and $arg -le $busMax) {
$this._monitoringBus = $this.remote.Setter("Bus[$arg].Monitor", $arg)
}
else {
Write-Warning ("Expected a bus index between 0 and $busMax")
}
}
)
}
class OptionBuffer : IRemote {
OptionBuffer ([Object]$remote) : base ($remote) {}
[string] identifier () {
return 'Option.Buffer'
}
hidden $_mme = $($this | Add-Member ScriptProperty 'mme' `
{
[int]$this.Getter('mme')
} `
{
param([int]$arg)
$opts = @(441, 480, 512, 576, 640, 704, 768, 896, 1024, 1536, 2048)
if ($opts.Contains($arg)) {
$this._mme = $this.Setter('mme', $arg)
}
else {
Write-Warning ('Expected one of', $opts)
}
}
)
hidden $_wdm = $($this | Add-Member ScriptProperty 'wdm' `
{
[int]$this.Getter('wdm')
} `
{
param([int]$arg)
$opts = @(128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 441, 448, 480, 512, 576, 640, 704, 768, 1024, 1536, 2048)
if ($opts.Contains($arg)) {
$this._wdm = $this.Setter('wdm', $arg)
}
else {
Write-Warning ('Expected one of', $opts)
}
}
)
hidden $_ks = $($this | Add-Member ScriptProperty 'ks' `
{
[int]$this.Getter('ks')
} `
{
param([int]$arg)
$opts = @(128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 441, 448, 480, 512, 576, 640, 704, 768, 1024, 1536, 2048)
if ($opts.Contains($arg)) {
$this._ks = $this.Setter('ks', $arg)
}
else {
Write-Warning ('Expected one of', $opts)
}
}
)
hidden $_asio = $($this | Add-Member ScriptProperty 'asio' `
{
[int]$this.Getter('asio')
} `
{
param([int]$arg)
$opts = @(0, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 441, 448, 480, 512, 576, 640, 704, 768, 1024)
if ($opts.Contains($arg)) {
$this._asio = $this.Setter('asio', $arg)
}
else {
Write-Warning ('Expected one of', $opts)
}
}
)
}
function Make_Option ([Object]$remote) {
return [Option]::new($remote)
}

52
lib/patch.ps1 Normal file
View File

@@ -0,0 +1,52 @@
class Patch : IRemote {
[System.Collections.ArrayList]$asio
[System.Collections.ArrayList]$composite
[System.Collections.ArrayList]$insert
Patch ([Object]$remote) : base ($remote) {
AddBoolMembers -PARAMS @('postFaderComposite', 'postFxInsert')
$this.AddASIOOutMembers()
$this.asio = @()
for ($i = 0; $i -lt $remote.kind.asio_in; $i++) {
$this.asio.Add([IntArrayMember]::new($i, 'asio', $this))
}
$this.composite = @()
for ($i = 0; $i -lt $remote.kind.composite; $i++) {
$this.composite.Add([IntArrayMember]::new($i, 'composite', $this))
}
$this.insert = @()
for ($i = 0; $i -lt $remote.kind.insert; $i++) {
$this.insert.Add([BoolArrayMember]::new($i, 'insert', $this))
}
}
[string] identifier () {
return 'Patch'
}
hidden [void] AddASIOOutMembers () {
$num_A = $this.remote.kind.p_out
if ($this.remote.kind.name -eq 'basic') {
$num_A += $this.remote.kind.v_out
}
$asio_out = $this.remote.kind.asio_out
if ($asio_out -le 0) { return }
for ($a = 2; $a -le $num_A; $a++) {
[System.Collections.ArrayList]$members = @()
for ($i = 0; $i -lt $asio_out; $i++) {
$members.Add([IntArrayMember]::new($i, "OutA$a", $this))
}
Add-Member -InputObject $this -MemberType NoteProperty -Name "OutA$a" -Value $members -Force
}
}
}
function Make_Patch ([Object]$remote) {
return [Patch]::new($remote)
}

View File

@@ -1,5 +1,5 @@
function Get_Profiles ([string]$kind_id) {
$basepath = Join-Path -Path $(Split-Path -Path $PSScriptRoot) -ChildPath "profiles"
$basepath = Join-Path -Path $(Split-Path -Path $PSScriptRoot) -ChildPath 'profiles'
if (Test-Path $basepath) {
$fullpath = Join-Path -Path $basepath -ChildPath $kind_id
}
@@ -11,7 +11,7 @@ function Get_Profiles ([string]$kind_id) {
$filenames | ForEach-Object {
(Join-Path -Path $fullpath -ChildPath $_) | ForEach-Object {
$filename = [System.IO.Path]::GetFileNameWithoutExtension($_)
Write-Host ("Importing profile " + $kind_id + "/" + $filename)
Write-Host ('Importing profile ' + $kind_id + '/' + $filename)
$data[$filename] = Import-PowerShellDataFile -Path $_
}
}
@@ -24,14 +24,9 @@ function Set_Profile {
param(
[Object]$DATA, [string]$CONF
)
try {
if ($null -eq $DATA -or -not $DATA.$CONF) {
throw [VMRemoteErrors]::new("No profile named $CONF was loaded")
}
Param_Set_Multi -HASH $DATA.$CONF
Start-Sleep -m 1
}
catch [VMRemoteErrors] {
Write-Warning $_.Exception.ErrorMessage()
if ($null -eq $DATA -or -not $DATA.$CONF) {
throw [VMRemoteErrors]::new("No profile named '$CONF' has been loaded into memory.")
}
Param_Set_Multi -HASH $DATA.$CONF
Start-Sleep -m 1
}

View File

@@ -1,76 +1,62 @@
class IRecorder {
[Object]$remote
IRecorder ([Object]$remote) {
$this.remote = $remote
}
[single] Getter ($param) {
$this.Cmd($param) | Write-Debug
return $this.remote.Getter($this.Cmd($param))
}
[void] Setter ($param, $val) {
"$($this.Cmd($param))=$val" | Write-Debug
if ($val -is [Boolean]) {
$this.remote.Setter($this.Cmd($param), $(if ($val) { 1 } else { 0 }))
}
else {
$this.remote.Setter($this.Cmd($param), $val)
}
}
[string] Cmd ($param) {
if ([string]::IsNullOrEmpty($param)) {
return $this.identifier()
}
return "$($this.identifier()).$param"
}
}
class Recorder : IRecorder {
[Object]$remote
class Recorder : IRemote {
[Object]$mode
[System.Collections.ArrayList]$armstrip
[System.Collections.ArrayList]$armbus
[System.Collections.ArrayList]$states
Recorder ([Object]$remote) : base ($remote) {
$this.mode = [RecorderMode]::new($remote)
$this.armstrip = @()
0..($remote.kind.p_in + $remote.kind.v_in - 1) | ForEach-Object {
$this.armstrip.Add([RecorderArmStrip]::new($_, $remote))
$stripCount = $($remote.kind.p_in + $remote.kind.v_in)
for ($i = 0; $i -lt $stripCount; $i++) {
$this.armstrip.Add([BoolArrayMember]::new($i, 'armstrip', $this))
}
$this.armbus = @()
0..($remote.kind.p_out + $remote.kind.v_out - 1) | ForEach-Object {
$this.armbus.Add([RecorderArmBus]::new($_, $remote))
$busCount = $($remote.kind.p_out + $remote.kind.v_out)
for ($i = 0; $i -lt $busCount; $i++) {
$this.armbus.Add([BoolArrayMember]::new($i, 'armbus', $this))
}
AddActionMembers -PARAMS @('play', 'stop', 'pause', 'replay', 'record', 'ff', 'rew')
$this.states = @('play', 'stop', 'record', 'pause')
AddActionMembers -PARAMS $this.states
AddActionMembers -PARAMS @('replay', 'ff', 'rew')
AddFloatMembers -PARAMS @('gain')
AddIntMembers -PARAMS @('prerectime')
AddStringMembers -PARAMS @('prefix') -WriteOnly
AddChannelMembers
}
[string] identifier () {
return "Recorder"
return 'Recorder'
}
[string] ToString() {
return $this.GetType().Name
[void] Eject () {
$this.remote.Setter('Command.Eject', 1)
}
hidden $_loop = $($this | Add-Member ScriptProperty 'loop' `
{
[bool]$this.mode.loop
} `
{
param($arg)
$this.mode.loop = $arg
[void] Load ([string]$filename) {
$this.Setter('load', $filename)
}
[void] GoTo ([string]$timestring) {
try {
if ([datetime]::ParseExact($timestring, 'HH:mm:ss', $null)) {
$timespan = [timespan]::Parse($timestring)
$this.Setter('GoTo', $timespan.TotalSeconds)
}
}
)
catch [FormatException] {
"Time string $timestring does not match the required format 'hh:mm:ss'" | Write-Warning
}
}
hidden $_samplerate = $($this | Add-Member ScriptProperty 'samplerate' `
{
$this.Getter('samplerate')
[int]$this.Getter('samplerate')
} `
{
param([int]$arg)
@@ -86,7 +72,7 @@ class Recorder : IRecorder {
hidden $_bitresolution = $($this | Add-Member ScriptProperty 'bitresolution' `
{
$this.Getter('bitresolution')
[int]$this.Getter('bitresolution')
} `
{
param([int]$arg)
@@ -102,22 +88,23 @@ class Recorder : IRecorder {
hidden $_channel = $($this | Add-Member ScriptProperty 'channel' `
{
$this.Getter('channel')
[int]$this.Getter('channel')
} `
{
param([int]$arg)
if ($arg -ge 1 -and $arg -le 8) {
$opts = @(2, 4, 6, 8)
if ($opts.Contains($arg)) {
$this._channel = $this.Setter('channel', $arg)
}
else {
"channel got: $arg, expected value from 1 to 8" | Write-Warning
"channel got: $arg, expected one of $opts" | Write-Warning
}
}
)
hidden $_kbps = $($this | Add-Member ScriptProperty 'kbps' `
{
$this.Getter('kbps')
[int]$this.Getter('kbps')
} `
{
param([int]$arg)
@@ -131,72 +118,77 @@ class Recorder : IRecorder {
}
)
[void] Load ([string]$filename) {
$this.Setter('load', $filename)
}
hidden $_filetype = $($this | Add-Member ScriptProperty 'filetype' `
{
return Write-Warning ("ERROR: $($this.identifier()).filetype is write only")
} `
{
param([string]$arg)
[int]$val = 0
switch ($arg) {
'wav' { $val = 1 }
'aiff' { $val = 2 }
'bwf' { $val = 3 }
'mp3' { $val = 100 }
default { "Filetype() got: $arg, expected one of 'wav', 'aiff', 'bwf', 'mp3'" }
}
$this._filetype = $this.Setter('filetype', $val)
}
)
[void] GoTo ([string]$timestring) {
try {
if ([datetime]::ParseExact($timestring, "HH:mm:ss", $null)) {
$timespan = [timespan]::Parse($timestring)
$this.Setter("GoTo", $timespan.TotalSeconds)
hidden $_armedbus = $($this | Add-Member ScriptProperty 'armedbus' `
{
foreach ($bus in 0..$($this.remote.kind.p_out + $this.remote.kind.v_out - 1)) {
if ($this.remote.Getter("Recorder.ArmBus[$bus]")) {
break
}
}
return $bus
} `
{
param([int]$arg)
$busMax = $this.remote.kind.p_out + $this.remote.kind.v_out - 1
if ($arg -ge 0 -and $arg -le $busMax) {
$this._armedbus = $this.remote.Setter("Recorder.ArmBus[$arg]", 1)
}
else {
Write-Warning ("Expected a bus index between 0 and $busMax")
}
}
catch [FormatException] {
"Time string $timestring does not match the required format 'hh:mm:ss'" | Write-Warning
}
}
)
[void] FileType($format) {
[int]$val = 0
switch ($format) {
"wav" { $val = 1 }
"aiff" { $val = 2 }
"bwf" { $val = 3 }
"mp3" { $val = 100 }
default { "Filetype() got: $format, expected one of 'wav', 'aiff', 'bwf', 'mp3'" }
hidden $_state = $($this | Add-Member ScriptProperty 'state' `
{
if ($this.Getter('pause')) { return 'pause' }
foreach ($state in $this.states) {
if ($this.Getter($state)) {
break
}
}
return $state
} `
{
param([string]$arg)
if (-not $this.states.Contains($arg)) {
Write-Warning ("Recorder.State got: $arg, expected one of $($this.states)")
return
}
if ($arg -eq 'pause' -and -not $this.Getter('record')) {
Write-Warning ("Recorder.State can only be set to 'pause' when recording")
return
}
$this._state = $this.Setter($arg, 1)
}
$this.Setter("filetype", $val)
}
)
}
class RecorderMode : IRecorder {
class RecorderMode : IRemote {
RecorderMode ([Object]$remote) : base ($remote) {
AddBoolMembers -PARAMS @('recbus', 'playonload', 'loop', 'multitrack')
}
[string] identifier () {
return "Recorder.Mode"
}
}
class RecorderArm : IRecorder {
[int]$index
RecorderArm ([int]$index, [Object]$remote) : base ($remote) {
$this.index = $index
}
Set ([bool]$val) {
$this.Setter("", $(if ($val) { 1 } else { 0 }))
}
}
class RecorderArmStrip : RecorderArm {
RecorderArmStrip ([int]$index, [Object]$remote) : base ($index, $remote) {
}
[string] identifier () {
return "Recorder.ArmStrip[$($this.index)]"
}
}
class RecorderArmBus : RecorderArm {
RecorderArmBus ([int]$index, [Object]$remote) : base ($index, $remote) {
}
[string] identifier () {
return "Recorder.ArmBus[$($this.index)]"
return 'Recorder.Mode'
}
}

View File

@@ -1,68 +1,27 @@
class IStrip {
[int]$index
[Object]$remote
IStrip ([int]$index, [Object]$remote) {
$this.index = $index
$this.remote = $remote
}
[string] identifier () {
return "Strip[" + $this.index + "]"
}
[single] Getter ($param) {
$this.Cmd($param) | Write-Debug
return $this.remote.Getter($this.Cmd($param))
}
[string] Getter_String ($param) {
$this.Cmd($param) | Write-Debug
return $this.remote.Getter_String($this.Cmd($param))
}
[void] Setter ($param, $val) {
"$($this.Cmd($param))=$val" | Write-Debug
$this.remote.Setter($this.Cmd($param), $val)
}
[string] Cmd ($param) {
if ([string]::IsNullOrEmpty($param)) {
return $this.identifier()
}
return "$($this.identifier()).$param"
}
}
class Strip : IStrip {
class Strip : IOControl {
[System.Collections.ArrayList]$gainlayer
[Object]$levels
Strip ([int]$index, [Object]$remote) : base ($index, $remote) {
AddBoolMembers -PARAMS @('mono', 'solo', 'mute')
AddIntMembers -PARAMS @('limit')
AddFloatMembers -PARAMS @('gain', 'pan_x', 'pan_y')
AddStringMembers -PARAMS @('label')
AddBoolMembers -PARAMS @('solo')
AddFloatMembers -PARAMS @('limit', 'pan_x', 'pan_y')
AddChannelMembers
AddGainlayerMembers
$this.levels = [StripLevels]::new($index, $remote)
$this.gainlayer = @()
for ($i = 0; $i -lt $remote.kind.gainlayer; $i++) {
$this.gainlayer.Add([FloatArrayMember]::new($i, 'gainlayer', $this))
}
}
[string] ToString() {
return $this.GetType().Name + $this.index
}
[void] FadeTo ([single]$target, [int]$time) {
$this.Setter('FadeTo', "($target, $time)")
}
[void] FadeBy ([single]$target, [int]$time) {
$this.Setter('FadeBy', "($target, $time)")
[string] identifier () {
return 'Strip[' + $this.index + ']'
}
}
class StripLevels : IStrip {
class StripLevels : IOLevels {
[int]$init
[int]$offset
@@ -78,23 +37,6 @@ class StripLevels : IStrip {
}
}
[float] Convert([float]$val) {
if ($val -gt 0) {
return [math]::Round(20 * [math]::Log10($val), 1)
}
else {
return -200.0
}
}
[System.Collections.ArrayList] Getter([int]$mode) {
[System.Collections.ArrayList]$vals = @()
$this.init..$($this.init + $this.offset - 1) | ForEach-Object {
$vals.Add($this.Convert($(Get_Level -MODE $mode -INDEX $_)))
}
return $vals
}
[System.Collections.ArrayList] PreFader() {
return $this.Getter(0)
}
@@ -114,169 +56,153 @@ class PhysicalStrip : Strip {
[Object]$denoiser
[Object]$eq
[Object]$device
[Object]$audibility
[Object]$pitch
PhysicalStrip ([int]$index, [Object]$remote) : base ($index, $remote) {
AddFloatMembers -PARAMS @('color_x', 'color_y', 'fx_x', 'fx_y')
AddFloatMembers -PARAMS @('reverb', 'delay', 'fx1', 'fx2')
AddBoolMembers -PARAMS @('postreverb', 'postdelay', 'postfx1', 'postfx2')
AddBoolMembers -PARAMS @('mono', 'vaio')
$this.comp = [StripComp]::new($index, $remote)
$this.gate = [StripGate]::new($index, $remote)
$this.denoiser = [StripDenoiser]::new($index, $remote)
$this.pitch = [StripPitch]::new($index, $remote)
$this.audibility = [StripAudibility]::new($index, $remote)
$this.eq = [StripEq]::new($index, $remote)
$this.device = [StripDevice]::new($index, $remote)
}
}
class StripComp : IStrip {
class StripKnob : IRemote {
StripKnob ([int]$index, [Object]$remote) : base ($index, $remote) {
}
hidden $_knob = $($this | Add-Member ScriptProperty 'knob' `
{
[math]::Round($this.Getter(''), 2)
} `
{
param([single]$arg)
return $this.Setter('', $arg)
}
)
}
class StripComp : StripKnob {
StripComp ([int]$index, [Object]$remote) : base ($index, $remote) {
AddFloatMembers -PARAMS @('gainin', 'ratio', 'threshold', 'attack', 'release', 'knee', 'gainout')
AddBoolMembers -PARAMS @('makeup')
}
[string] identifier () {
return "Strip[" + $this.index + "].Comp"
return 'Strip[' + $this.index + '].Comp'
}
hidden $_knob = $($this | Add-Member ScriptProperty 'knob' `
{
$this.Getter_String('')
} `
{
param($arg)
return $this.Setter('', $arg)
}
)
}
class StripGate : IStrip {
class StripGate : StripKnob {
StripGate ([int]$index, [Object]$remote) : base ($index, $remote) {
AddFloatMembers -PARAMS @('threshold', 'damping', 'bpsidechain', 'attack', 'hold', 'release')
}
[string] identifier () {
return "Strip[" + $this.index + "].Gate"
return 'Strip[' + $this.index + '].Gate'
}
hidden $_knob = $($this | Add-Member ScriptProperty 'knob' `
{
$this.Getter_String('')
} `
{
param($arg)
return $this.Setter('', $arg)
}
)
}
class StripDenoiser : IStrip {
class StripDenoiser : StripKnob {
StripDenoiser ([int]$index, [Object]$remote) : base ($index, $remote) {
AddFloatMembers -PARAMS @('threshold')
}
[string] identifier () {
return "Strip[" + $this.index + "].Denoiser"
}
hidden $_knob = $($this | Add-Member ScriptProperty 'knob' `
{
$this.Getter_String('')
} `
{
param($arg)
return $this.Setter('', $arg)
}
)
}
class StripEq : IStrip {
StripEq ([int]$index, [Object]$remote) : base ($index, $remote) {
AddBoolMembers -PARAMS @('on', 'ab')
}
[string] identifier () {
return "Strip[" + $this.index + "].EQ"
return 'Strip[' + $this.index + '].Denoiser'
}
}
class StripDevice : IStrip {
StripDevice ([int]$index, [Object]$remote) : base ($index, $remote) {
class StripPitch : IRemote {
StripPitch ([int]$index, [Object]$remote) : base ($index, $remote) {
AddBoolMembers -PARAMS @('on')
AddFloatMembers -PARAMS @('drywet', 'pitchvalue', 'loformant', 'medformant', 'hiformant')
}
[string] identifier () {
return "Strip[" + $this.index + "].Device"
return 'Strip[' + $this.index + '].Pitch'
}
hidden $_name = $($this | Add-Member ScriptProperty 'name' `
{
$this.Getter_String('name')
} `
{
return Write-Warning ("ERROR: $($this.identifier()).name is read only")
}
)
[void] RecallPreset ([int]$presetIndex) {
$this.Setter('RecallPreset', $presetIndex)
}
}
hidden $_sr = $($this | Add-Member ScriptProperty 'sr' `
{
$this.Getter('sr')
} `
{
return Write-Warning ("ERROR: $($this.identifier()).sr is read only")
}
)
class StripAudibility : StripKnob {
StripAudibility ([int]$index, [Object]$remote) : base ($index, $remote) {
}
hidden $_wdm = $($this | Add-Member ScriptProperty 'wdm' `
{
return Write-Warning ("ERROR: $($this.identifier()).wdm is write only")
} `
{
param($arg)
return $this.Setter('wdm', $arg)
}
)
[string] identifier () {
return 'Strip[' + $this.index + '].Audibility'
}
}
hidden $_ks = $($this | Add-Member ScriptProperty 'ks' `
{
return Write-Warning ("ERROR: $($this.identifier()).ks is write only")
} `
{
param($arg)
return $this.Setter('ks', $arg)
}
)
class StripEq : IOEq {
StripEq ([int]$index, [Object]$remote) : base ($index, $remote, 'Strip') {
}
hidden $_mme = $($this | Add-Member ScriptProperty 'mme' `
{
return Write-Warning ("ERROR: $($this.identifier()).mme is write only")
} `
{
param($arg)
return $this.Setter('mme', $arg)
}
)
[string] identifier () {
return 'Strip[' + $this.index + '].EQ'
}
}
hidden $_asio = $($this | Add-Member ScriptProperty 'asio' `
{
return Write-Warning ("ERROR: $($this.identifier()).asio is write only")
} `
{
param($arg)
return $this.Setter('asio', $arg)
}
)
class StripDevice : IODevice {
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 {
VirtualStrip ([int]$index, [Object]$remote) : base ($index, $remote) {
AddBoolMembers -PARAMS @('mc')
AddIntMembers -PARAMS @('k')
AddFloatMembers -PARAMS @('eqgain1', 'eqgain2', 'eqgain3')
AddAliasMembers -MAP @{
mono = 'mc'
karaoke = 'k'
bass = 'eqgain1'
low = 'eqgain1'
mid = 'eqgain2'
med = 'eqgain2'
treble = 'eqgain3'
high = 'eqgain3'
}
}
[void] AppGain ([string]$appname, [single]$gain) {
$this.Setter('AppGain', "(`"$appname`", $gain)")
}
[void] AppGain ([int]$appindex, [single]$gain) {
$this.Setter("App[$appindex].Gain", $gain)
}
[void] AppMute ([string]$appname, [bool]$mutestate) {
$this.Setter('AppMute', "(`"$appname`", $(if ($mutestate) { 1 } else { 0 })")
$this.Setter('AppMute', "(`"$appname`", $(if ($mutestate) { 1 } else { 0 }))")
}
[void] AppMute ([int]$appindex, [bool]$mutestate) {
$this.Setter("App[$appindex].Mute", $mutestate)
}
}

View File

@@ -1,84 +1,23 @@
class IVban {
[int32]$index
[Object]$remote
class Vban : IRemote {
[string]$direction
IVban ([int]$index, [Object]$remote, [string]$direction) {
$this.index = $index
$this.remote = $remote
Vban ([int]$index, [Object]$remote, [string]$direction) : base ($index, $remote) {
$this.direction = $direction
AddBoolMembers -PARAMS @('on')
AddStringMembers -PARAMS @('name', 'ip')
}
[string] identifier () {
return "vban." + $this.direction + "stream[" + $this.index + "]"
return 'vban.' + $this.direction + 'stream[' + $this.index + ']'
}
[single] Getter ($param) {
return $this.remote.Getter($this.Cmd($param))
}
[string] Getter_String ($param) {
$this.Cmd($param) | Write-Debug
return $this.remote.Getter_String($this.Cmd($param))
}
[void] Setter ($param, $val) {
"$($this.Cmd($param))=$val" | Write-Debug
$this.remote.Setter($this.Cmd($param), $val)
}
[string] Cmd ($param) {
if ([string]::IsNullOrEmpty($param)) {
return $this.identifier()
}
return "$($this.identifier()).$param"
}
}
class Vban : IVban {
Vban ([int]$index, [Object]$remote, [string]$direction) : base ($index, $remote, $direction) {
}
[string] ToString() {
return $this.GetType().Name + $this.index
}
hidden $_on = $($this | Add-Member ScriptProperty 'on' `
{
$this.Getter('on')
} `
{
param([bool]$arg)
$this._on = $this.Setter('on', $arg)
}
)
hidden $_name = $($this | Add-Member ScriptProperty 'name' `
{
$this.Getter_String('name')
} `
{
param([string]$arg)
$this._name = $this.Setter('name', $arg)
}
)
hidden $_ip = $($this | Add-Member ScriptProperty 'ip' `
{
$this.Getter_String('ip')
} `
{
param([string]$arg)
$this._ip = $this.Setter('ip', $arg)
}
)
hidden $_port = $($this | Add-Member ScriptProperty 'port' `
{
$this.Getter('port')
[int]$this.Getter('port')
} `
{
param([string]$arg)
param([int]$arg)
if ($arg -ge 1024 -and $arg -le 65535) {
$this._port = $this.Setter('port', $arg)
}
@@ -87,40 +26,72 @@ class Vban : IVban {
}
}
)
}
class VbanAudio : Vban {
VbanAudio ([int]$index, [Object]$remote, [string]$direction) : base ($index, $remote, $direction) {
AddIntMembers -PARAMS @('quality', 'route')
}
}
class VbanMidi : Vban {
VbanMidi ([int]$index, [Object]$remote, [string]$direction) : base ($index, $remote, $direction) {
}
}
class VbanText : Vban {
VbanText ([int]$index, [Object]$remote, [string]$direction) : base ($index, $remote, $direction) {
}
}
class VbanVideo : Vban {
VbanVideo ([int]$index, [Object]$remote, [string]$direction) : base ($index, $remote, $direction) {
}
}
class VbanInAudio : VbanAudio {
VbanInAudio ([int]$index, [Object]$remote) : base ($index, $remote, 'in') {
AddIntMembers -ReadOnly -PARAMS @('sr', 'channel')
}
hidden $_bit = $($this | Add-Member ScriptProperty 'bit' `
{
$val = if ($this.Getter('bit') -eq 1) { 16 } else { 24 }
return $val
} `
{
Write-Warning ("ERROR: $($this.identifier()).bit is read only")
}
)
}
class VbanInMidi : VbanMidi {
VbanInMidi ([int]$index, [Object]$remote) : base ($index, $remote, 'in') {
}
}
class VbanInText : VbanText {
VbanInText ([int]$index, [Object]$remote) : base ($index, $remote, 'in') {
}
}
class VbanOutAudio : VbanAudio {
VbanOutAudio ([int]$index, [Object]$remote) : base ($index, $remote, 'out') {
AddIntMembers -PARAMS @('channel')
}
hidden $_sr = $($this | Add-Member ScriptProperty 'sr' `
{
$this.Getter('sr')
[int]$this.Getter('sr')
} `
{
param([int]$arg)
if ($this.direction -eq "in") { Write-Warning ('Error, read only value') }
else {
$opts = @(11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
if ($opts.Contains($arg)) {
$this._port = $this.Setter('sr', $arg)
}
else {
Write-Warning ('Expected one of', $opts)
}
$opts = @(11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
if ($opts.Contains($arg)) {
$this._sr = $this.Setter('sr', $arg)
}
}
)
hidden $_channel = $($this | Add-Member ScriptProperty 'channel' `
{
$this.Getter('channel')
} `
{
param([int]$arg)
if ($this.direction -eq "in") { Write-Warning ('Error, read only value') }
else {
if ($arg -ge 1 -and $arg -le 8) {
$this._channel = $this.Setter('channel', $arg)
}
else {
Write-Warning ('Expected value from 1 to 8')
}
Write-Warning ('Expected one of', $opts)
}
}
)
@@ -132,78 +103,108 @@ class Vban : IVban {
} `
{
param([int]$arg)
if ($this.direction -eq "in") { Write-Warning ('Error, read only value') }
if (@(16, 24).Contains($arg)) {
$val = if ($arg -eq 16) { 1 } else { 2 }
$this._bit = $this.Setter('bit', $val)
}
else {
if (@(16, 24).Contains($arg)) {
$val = if ($arg -eq 16) { 1 } else { 2 }
$this._bit = $this.Setter('bit', $val)
}
else {
Write-Warning ('Expected value 16 or 24')
}
Write-Warning ('Expected value 16 or 24')
}
}
)
}
hidden $_quality = $($this | Add-Member ScriptProperty 'quality' `
{
$this.Getter('quality')
} `
{
param([int]$arg)
if ($this.direction -eq "in") { Write-Warning ('Error, read only value') }
else {
if ($arg -ge 0 -and $arg -le 4) {
$this._quality = $this.Setter('quality', $arg)
}
else {
Write-Warning ('Expected value from 0 to 4')
}
}
}
)
class VbanOutMidi : VbanMidi {
VbanOutMidi ([int]$index, [Object]$remote) : base ($index, $remote, 'out') {
}
hidden $_route = $($this | Add-Member ScriptProperty 'route' `
{
$this.Getter('route')
[string]$val = ''
switch ($this.Getter('route')) {
0 { $val = 'none' }
1 { $val = 'midi_in' }
2 { $val = 'aux_in' }
4 { $val = 'vban_in' }
7 { $val = 'all_in' }
8 { $val = 'midi_out' }
}
return $val
} `
{
param([int]$arg)
if ($this.direction -eq "in") { Write-Warning ('Error, read only value') }
else {
if ($arg -ge 0 -and $arg -le 8) {
$this._route = $this.Setter('route', $arg)
}
else {
Write-Warning ('Expected value from 0 to 8')
}
param([string]$arg)
[int]$val = 0
switch ($arg) {
'none' { $val = 0 }
'midi_in' { $val = 1 }
'aux_in' { $val = 2 }
'vban_in' { $val = 4 }
'all_in' { $val = 7 }
'midi_out' { $val = 8 }
default { Write-Warning ("route got: $arg, expected one of 'none', 'midi_in', 'aux_in', 'vban_in', 'all_in', 'midi_out'") }
}
$this._route = $this.Setter('route', $val)
}
)
}
class VbanInstream : Vban {
VbanInstream ([int]$index, [Object]$remote, [string]$direction) : base ($index, $remote, $direction) {
class VbanOutVideo : VbanVideo {
VbanOutVideo ([int]$index, [Object]$remote) : base ($index, $remote, 'out') {
AddIntMembers -PARAMS @('vfps', 'vquality')
AddIntMembers -WriteOnly -PARAMS @('route')
AddBoolMembers -PARAMS @('vcursor')
}
hidden $_vformat = $($this | Add-Member ScriptProperty 'vformat' `
{
[string]$val = ''
switch ($this.Getter('vformat')) {
1 { $val = 'png' }
2 { $val = 'jpg' }
}
return $val
} `
{
param([string]$arg)
[int]$val = 0
switch ($arg) {
'png' { $val = 1 }
'jpg' { $val = 2 }
default { Write-Warning ("vformat got: $arg, expected one of 'png', 'jpg'") }
}
$this._vformat = $this.Setter('vformat', $val)
}
)
}
class VbanOutstream : Vban {
VbanOutstream ([int]$index, [Object]$remote, [string]$direction) : base ($index, $remote, $direction) {
}
}
function Make_Vban ([Object]$remote) {
[System.Collections.ArrayList]$instream = @()
[System.Collections.ArrayList]$outstream = @()
0..$($remote.kind.vban_in - 1) | ForEach-Object {
[void]$instream.Add([VbanInstream]::new($_, $remote, "in"))
$totalInstreams = $remote.kind.vban.in + $remote.kind.vban.midi + $remote.kind.vban.text
$totalOutstreams = $remote.kind.vban.out + $remote.kind.vban.midi + $remote.kind.vban.video
for ($i = 0; $i -lt $totalInstreams; $i++) {
if ($i -lt $remote.kind.vban.in) {
[void]$instream.Add([VbanInAudio]::new($i, $remote))
}
elseif ($i -lt ($remote.kind.vban.in + $remote.kind.vban.midi)) {
[void]$instream.Add([VbanInMidi]::new($i, $remote))
}
else {
[void]$instream.Add([VbanInText]::new($i, $remote))
}
}
0..$($remote.kind.vban_out - 1) | ForEach-Object {
[void]$outstream.Add([VbanOutstream]::new($_, $remote, "out"))
for ($i = 0; $i -lt $totalOutstreams; $i++) {
if ($i -lt $remote.kind.vban.out) {
[void]$outstream.Add([VbanOutAudio]::new($i, $remote))
}
elseif ($i -lt ($remote.kind.vban.out + $remote.kind.vban.midi)) {
[void]$outstream.Add([VbanOutMidi]::new($i, $remote))
}
else {
[void]$outstream.Add([VbanOutVideo]::new($i, $remote))
}
}
$CustomObject = [pscustomobject]@{
@@ -213,11 +214,25 @@ function Make_Vban ([Object]$remote) {
$CustomObject | Add-Member ScriptProperty 'enable' `
{
return Write-Warning ("ERROR: vban.enable is write only")
return [bool]( Param_Get -PARAM 'vban.enable' )
} `
{
param([bool]$arg)
Param_Set -PARAM 'vban.Enable' -Value $(if ($arg) { 1 } else { 0 })
Param_Set -PARAM 'vban.enable' -Value $(if ($arg) { 1 } else { 0 })
}
$CustomObject | Add-Member ScriptProperty 'port' `
{
return [int]( Param_Get -PARAM 'vban.instream[0].port' )
} `
{
param([int]$arg)
if ($arg -ge 1024 -and $arg -le 65535) {
Param_Set -PARAM 'vban.instream[0].port' -Value $arg
}
else {
Write-Warning ('Expected value from 1024 to 65535')
}
}
$CustomObject

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,13 @@ Describe -Tag 'lower', -TestName 'All Lower Tests' {
Describe 'Macrobutton Tests' -ForEach @(
@{ Value = 1; Expected = 1 }
@{ Value = 0; Expected = 0 }
){
) {
Context 'buttons 0, 69' -ForEach @(
@{ Index = 0 }, @{ Index = 69 }
){
) {
Context 'state, stateonly and trigger' -ForEach @(
@{ Mode = 1 }, @{ Mode = 2 }, @{ Mode = 3 }
){
) {
It "Should set and get macrobutton[$index] State" {
MB_Set -ID $index -SET $value -MODE $mode
MB_Get -ID $index -MODE $mode | Should -Be $expected
@@ -24,13 +24,13 @@ Describe -Tag 'lower', -TestName 'All Lower Tests' {
Describe 'Set and Get Param Float Tests' -ForEach @(
@{ Value = 1; Expected = 1 }
@{ Value = 0; Expected = 0 }
){
) {
Context 'Strip, one physical one virtual' -ForEach @(
@{ Index = $phys_in }, @{ Index = $virt_in }
){
) {
Context 'mute, mono, A1, B2' -ForEach @(
@{ param = "mute" }, @{ param = "A1" }
){
@{ param = 'mute' }, @{ param = 'A1' }
) {
It "Should set Strip[0].$param to 1" {
Param_Set -PARAM "Strip[$index].$param" -VALUE $value
Param_Get -PARAM "Strip[$index].$param" | Should -Be $expected
@@ -42,10 +42,10 @@ Describe -Tag 'lower', -TestName 'All Lower Tests' {
Describe 'Set and Get Param String Tests' -ForEach @(
@{ Value = 'test0'; Expected = 'test0' }
@{ Value = 'test1'; Expected = 'test1' }
){
) {
Context 'Strip, one physical one virtual' -ForEach @(
@{ Index = $phys_in }, @{ Index = $virt_in }
){
) {
It "Should set Strip[$index].Label to $value" {
Param_Set -PARAM "Strip[$index].Label" -VALUE $value
Param_Get -PARAM "Strip[$index].Label" -IS_STRING $true | Should -Be $expected

View File

@@ -1,76 +0,0 @@
Param([String]$tag, [Int]$num = 1, [switch]$log, [string]$kind = "potato")
Import-Module .\lib\Voicemeeter.psm1
Function ParseLog {
Param([String]$logfile)
$summary_file = Join-Path $PSScriptRoot "_summary.log"
if (Test-Path $summary_file) { Clear-Content $summary_file }
$PASSED_PATTERN = "^PassedCount\s+:\s(\d+)"
$FAILED_PATTERN = "^FailedCount\s+:\s(\d+)"
$DATA = @{
"passed" = 0
"failed" = 0
}
ForEach ($line in `
$(Get-Content -Path "${logfile}")) {
if ($line -match $PASSED_PATTERN) {
$DATA["passed"] += $Matches[1]
}
elseif ($line -match $FAILED_PATTERN) {
$DATA["failed"] += $Matches[1]
}
}
"=========================`n" + `
"$num tests run:`n" + `
"=========================" | Tee-Object -FilePath $summary_file -Append
$DATA | ForEach-Object { $_ } | Tee-Object -FilePath $summary_file -Append
}
function main() {
try {
$vmr = Connect-Voicemeeter -Kind $kind
$vmr.command.RunMacrobuttons() # ensure macrobuttons is running before we begin
Write-Host "Running tests for $vmr"
# test boundaries by kind
$phys_in = $vmr.kind.p_in - 1
$virt_in = $vmr.kind.p_in + $vmr.kind.v_in - 1
$phys_out = $vmr.kind.p_out - 1
$virt_out = $vmr.kind.p_out + $vmr.kind.v_out - 1
$vban_in = $vmr.kind.vban_in - 1
$vban_out = $vmr.kind.vban_out - 1
# skip conditions by kind
$ifBasic = $vmr.kind.name -eq "basic"
$ifBanana = $vmr.kind.name -eq "banana"
$ifPotato = $vmr.kind.name -eq "potato"
$ifNotBasic = $vmr.kind.name -ne "basic"
$ifNotBanana = $vmr.kind.name -ne "banana"
$ifNotPotato = $vmr.kind.name -ne "potato"
$logfile = Join-Path $PSScriptRoot "_results.log"
if (Test-Path $logfile) { Clear-Content $logfile }
1..$num | ForEach-Object {
if ($log) {
"Running test $_ of $num" | Tee-Object -FilePath $logfile -Append
Invoke-Pester -Tag $tag -PassThru | Tee-Object -FilePath $logfile -Append
}
else {
"Running test $_ of $num"
Invoke-Pester -Tag $tag -PassThru
}
}
if ($log) { Parselog -logfile $logfile }
}
finally { Disconnect-Voicemeeter }
}
main

89
tests/run.ps1 Normal file
View File

@@ -0,0 +1,89 @@
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Target = "variablename")]
Param([String]$tag, [string]$kind = 'potato', [string]$recDir = (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'Voicemeeter'))
Import-Module (Join-Path (Split-Path $PSScriptRoot -Parent) 'lib\Voicemeeter.psm1') -Force
function Test-RecDir ([object]$vmr, [string]$recDir) {
$prefix = 'temp'
$filetype = 'wav'
$vmr.recorder.prefix = $prefix
$vmr.recorder.filetype = $filetype
try {
$start = Get-Date
$vmr.recorder.record()
Start-Sleep -Milliseconds 2000
$tmp = Get-ChildItem -Path $recDir -Filter ("{0}*.{1}" -f $prefix, $filetype) -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -gt $start } |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if (-not $tmp) {
throw "'$filetype' file with prefix '$prefix' was not found in '$recDir'."
}
$vmr.recorder.stop()
$vmr.recorder.eject()
Start-Sleep -Milliseconds 500
}
catch {
Write-Warning "Failed to record pre-check clip: $_"
}
if (Test-Path $tmp) {
Remove-Item -Path $tmp -Force
return $false
}
else {
Write-Warning "Recorder output not found at given path: $tmp"
Write-Warning "Skipping Recording/Playback tests. Provide custom path with -recDir"
return $true
}
}
function main() {
try {
$vmr = Connect-Voicemeeter -Kind $kind
$vmr.command.RunMacrobuttons() # ensure macrobuttons is running before we begin
Write-Host "Running tests for $vmr"
# test boundaries by kind
$phys_in = $vmr.kind.p_in - 1
$virt_in = $vmr.kind.p_in + $vmr.kind.v_in - 1
$phys_out = $vmr.kind.p_out - 1
$virt_out = $vmr.kind.p_out + $vmr.kind.v_out - 1
$vban_inA = $vmr.kind.vban.in - 1
$vban_inM = $vmr.kind.vban.in + $vmr.kind.vban.midi - 1
$vban_inT = $vmr.kind.vban.in + $vmr.kind.vban.midi + $vmr.kind.vban.text - 1
$vban_outA = $vmr.kind.vban.out - 1
$vban_outM = $vmr.kind.vban.out + $vmr.kind.vban.midi - 1
$vban_outV = $vmr.kind.vban.out + $vmr.kind.vban.midi + $vmr.kind.vban.video - 1
$insert = $vmr.kind.insert - 1
$composite = $vmr.kind.composite - 1
$strip_ch = $vmr.kind.eq_ch['strip'] - 1
$bus_ch = $vmr.kind.eq_ch['bus'] - 1
$cells = $vmr.kind.cells - 1
# skip conditions by kind
$ifBasic = $vmr.kind.name -eq 'basic'
$ifNotBasic = $vmr.kind.name -ne 'basic'
$ifNotPotato = $vmr.kind.name -ne 'potato'
# recording directory: default ~/My Documents/Voicemeeter, override if custom
$recDir = [System.IO.Path]::GetFullPath($recDir)
if ($ifBasic) {
$ifCustomDir = $ifBasic # basic can't record, so skip the test
}
else {
$ifCustomDir = Test-RecDir -vmr $vmr -recDir $recDir # avoid creating files we can't delete
}
Invoke-Pester -Tag $tag -PassThru | Out-Null
}
finally { Disconnect-Voicemeeter }
}
main