PoshPulseAudio.psm1

. $PSScriptRoot/PAParsing.ps1
. $PSScriptRoot/PADataStructs.ps1

function New-PulseAudioProfile {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $profileData
    )
    $pattern = "^(?<name>.*): (?<displayName>.*) \(sinks: (?<sinkCount>[0-9]+), sources: (?<sourceCount>[0-9]+), priority: (?<priority>[0-9]+), available: (?<available>yes|no)"
    if ($profileData -match $pattern) {
        return [PulseAudioProfile] @{
            SymbolicName = $Matches.name
            DisplayName = $Matches.displayName
            SinkCount = $Matches.sinkCount
            SourceCount = $Matches.sourceCount
            Priority = $Matches.priority
            Available = $Matches.available -eq "yes"
        }
    } else {
        Write-Warning "Unexpected profile data format: $profileData"
    }
}

function New-PulseAudioPort {
    [CmdletBinding()]
    param (
        [Parameter()]
        [IndentedDataItem]
        $portData
    )
    $pattern = "^(?<name>.*): (?<displayName>.*) \(.*, (?<available>.*)\)"
    if ($portData.Value -match $pattern) {
        $properties = $portData.FindChild("^Properties:.*")
        $productName = ($null -eq $properties) ? $null : $properties.ParseChildValue("device.product.name = ")
        $profileNames = $portData.ParseChildValue("Part of profile\(s\): ")
        return [PulseAudioPort] @{
            SymbolicName = $Matches.name
            DisplayName = $Matches.displayName
            ProductName = ($null -eq $productName) ? $null : $productName.Trim('"')
            Available = $Matches.available -ne "not available"
            ProfileNames = $profileNames -split ", "
        }
    } else {
        Write-Warning "Unexpected port data format: $($portData.Value)"
    }
}

<#
    .SYNOPSIS
    Gets all Pulse Audio cards. (See `pactl list cards` for details)

    .PARAMETER Name
    The name of the desired cards, or a pattern to match the name against.

    .OUTPUTS
    A list of PulseAudioCards matching the provided pattern, or all cards if no name was specified.
#>

function Get-PACard {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $Name
    )
    pactl list cards |
        Remove-InvalidNewlines |
        Split-IndentedData |
        ForEach-Object {
            $profiles = $_.FindChild("^Profiles:.*").Children | ForEach-Object { New-PulseAudioProfile $_.Value }
            $activeProfileName = $_.ParseChildValue("Active Profile: ")
            $ports = $_.FindChild("^Ports:.*").Children | ForEach-Object { New-PulseAudioPort $_ }
            [PulseAudioCard] @{
                Index = $_.ParseValue("Card #")
                Name = $_.ParseChildValue("Name: ")
                Driver = $_.ParseChildValue("Driver: ")
                Profiles = $profiles
                ActiveProfile = $profiles | Where-Object { $_.SymbolicName -eq $activeProfileName } | Select-Object -First 1
                Ports = $ports
            }
        } |
        # Do not filter on $Name if it is not set
        Where-Object { -not $Name -or $_.Name -like $Name }
}

<#
    .SYNOPSIS
    Sets the active profile of the specified Pulse Audio card. (See `pactl set-card-profile` for details)

    .PARAMETER PACard
    The PulseAudioCard to be updated, or the name of the card.

    .PARAMETER PAProfile
    The PulseAudioProfile to be used, or the name of the profile.

    .INPUTS
    See parameter PACard.

    .OUTPUTS
    None
#>

function Set-PACardProfile {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]
        $PACard,
        [Parameter(Mandatory)]
        [object]
        $PAProfile
    )
    # Because the test dot-sources PADataStructs.ps1 separately, the -is operator cannot be used
    # here because the type handle is not the same
    if ($PACard.GetType().Name -eq 'PulseAudioCard') {
        $PACardName = $PACard.Name
    } else {
        $PACardName = [string] $PACard
    }

    if ($PAProfile.GetType().Name -eq 'PulseAudioProfile') {
        $PAProfileName = $PAProfile.SymbolicName
    } else {
        $PAProfileName = [string] $PAProfile
    }

    $output = pactl set-card-profile $PACardName $PAProfileName
    if ($output) {            
        throw "Could not set profile for $PACardName to $PAProfileName`: $output"
    }
}

<#
    .SYNOPSIS
    Gets all Pulse Audio output devices. (See `pactl list sinks` for details)

    .PARAMETER Name
    The name of the desired sink, or a pattern to match the name against.

    .OUTPUTS
    A list of PulseAudioSinks matching the provided pattern, or all sinks if no name was specified.
#>

function Get-PASink {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $Name
    )
    pactl list sinks |
        Split-IndentedData |
        ForEach-Object {
            [PulseAudioSink] @{
                Index = $_.ParseValue("Sink #")
                Name = $_.ParseChildValue("Name: ")
                Description = $_.ParseChildValue("Description: ")
            }
        } |
        # Do not filter on $Name if it is not set
        Where-Object { -not $Name -or $_.Name -like $Name }
}

<#
    .SYNOPSIS
    Gets all applications outputting audio to Pulse Audio output devices. (See `pactl list sink-inputs` for details)

    .OUTPUTS
    A list of PulseAudioSinkInputs.
#>

function Get-PASinkInput {
    pactl list sink-inputs |
        Split-IndentedData |
        ForEach-Object {
            [PulseAudioSinkInput] @{
                Index = $_.ParseValue("Sink Input #")
                ApplicationName = $_.FindChild("Properties").ParseChildValue("application.name = ").Trim('"')
                BinaryName = $_.FindChild("Properties").ParseChildValue("application.process.binary = ").Trim('"')
                ProcessId = $_.FindChild("Properties").ParseChildValue("application.process.id = ").Trim('"')
            }
        }
}

<#
    .SYNOPSIS
    Gets all Pulse Audio input devices. (See `pactl list sources` for details)

    .PARAMETER Name
    The name of the desired source, or a pattern to match the name against.

    .OUTPUTS
    A list of PulseAudioSources matching the provided pattern, or all sources if no name was specified.
#>

function Get-PASource {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $Name
    )
    pactl list sources |
        Split-IndentedData |
        ForEach-Object {
            [PulseAudioSource] @{
                Index = $_.ParseValue("Source #")
                Name = $_.ParseChildValue("Name: ")
                Description = $_.ParseChildValue("Description: ")
            }
        } |
        # Do not filter on $Name if it is not set
        Where-Object { -not $Name -or $_.Name -like $Name }
}


<#
    .SYNOPSIS
    Gets all applications receiving audio from Pulse Audio input devices. (See `pactl list source-outputs` for details)

    .OUTPUTS
    A list of PulseAudioSourceOutputs.
#>

function Get-PASourceOutput {
    pactl list source-outputs |
        Split-IndentedData |
        ForEach-Object {
            [PulseAudioSourceOutput] @{
                Index = $_.ParseValue("Source Output #")
                ApplicationName = $_.FindChild("Properties").ParseChildValue("application.name = ").Trim('"')
                BinaryName = $_.FindChild("Properties").ParseChildValue("application.process.binary = ").Trim('"')
                ProcessId = $_.FindChild("Properties").ParseChildValue("application.process.id = ").Trim('"')
            }
        }
}

<#
    .SYNOPSIS
    Sets the Pulse Audio sink being used by the specified application. (See `pactl move-sink-input` for details)

    .PARAMETER PASink
    The PulseAudioSink to be used, or the name of the sink.

    .PARAMETER PAInput
    The PulseAudioSinkInput to be moved, or the index of the sink input.

    .INPUTS
    See parameter PAInput.

    .OUTPUTS
    None
#>

function Set-PAInputSink {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [object]
        $PASink,
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]
        $PAInput
    )
    # Because the test dot-sources PADataStructs.ps1 separately, the -is operator cannot be used
    # here because the type handle is not the same
    if ($PASink.GetType().Name -eq 'PulseAudioSink') {
        $PASinkName = $PASink.Name
    } else {
        $PASinkName = [string] $PASink
    }

    if ($PAInput.GetType().Name -eq 'PulseAudioSinkInput') {
        $PAInputIndex = $PAInput.Index
    } else {
        $PAInputIndex = [int] $PAInput
    }

    $output = pactl move-sink-input $PAInputIndex $PASinkName
    if ($output) {            
        throw "Could not move input $PAInputIndex to $PASinkName`: $output"
    }
}

<#
    .SYNOPSIS
    Sets the default Pulse Audio sink that will be used when an application starts. (See `pactl set-default-sink` for details)

    .PARAMETER PASink
    The PulseAudioSink to be used, or the name of the sink.

    .INPUTS
    See parameter PASink.

    .OUTPUTS
    None
#>

function Set-DefaultPASink {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]
        $PASink
    )
    # Because the test dot-sources PADataStructs.ps1 separately, the -is operator cannot be used
    # here because the type handle is not the same
    if ($PASink.GetType().Name -eq 'PulseAudioSink') {
        $PASinkName = $PASink.Name
    } else {
        $PASinkName = [string] $PASink
    }

    $output = pactl set-default-sink $PASinkName
    if ($output) {            
        throw "Could not set default sink to $PASinkName`: $output"
    }
}

Export-ModuleMember -Function Get-PACard
Export-ModuleMember -Function Set-PACardProfile
Export-ModuleMember -Function Get-PASink
Export-ModuleMember -Function Get-PASinkInput
Export-ModuleMember -Function Get-PASource
Export-ModuleMember -Function Get-PASourceOutput
Export-ModuleMember -Function Set-PAInputSink
Export-ModuleMember -Function Set-DefaultPASink