diff --git a/CHANGELOG.md b/CHANGELOG.md index 78c3bc5..a2e8e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 76a0ed0..8adc862 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/lib/recorder.ps1 b/lib/recorder.ps1 index c8d95dd..b07c611 100644 --- a/lib/recorder.ps1 +++ b/lib/recorder.ps1 @@ -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)) + $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') + 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 - } - ) - - 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] Eject () { + $this.remote.Setter('Command.Eject', 1) + } [void] Load ([string]$filename) { $this.Setter('load', $filename) @@ -112,17 +53,142 @@ class Recorder : IRemote { } } - [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 $_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 + } } - $this.Setter('filetype', $val) - } + ) + + 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 ($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) + } + ) + + 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) } diff --git a/tests/higher.Tests.ps1 b/tests/higher.Tests.ps1 index 7e45b03..02f8e38 100644 --- a/tests/higher.Tests.ps1 +++ b/tests/higher.Tests.ps1 @@ -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' + } + } + } } } diff --git a/tests/run.ps1 b/tests/run.ps1 index 02daa8b..ebf19fe 100644 --- a/tests/run.ps1 +++ b/tests/run.ps1 @@ -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 }