Types/Wave/set_MetaData.ps1

<#
.SYNOPSIS
    Sets Wave Metadata
.DESCRIPTION
    Sets 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 an ASCII string.
.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.
#>

param([Collections.IDictionary]$MetaData)

$shortNames = [Ordered]@{
    "ArchiveLocation" = 'IARL'
    "Artist" = 'IART'
    'BPM' = 'TBPM'
    'BeatsPerMinute' = 'TBPM'
    "Commissioned" = 'ICMS'
    "Comment" ='ICMT'
    "Comments" ='ICMT'
    "Copyright" = 'ICOP'
    "CreationDate"= 'ICRD'
    "Engineer"= 'IENG'
    "Genre"='IGNR'
    "Tag" = "IKEY"
    "Tags" = "IKEY"
    "Keyword"="IKEY"
    "Keywords"='IKEY'
    "Medium"='IMED'
    "Title"='INAM'
    'Album' = 'IPRD'
    "Product"= 'IPRD' 
    "TrackNumber"= 'IPRT'
    "Subject"= 'ISBJ'
    "Software" = 'ISFT'
    "Source"= 'ISRC'
    "SourceForm" =  'ISRF'
    "Technician" = 'ITCH'    
}

# https://mediaarea.net/BWFMetaEdit/listinfo

# Start off by refreshing out chunk list
$this | Add-Member NoteProperty '#Chunk' $null -Force
$chunks = $this.Chunk

# We will need to locate the list start if we have one
$listStart = 0 

# Walk over each chunk
$afterList = foreach ($chunk in $chunks) {
    # we will put the data into the first LIST chunk we see
    if ($chunk.id -eq 'LIST') {
        $listStart = $chunk.start
    }
    # and we will keep track of all chunks after this location
    elseif ($listStart) {
        $chunk
    }
}

# If we do not have a LIST
if (-not $listStart) {
    # Put it before the last chunk (often `data`)
    $listStart = $chunks[-1].start
    $afterList = @($chunks[-1])
}

# Create a writer
$writer = [IO.BinaryWriter]::new($this)
# and seek to the list start
$this.Position = $listStart
# Then write the list
$writer.Write([Text.Encoding]::ASCII.GetBytes("LIST"))

# Collect our list data
[byte[]]$listData = @(
    # `LIST` chunks begin with the ASCII characters `INFO`.
    [Text.Encoding]::ASCII.GetBytes("INFO")
    # followed by a series of key-value pairs
    foreach ($key in $MetaData.Keys) {
        # Each key can only be four ASCII characters.
        $shortKey = 
            # If the key was longer than four characters
            if ($shortNames[$key]) {
                $shortNames[$key]
            }
            elseif ("$key".Length -gt 4) {
                # take the first four characters
                "$key".Substring(0, 4)
            } else {
                # otherwise, pad right with spaces.
                "$key".PadRight(4, ' ')
            }
                    
        [Text.Encoding]::ASCII.GetBytes($shortKey)
        
        [byte[]]$valueBytes = if ($shortKey -match 'xml') {
            if ($MetaData[$key] -is [xml]) {
                [Text.Encoding]::UTF8.GetBytes(
                    $MetaData[$key].OuterXml
                )
            } else {
                [Text.Encoding]::UTF8.GetBytes("$($MetaData[$key])")
            }            
        } else {
            if ($MetaData[$key] -is [DateTime]) {
                [Text.Encoding]::ASCII.GetBytes("$(
                    ($MetaData[$key]).ToString('yyyy-MM-dd')
                )"
)
            } else {
                # Get the bytes stored in the value
                [Text.Encoding]::ASCII.GetBytes("$($MetaData[$key])")
            }
        }
        
        [BitConverter]::GetBytes([uint32]$valueBytes.Length)
        $valueBytes
        if ($valueBytes.Length % 2) {
            [byte]0
        }
    }
)
$writer.Write([uint32]$listData.Length)
$writer.Write($listData)

foreach ($after in $afterList) {
    $writer.Write([Text.Encoding]::ASCII.GetBytes($after.id))
    $writer.Write([uint32]$after.length)
    $writer.Write([byte[]]$after.data)
}

$this.Position = 4
# Then write our stream length, minus 8 bytes
$writer.Write([uint32]($this.Length - 8))

# and set our position back to the start.
$this.Position = 0

foreach ($prop in $this.psobject.properties) {
    if ($prop.Name -match '^#') {
        $prop.Value = $null
    }
}

return $this

# 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]@{}

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

# 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
    # Get the string
    $metadata[$metaDataId] = [Text.Encoding]::ASCII.GetString($list, $index, $metadataLength)
    # and advance by the string length.
    $index += $metadataLength
    # If our index was odd
    if ($index % 2) {
        $index++ # advance one more byte.
    }
}


# Return our metadata.
return $metadata