io.ps1

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