Types/Wave/Go.ps1

<#
.SYNOPSIS
    Moves the wave
.DESCRIPTION
    Moves the wave with any number of instrucitons.
.EXAMPLE
    $wave = wave
    $wave.Go("square", 440)
    $wave.Go("play")
#>

[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    'PSAvoidAssignmentToAutomaticVariable', '', Justification='We need $this to work'
)]
param()

$currentWave = if ($this) { $this } else { wave }
# Unroll our arguments so we handle lists of lists
$ArgumentList = @($args)

if (-not $ArgumentList) { return $currentWave }

$waveType = $(Get-TypeData -TypeName audio/wav)

$waveSplat = [Ordered]@{
    AudioFormat = $currentWave.AudioFormat
    SampleRate = $currentWave.SampleRate
    ChannelCount = $currentWave.ChannelCount
    BitsPerSample = $currentWave.BitsPerSample
}

if ($VerbosePreference -notin 'ignore','silentlyContinue') {
    Write-Verbose "wave Go`n`t$(
        @(foreach ($arg in $ArgumentList) {
            $arg
        }) -join "`n`t"
    )"

}

$helpfulKeywords = @(
    '?','--help','help','/help','/?'
)

filter getScriptHelp {
    $scriptBlock = $_
    $Name = $args -join ''
    $ExecutionContext.SessionState.PSVariable.Set("function:$Name",$scriptBlock)            
    if ($switches -is [Collections.IDictionary]) {
        if ($switches.Syntax) {
            Get-Command $Name -Syntax
        } else {
            Get-Help $Name @switches
        }                
    } else {
        Get-Help $Name
    }
    $ExecutionContext.SessionState.PSVariable.Remove("function:$Name")
}


# First we want to split each argument into words.
# This way, it is roughly the same if you say:
# * `wave 'forward 10'`
# * `wave forward 10`
# * `wave 'forward', 10`
$wordsAndArguments = @(foreach ($arg in $ArgumentList) {
    # If the argument is a string,
    if ($arg -is [string]) {
        $arg # leave it alone
    }
    # If the argument is an enumerable
    elseif ($arg -is [Collections.IEnumerable]) {
        # preceed by comma so we do not enumerate.
        ,$arg
    }
    else {
        # otherwise, leave the argument alone.
        $arg
    }
})

# We want to keep track of the current member,
# and continue to the next word until we find a member name.
$currentMember = $null
# We want to output the wave by default, in case we were called with no parameters.
$outputWave = $true

$progress = @{id=Get-Random;activity='Making Waves ~'; status=' '}

# To do this in one pass, we will iterate through the words and arguments.
# We use an indexed loop so we can skip past claimed arguments.
for ($argIndex =0; $argIndex -lt $wordsAndArguments.Length; $argIndex++) {

    $progress.PercentComplete = $argIndex * 100 / $wordsAndArguments.Length
    $arg = $wordsAndArguments[$argIndex]
    if ($arg.pstypenames -contains 'audio/wav') {
        $currentWave.Data += $arg.Data
        continue 
    }
    $progress.status = "$($argIndex) / $($wordsAndArguments.Length)"    
    Write-Progress @progress
    # If the argument is not in the member names list, we can complain about it.
    if ($arg -is [string]) {
        
        if ($arg -notin $waveType.Members.Keys) {
            $stepOutput = $currentWave.Note($arg)
            if ($stepOutput -as [Collections.IDictionary[]]) {
                $this.Melody += foreach ($stepOut in $stepOutput) {
                    [PSCustomObject](
                        [Ordered]@{PSTypeName='Note'} + $stepOut
                    )
                }
            }
            elseif ($stepOutput.pstypenames -contains 'audio/wav') {
                $newWave = $stepOutput
                $currentWave = $newWave
                continue
            }
            elseif ($stepOutput -is [double[]]) {
                $currentWave.Samples += $stepOutput
                continue
            }
            elseif ($stepOutput -is [byte[]]) {
                $currentWave.Data += $stepOutput
                continue
            }            
            elseif ($currentWave.NoteFrequency[$arg]) {
                $currentWave.Data += $currentWave.Tone($currentWave.NoteFrequency[$arg])
                continue
            }
            elseif ($currentWave.NoteFrequency["${arg}4"]) {
                $currentWave.Data += $currentWave.Tone($currentWave.NoteFrequency["${arg}4"])
                continue
            }            
            elseif (
                # (we might not want to, if it starts with a bracket)
                -not $currentMember -and $arg -is [string] -and
                "$arg".Trim()
            ) {            
                Write-Warning "Unknown command '$arg'."
                continue
            }
        }
    }    
    
    # If we have a current member, we can invoke it or get it.
    $currentMember = $arg
    $memberInfo = $waveType.Members[$currentMember]

    if (-not $memberInfo) {
        $memberInfo = foreach ($typeInfo in $waveTypes) {
            if ($typeInfo.Members -is [Collections.IDictionary] -and 
                $typeInfo.Members[$currentMember]) {
                $typeInfo; break
            }
            if ($typeInfo::$currentMember) {
                $typeInfo::$currentMember
                break
            }
        }
    }

    $IsMethod = 
        $memberInfo -is [Management.Automation.Runspaces.ScriptMethodData] -or 
        $memberInfo -is [Management.Automation.PSMethod] -or
        (
            $memberInfo.ReferencedMemberName -and (
                $waveType.Members[$memberInfo.ReferencedMemberName] -is 
                    [Management.Automation.Runspaces.ScriptMethodData]
            )
        )
        

    # If it's an alias
    if ($memberInfo.ReferencedMemberName) {
        # try to resolve it.
        $currentMember = $memberInfo.ReferencedMemberName
        $memberInfo = $waveType.Members[$currentMember]
    }
    
    # We can also begin looking for arguments.
    for (
        # Let's start at the next index.
        $methodArgIndex = $argIndex + 1; 
        # and continue until we reach the end of the words and arguments,
        $methodArgIndex -lt $wordsAndArguments.Length;
        $methodArgIndex++
    ) {
        # Count our brackets
        if ($wordsAndArguments[$methodArgIndex] -is [string]) {                        
            # If the next word is a method name, and our brackets are balanced
            if ($wordsAndArguments[$methodArgIndex] -in $waveType.Members.Keys) {
                # break out of the loop.
                break
            }
        }
    }
    # Now we know how far we had to look to get to the next member name.

    # And we can determine if we have any parameters.
    # (it is important that we always force any parameters into an array)
    $HelpWanted = $false
    $switches = [Ordered]@{}

    $argList = 
        @(if ($methodArgIndex -ne ($argIndex + 1)) {                        
            foreach ($word in $wordsAndArguments[($argIndex + 1)..($methodArgIndex - 1)]) {
                if ($word -in $helpfulKeywords) {
                    $HelpWanted = $true
                    continue
                }
                if ($HelpWanted -and 
                    $word -is [string] -and 
                    $word -in 'example', 'examples', 'parameter','parameters','online'
                ) {
                    if ($word -in 'example','examples') {
                        $switches['Examples'] = $true
                    }
                    if ($word -in 'parameter','parameters') {
                        $switches['Parameter'] = '*'
                    }
                    if ($word -eq 'online') {
                        $switches['Online'] = $true
                    }
                    continue
                }
                if ($word -is [string] -and $word -match '^[-/]+?[\D-[\.]]') {
                    $switchInfo = $word -replace '^[-/]+'
                    $switchName, $switchValue = $switchInfo -split ':', 2
                    if ($null -eq ($switchName -as [double])) {
                        $switches[$switchName] =
                            if ($switchValue) {
                                $switchValue
                            } else {
                                $true
                            }
                        continue
                    }                            
                }
                elseif ($word -is [object[]]) {
                    ,$word
                } else {
                    $word # output the word into the array.
                }                                            
            }
            $argIndex = $methodArgIndex - 1
        })
                                        
    # Now we want to get the output from the step.
    $stepOutput =
        if (
            # If the member is a method, let's invoke it.
            $IsMethod            
        ) {            
            # If we have arguments,
            if ($argList) {                
                # and a script method
                if ($memberInfo -is [Management.Automation.Runspaces.ScriptMethodData]) {
                    # Check to see if we want help.
                    if ($HelpWanted) {
                        # If we do, get some help.
                        $memberInfo.Script | getScriptHelp $memberInfo.Name
                    } else {
                        # Otherwise, set `$this` to the current wave
                        $this = $currentWave
                        # and call the script, splatting positional parameters
                        # (this allows more complex binding, like ValueFromRemainingArguments).
                        . $currentWave.$currentMember.Script @argList
                    }                            
                } 
                elseif ($currentWave.$currentMember.Invoke) {
                    $currentWave.$currentMember.Invoke($argList)
                } elseif ($memberInfo.Invoke) {
                    $memberInfo.Invoke($argList)
                } elseif ($memberInfo -is [ValueType]) {
                    $memberInfo
                }
            }
            # If we don't have any arguments, but are still dealing with a method
            else {
                # If we want help,
                if ($HelpWanted -and $memberInfo.Script) {
                    # get some help.
                    $memberInfo.Script | getScriptHelp $memberInfo.Name                            
                } else {
                    # otherwise, invoke the method with no parameters.
                    $currentWave.$currentMember.Invoke()
                }
            }                    
        } else {
            # If the member is a property, we can get it or set it.

            # If we have any arguments,
            if ($argList.Length) {
                # and we want help
                if ($HelpWanted -and $memberInfo.SetScriptBlock) {                            
                    # get help about the set.
                    $memberInfo.SetScriptBlock | getScriptHelp $memberInfo.Name                            
                } else {
                    # Otherwise, check to see if the arguments are strongly typed.
                    if ($memberInfo -is [Management.Automation.Runspaces.ScriptPropertyData]) {
                        $desiredType = $memberInfo.SetScriptBlock.Ast.ParamBlock.Parameters.StaticType
                        if ($desiredType -is [Type] -and
                            $argList.Length -eq 1 -and
                            $null -ne ($argList[0] -as $desiredType)) {
                            $argList = $argList[0] -as $desiredType
                        }                            
                    }
                    
                    # And try to set the property.
                    try {
                        $currentWave.$currentMember = $argList
                    } catch {
                        # If that fails,
                        $ex  = $_                        
                        # If we are running with a cmdlet,
                        if ($PSCmdlet) {                            
                            # use .WriteError for a cleaner error.
                            $PSCmdlet.WriteError($ex)
                        } else {
                            # otherwise, use Write-Error.
                            Write-Error -ErrorRecord $ex
                        }                        
                    }
                }
                
            } else {
                # otherwise, lets get the property
                # If we are getting a script and we want help
                if ($memberInfo.GetScriptBlock -and $HelpWanted) {
                    # momentarily turn that script into a function
                    $memberInfo.GetScriptBlock | getScriptHelp $memberInfo.Name                            
                }
                elseif ($null -ne $currentWave.$currentMember) {
                    $currentWave.$currentMember
                } elseif ($memberInfo -is [ValueType]) {
                    $memberInfo
                }                        
            }
        }

    if ((-not $IsMethod) -and $null -ne $stepOutput) {
        $stepOutput
        $outputWave = $false
        continue
    }
    # If the output is not a wave object, we can output it.
    # NOTE: This may lead to multiple types of output in the pipeline.
    # Luckily, this should be one of the few cases where this does not annoy too much.
    # Properties being returned will largely be strings or numbers, and these will always output directly.
    if ($null -ne $stepOutput -and -not ($stepOutput.pstypenames -eq 'wave') -and $IsMethod) {
        if ($stepOutput -as [Collections.IDictionary[]]) {
            $currentWave.Melody += $stepOutput
            $currentWave = $currentWave.Sound()
        }
        elseif ($stepOutput -is [double[]]) {
            $currentWave.Data += (wave @waveSplat -Samples $stepOutput).Data
        }
        elseif ($stepOutput -is [byte[]]) {
            $currentWave.Data += $stepOutput
        }        
        elseif ($stepOutput -is [object[]] -and $stepOutput.Length) {
            if ($stepOutput -is [Collections.IDictionary[]]) {
                $currentWave.Melody += $stepOutput
                $currentWave = $currentWave.Sound()
            }
            elseif ($stepOutput[0] -is [byte]) {
                $currentWave.Data += $stepOutput
                $outputWave = $true
            } elseif ($stepOutput[0] -is [double]) {                
                $currentWave.Data += (wave @waveSplat -Samples $stepOutput).Data
            } elseif ($stepOutput[0] -is [Collections.IDictionary]) {
                $stepOutput
                $outputWave = $false
            }            
        } else {
            # Output the step
            $stepOutput
            # and set the output wave to false.
            $outputWave = $false
        }        
    } elseif ($stepOutput.pstypenames -contains 'audio/wav') {
        # The step output is our new wave
        $newWave = $stepOutput        

        # Set the current wave to the new wave.
        $currentWave = $newWave
        # and output it later (presumably).
        $outputWave = $true
    }
}

$progress.Remove('PercentComplete')
$progress.Completed = $true
Write-Progress @progress

# If the last members returned a wave object, we can output it.
if ($outputWave) {
    if ($currentWave.Melody -and -not $currentWave.Duration) {
        return $currentWave.Sound()
    }
    return $currentWave
}