Types/Wave/get_MetaData.ps1

<#
.SYNOPSIS
    Gets Wave Metadata
.DESCRIPTION
    Gets Metadata about a Wave File.

    Wave files can include metadata in a LIST chunk

    Each piece of metadata is a 4-byte ASCII identifier,
    a 4-byte length, and a string.
    
    If the identifier contains `XML`,
    the string will be decoded as UTF-8 and cast to `[XML]`

    If the identifier does not contain `XML`,
    the string will be decoded as ASCII.
.NOTES
    Each header must start at a WORD boundary, or two bytes.
    
    If the ASCII string had an odd number of characters, the next byte will be ignored.
#>

[OutputType([Collections.IDictionary])]
param()

# If we have no cached list
if (-not $this.'#list') {
    # read chunks.
    $null = $this.Chunk
    # Return if we still have no cached list
    if (-not $this.'#list') { return }
}

# Almost everything in metadata is encoded in ASCII
# (Wave file predate UTF-8 encoding by about a decade)

# We want to return metadata as a dictionary
$metadata = [Ordered]@{}

# We also want to make names more readable where we can
$LongNames = [Ordered]@{
    # Standard Wave Information
    IARL = "ArchiveLocation"
    IART = "Artist"
    ICMS = "Commissioned"
    ICMT = "Comments"
    ICOP = "Copyright"
    ICRD = "CreationDate"
    IENG = "Engineer"
    IGNR = "Genre"
    IKEY = "Keywords"
    IMED = "Medium"
    INAM = "Title" # (Technically `Name`)
    IPRD = "Album" # (Technically `Product`)
    IPRT = "TrackNumber"
    ISBJ = "Subject"
    ISFT = "Software"
    ISRC = "Source"
    ISRF = "SourceForm"
    ITCH = "Technician"    

    # Supported ID3 tags
    TBPM = "BPM"
}

# Get our list bytes
$list = $this.'#list'

# The first four bytes of a list are always `INFO`,
# so walk over our list, starting at 4 and incrementing as we read.
for ($index = 4; $index -lt $list.Length; ) {
    # Get the ID
    $metaDataId = [Text.Encoding]::ASCII.GetString($list, $index, 4) 
    # and advance four bytes.
    $index +=4
    # Get the chunk length
    $metadataLength = [BitConverter]::ToUInt32($list,$index)
    # and advance four bytes
    $index +=4    

    # While _most_ content in LIST sections is ASCII,
    # the Broadcast Wave Standard allows a UTF-8 encoded XML.
    
    $fourByteId = $metaDataId

    if ($LongNames[$metaDataId]) {
        $metaDataId = $LongNames[$metaDataId]
    }

    # Rather than special case this one scenario,
    # we will presume anything with the letters `XML` in the key is UTF-8 XML.
    if ($fourByteId -match 'XML') {
        $metadata[$metaDataId] = [Text.Encoding]::UTF8.GetString($list, $index, $metadataLength).Trim()
        # If we can cast the value to XML
        if ($metadata[$metaDataId] -as [xml]) {
            # then return this metadata as XML
            $metadata[$metaDataId] = $metadata[$metaDataId] -as [xml]
        }
    } else {
        # Get the string
        $metadata[$metaDataId] = [Text.Encoding]::ASCII.GetString($list, $index, $metadataLength).Trim()
    }
    
    # and advance by the string length.
    $index += $metadataLength
    # If our index was odd
    if ($index % 2) {
        $index++ # advance one more byte.
    }
}

foreach ($key in @($metadata.Keys)) {
    if ($key -match 'Date$') {
        $date = $metadata[$key] -as [DateTime]        
        if ($date) {
            $metadata[$key] = $date
        }
        elseif ($metadata[$key] -match '(?<y>\d{4})') {
            $metadata[$key] = [DateTime]::new($matches.y, 1, 1)
        }
    }
    if ($null -ne ($metadata[$key] -as [double])) {        
        $metadata[$key] = $metadata[$key] -as [double]
        if (
            ($metadata[$key] -as [int]) -eq [Math]::Floor($metadata[$key])
        ) {
            $metadata[$key] = $metadata[$key] -as [int]
        }
    }
}

# Return our metadata.
return $metadata