Merge pull request #27 from pblivingston/recorder-commands

Recorder commands
This commit is contained in:
Onyx and Iris 2025-12-04 05:45:40 +00:00 committed by GitHub
commit 77a8792377
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 378 additions and 120 deletions

View File

@ -15,6 +15,9 @@ AddActionMembers now adds ScriptMethods instead of ScriptProperties:
- See Command section of README for details on using special commands
- See Recorder section of README for details on using playback/record actions
Deprecated Recorder.Loop removed: use Recorder.Mode.Loop
Recorder.FileType changed from method to write-only property
### Added
- IRemote base class
@ -31,6 +34,11 @@ AddActionMembers now adds ScriptMethods instead of ScriptProperties:
- ip, write-only
- Bus.Sel, Bus.Monitor, Bus.Vaio
- Bus.Mode.Set($mode)
- Recorder.Armedbus
- Recorder.PreRecTime
- Recorder.Prefix
- Recorder.Eject() references 'Command.Eject'
- Recorder.State
### Changed
@ -43,6 +51,8 @@ AddActionMembers now adds ScriptMethods instead of ScriptProperties:
- Bus.Levels.Convert return type [float] -> [single] for naming consistency, no functional change
- Meta: AddBoolMembers, AddIntMembers $arg types for consistency
- Device: explicit $arg types for consistency
- Recorder.Armstrip|Armbus -> BoolArrayMember: now have .Get()
- Cast Recorder getters to types for consistency
### Fixed
@ -53,6 +63,7 @@ AddActionMembers now adds ScriptMethods instead of ScriptProperties:
- 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)
## [3.3.0] - 2024-06-29

View File

@ -527,9 +527,15 @@ The following commands are available:
- A1 - A5: bool
- B1 - B3: bool
- gain: float, from -60.0 to 12.0
- 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:
@ -541,9 +547,9 @@ The following methods are available:
- Rew()
- Record()
- Pause()
- Eject()
- Load($filepath): string
- GoTo($timestring): string, must match the format 'hh:mm:ss'
- FileType($format): string, ('wav', 'aiff', 'bwf', 'mp3')
example:
@ -571,9 +577,10 @@ $vmr.recorder.mode.loop = $true
#### ArmStrip[i]|ArmBus[i]
The following method is available:
The following methods are available:
- Set($val): bool
- Get()
example:

View File

@ -2,20 +2,30 @@ 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))
}
$this.armbus = @()
0..($remote.kind.p_out + $remote.kind.v_out - 1) | ForEach-Object {
$this.armbus.Add([RecorderArmBus]::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))
}
AddActionMembers -PARAMS @('play', 'stop', 'pause', 'replay', 'record', 'ff', 'rew')
$this.armbus = @()
$busCount = $($remote.kind.p_out + $remote.kind.v_out)
for ($i = 0; $i -lt $busCount; $i++) {
$this.armbus.Add([BoolArrayMember]::new($i, 'armbus', $this))
}
$this.states = @('play', 'stop', 'record', 'pause')
AddActionMembers -PARAMS $this.states
AddActionMembers -PARAMS @('replay', 'ff', 'rew')
AddFloatMembers -PARAMS @('gain')
AddIntMembers -PARAMS @('prerectime')
AddChannelMembers
}
@ -23,78 +33,9 @@ class Recorder : IRemote {
return 'Recorder'
}
hidden $_loop = $($this | Add-Member ScriptProperty 'loop' `
{
[bool]$this.mode.loop
} `
{
param($arg)
$this.mode.loop = $arg
[void] Eject () {
$this.remote.Setter('Command.Eject', 1)
}
)
hidden $_samplerate = $($this | Add-Member ScriptProperty 'samplerate' `
{
$this.Getter('samplerate')
} `
{
param([int]$arg)
$opts = @(22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
if ($opts.Contains($arg)) {
$this._samplerate = $this.Setter('samplerate', $arg)
}
else {
"samplerate got: $arg, expected one of $opts" | Write-Warning
}
}
)
hidden $_bitresolution = $($this | Add-Member ScriptProperty 'bitresolution' `
{
$this.Getter('bitresolution')
} `
{
param([int]$arg)
$opts = @(8, 16, 24, 32)
if ($opts.Contains($arg)) {
$this._bitresolution = $this.Setter('bitresolution', $arg)
}
else {
"bitresolution got: $arg, expected one of $opts" | Write-Warning
}
}
)
hidden $_channel = $($this | Add-Member ScriptProperty 'channel' `
{
$this.Getter('channel')
} `
{
param([int]$arg)
if ($arg -ge 1 -and $arg -le 8) {
$this._channel = $this.Setter('channel', $arg)
}
else {
"channel got: $arg, expected value from 1 to 8" | Write-Warning
}
}
)
hidden $_kbps = $($this | Add-Member ScriptProperty 'kbps' `
{
$this.Getter('kbps')
} `
{
param([int]$arg)
$opts = @(32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320)
if ($opts.Contains($arg)) {
$this._kbps = $this.Setter('kbps', $arg)
}
else {
"kbps got: $arg, expected one of $opts" | Write-Warning
}
}
)
[void] Load ([string]$filename) {
$this.Setter('load', $filename)
@ -112,17 +53,142 @@ class Recorder : IRemote {
}
}
[void] FileType($format) {
hidden $_samplerate = $($this | Add-Member ScriptProperty 'samplerate' `
{
[int]$this.Getter('samplerate')
} `
{
param([int]$arg)
$opts = @(22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
if ($opts.Contains($arg)) {
$this._samplerate = $this.Setter('samplerate', $arg)
}
else {
"samplerate got: $arg, expected one of $opts" | Write-Warning
}
}
)
hidden $_bitresolution = $($this | Add-Member ScriptProperty 'bitresolution' `
{
[int]$this.Getter('bitresolution')
} `
{
param([int]$arg)
$opts = @(8, 16, 24, 32)
if ($opts.Contains($arg)) {
$this._bitresolution = $this.Setter('bitresolution', $arg)
}
else {
"bitresolution got: $arg, expected one of $opts" | Write-Warning
}
}
)
hidden $_channel = $($this | Add-Member ScriptProperty 'channel' `
{
[int]$this.Getter('channel')
} `
{
param([int]$arg)
$opts = @(2, 4, 6, 8)
if ($opts.Contains($arg)) {
$this._channel = $this.Setter('channel', $arg)
}
else {
"channel got: $arg, expected one of $opts" | Write-Warning
}
}
)
hidden $_kbps = $($this | Add-Member ScriptProperty 'kbps' `
{
[int]$this.Getter('kbps')
} `
{
param([int]$arg)
$opts = @(32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320)
if ($opts.Contains($arg)) {
$this._kbps = $this.Setter('kbps', $arg)
}
else {
"kbps got: $arg, expected one of $opts" | Write-Warning
}
}
)
hidden $_prefix = $($this | Add-Member ScriptProperty 'prefix' `
{
return Write-Warning ("ERROR: $($this.identifier()).prefix is write only")
} `
{
param([string]$arg)
$this._prefix = $this.Setter('prefix', $arg)
}
)
hidden $_filetype = $($this | Add-Member ScriptProperty 'filetype' `
{
return Write-Warning ("ERROR: $($this.identifier()).filetype is write only")
} `
{
param([string]$arg)
[int]$val = 0
switch ($format) {
switch ($arg) {
'wav' { $val = 1 }
'aiff' { $val = 2 }
'bwf' { $val = 3 }
'mp3' { $val = 100 }
default { "Filetype() got: $format, expected one of 'wav', 'aiff', 'bwf', 'mp3'" }
default { "Filetype() got: $arg, expected one of 'wav', 'aiff', 'bwf', 'mp3'" }
}
$this.Setter('filetype', $val)
$this._filetype = $this.Setter('filetype', $val)
}
)
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")
}
}
)
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)
}
)
}
class RecorderMode : IRemote {
@ -135,33 +201,6 @@ class RecorderMode : IRemote {
}
}
class RecorderArm : IRemote {
RecorderArm ([int]$index, [Object]$remote) : base ($index, $remote) {
}
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)]"
}
}
function Make_Recorder ([Object]$remote) {
return [Recorder]::new($remote)
}

View File

@ -157,8 +157,30 @@ Describe -Tag 'higher', -TestName 'All Higher Tests' {
$vmr.recorder.B1 | Should -Be $expected
}
It 'Should set and get Recorder.loop' {
$vmr.recorder.loop = $value
It 'Should set and get Recorder.armstrip[i]' -ForEach @(
@{ Index = $phys_in }, @{ Index = $virt_in }
) {
$vmr.recorder.armstrip[$index].set($value)
$vmr.recorder.armstrip[$index].get() | Should -Be $value
}
It 'Should set and get Recorder.armbus[i]' -ForEach @(
@{ Index = $phys_out }, @{ Index = $virt_out }
) {
$vmr.recorder.armbus[$index].set($value)
$vmr.recorder.armbus[$index].get() | Should -Be $value
}
Context 'Mode' {
It 'Should set and get Recorder.mode.multitrack' {
$vmr.recorder.mode.multitrack = $value
$vmr.recorder.mode.multitrack | Should -Be $expected
}
It 'Should set and get Recorder.mode.loop' {
$vmr.recorder.mode.loop = $value
$vmr.recorder.mode.loop | Should -Be $expected
}
}
}
@ -603,6 +625,43 @@ Describe -Tag 'higher', -TestName 'All Higher Tests' {
}
}
}
Context 'Recorder' -Skip:$ifBasic {
It 'Should set and get Recorder.armedbus' -ForEach @(
@{ Value = $phys_out }, @{ Value = $virt_out }
) {
$vmr.recorder.armedbus = $value
$vmr.recorder.armedbus | Should -Be $value
}
It 'Should set and get Recorder.prerectime' -ForEach @(
@{ Value = 5 }, @{ Value = 20 }
) {
$vmr.recorder.prerectime = $value
$vmr.recorder.prerectime | Should -Be $value
}
It 'Should set and get Recorder.samplerate' -ForEach @(
@{ Value = 44100 }, @{ Value = 48000 }
) {
$vmr.recorder.samplerate = $value
$vmr.recorder.samplerate | Should -Be $value
}
It 'Should set and get Recorder.bitresolution' -ForEach @(
@{ Value = 24 }, @{ Value = 16 }
) {
$vmr.recorder.bitresolution = $value
$vmr.recorder.bitresolution | Should -Be $value
}
It 'Should set and get Recorder.kbps' -ForEach @(
@{ Value = 96 }, @{ Value = 192 }
) {
$vmr.recorder.kbps = $value
$vmr.recorder.kbps | Should -Be $value
}
}
}
Describe 'String Tests' -Tag 'string' {
@ -845,5 +904,105 @@ Describe -Tag 'higher', -TestName 'All Higher Tests' {
}
}
}
Context 'Recorder' -Skip:$ifBasic {
It 'Should record a test file, eject, and load it back' -Skip:$ifCustomDir {
try {
$prefix = 'stringtest'
$filetype = 'wav'
$vmr.recorder.prefix = $prefix
$vmr.recorder.filetype = $filetype
$vmr.recorder.state = 'record'
$stamp = '{0:yyyy-MM-dd} at {0:HH}h{0:mm}m{0:ss}s' -f (Get-Date)
$vmr.recorder.state | Should -Be 'record'
Start-Sleep -Milliseconds 2000
$tmp = [System.IO.Path]::Combine($recDir, ("{0} {1}.{2}" -f $prefix, $stamp, $filetype))
$vmr.recorder.state = 'stop'
$vmr.recorder.eject()
Start-Sleep -Milliseconds 500
$vmr.recorder.state = 'play'
$vmr.recorder.state | Should -Be 'stop' # because no file is loaded
$vmr.recorder.load($tmp)
Start-Sleep -Milliseconds 500
if (-not $vmr.recorder.mode.playonload) {
$vmr.recorder.state = 'play'
}
$vmr.recorder.state | Should -Be 'play'
}
finally {
$vmr.recorder.state = 'stop'
$vmr.recorder.eject()
Start-Sleep -Milliseconds 500
if (Test-Path $tmp) {
Remove-Item -Path $tmp -Force
}
else {
throw "Recording file $tmp was not found."
}
}
}
}
}
Describe 'Action Tests' -Tag 'action' {
Context 'Recorder' -Skip:$ifBasic {
Context 'Recording/Playback' -Skip:$ifCustomDir {
BeforeAll {
$prefix = 'actiontest'
$filetype = 'wav'
$vmr.recorder.prefix = $prefix
$vmr.recorder.filetype = $filetype
}
BeforeEach {
$vmr.recorder.record()
$stamp = '{0:yyyy-MM-dd} at {0:HH}h{0:mm}m{0:ss}s' -f (Get-Date)
Start-Sleep -Milliseconds 2000
$tmp = [System.IO.Path]::Combine($recDir, ("{0} {1}.{2}" -f $prefix, $stamp, $filetype))
$vmr.recorder.pause()
Start-Sleep -Milliseconds 500
}
AfterEach {
$vmr.recorder.stop()
$vmr.recorder.eject()
Start-Sleep -Milliseconds 500
if (Test-Path $tmp) {
Remove-Item -Path $tmp -Force
}
else {
throw "Recording file $tmp was not found."
}
}
It 'Should call Recorder.record()' {
$vmr.recorder.record()
$vmr.recorder.state | Should -Be 'record'
}
It 'Should call Recorder.pause()' {
$vmr.recorder.record()
Start-Sleep -Milliseconds 500
$vmr.recorder.pause()
$vmr.recorder.state | Should -Be 'pause'
}
It 'Should call Recorder.play()' {
$vmr.recorder.stop()
Start-Sleep -Milliseconds 500
$vmr.recorder.play()
$vmr.recorder.state | Should -Be 'play'
}
}
}
}
}

View File

@ -1,8 +1,41 @@
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Target = "variablename")]
Param([String]$tag, [string]$kind = 'potato')
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 {
$vmr.recorder.record()
$stamp = '{0:yyyy-MM-dd} at {0:HH}h{0:mm}m{0:ss}s' -f (Get-Date)
Start-Sleep -Milliseconds 2000
$tmp = Join-Path $recDir ("{0} {1}.{2}" -f $prefix, $stamp, $filetype)
$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
@ -30,6 +63,15 @@ function main() {
$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 }