Skip to content

Instantly share code, notes, and snippets.

@StartAutomating
Created September 14, 2026 04:45
Show Gist options
  • Select an option

  • Save StartAutomating/c373e50042d25754090269fee8406001 to your computer and use it in GitHub Desktop.

Select an option

Save StartAutomating/c373e50042d25754090269fee8406001 to your computer and use it in GitHub Desktop.
Gist get midi
<#
.SYNOPSIS
Get Midi
.DESCRIPTION
Reads MIDI files
.NOTES
Adapted with care from JavaScript.
.LINK
https://github.com/carter-thaxton/midi-file/blob/master/lib/midi-parser.js
#>
param(
[byte[]]
$MidiBuffer
)
function midiByteParser([byte[]]$Buffer) {
$midiStream = [IO.MemoryStream]::new($Buffer)
$extendStream = [Ordered]@{Force=$true;PassThru=$true;MemberType='ScriptMethod'}
$midiStream |
Add-Member -Name ReadInt8 {
$byte = $this.ReadByte()
if ($byte -band 0x80) { ([int]$byte - 0x100) -as [sbyte] }
else { [sbyte]$byte }
} @extendStream |
Add-Member -Name ReadUint16 {
[uint16](([uint32]$this.ReadByte() -shl 8) + $this.ReadByte())
} @extendStream|
Add-Member -Name ReadUInt24 {
$byte1, $byte2, $byte3 = $this.ReadByte(), $this.ReadByte(), $this.ReadByte()
[uint32](
([uint32]$byte1 -shl 16) + ([uint32]$byte2 -shl 8) + $byte3
)
} @extendStream |
Add-Member -Name ReadUInt32 {
[uint32](
($this.ReadByte() -shl 24) +
($this.ReadByte() -shl 16) +
($this.ReadByte() -shl 8) +
$this.ReadByte()
)
} @extendStream |
Add-Member -Name ReadBytes {
param([uint32]$Length)
[byte[]]$buffer = [byte[]]::new($Length)
$readBytes = $this.Read($buffer, 0, $Length)
if ($readBytes) {
return ,$buffer
} else {
return ,([byte[]]::new(0))
}
} @extendStream |
Add-Member -Name ReadString {
param([uint32]$Length)
[byte[]]$buffer = [byte[]]::new($Length)
$readBytes = $this.Read($buffer, 0, $Length)
if ($readBytes) {
return [Text.Encoding]::ASCII.GetString($buffer)
} else {
return ""
}
} @extendStream |
Add-Member -Name ReadVarInt {
[uint32]$Result = 0
do {
$byte = $this.ReadByte()
if ($byte -band 0x80) {
$result += ($byte -band 0x7f)
$result = $result -shl 7
} else {
$result += $byte
}
} while ($byte -band 0x80)
return $result
} @extendStream |
Add-Member -Name ReadChunk {
$chunkStart = $this.Position
[string]$Id = $this.ReadString(4)
[uint32]$Length = $this.ReadUInt32()
[byte[]]$Data = $this.ReadBytes($Length)
[PSCustomObject]@{
PSTypeName='MIDI.Chunk';
Id = $Id; Length = $Length ; Data = $Data
Start = $chunkStart; End = $this.Position
}
} @extendStream |
Add-Member -Name ParseMidi {
$this.ParseHeader()
$tracks = [Ordered]@{}
while ($this.Position -lt $this.Length) {
$midiChunk = $this.ReadChunk()
if ($midiChunk.id -cne 'MTrk') { continue }
$trackParser = (midiByteParser $midiChunk.data)
$parsedTrack = $trackParser.ParseTrack()
$tracks[$midiChunk.id + ($tracks.Count + 1)] = $parsedTrack
}
$this | Add-Member NoteProperty Tracks $tracks -Force
} @extendStream |
Add-Member -Name ParseHeader {
$headerChunk = $this.ReadChunk()
if ($headerChunk.id -cne 'MThd') {
throw "Not a midi file"
}
$headerParser = (midiByteParser $headerChunk.data)
$format = $headerParser.ReadUint16()
$numTracks = $headerParser.ReadUint16()
$timeDivision = $headerParser.ReadUint16()
$result = [Ordered]@{
PSTypeName='MIDI.Header'
Format = $format
TrackCount = $numTracks
}
if ($timeDivision -band 0x8000) {
$result.FramesPerSecond = 0x100 - ($timeDivision -shr 8)
$result.TicksPerFrame = $timeDivision -band 0xff
} else {
$result.TicksPerBeat = $timeDivision
}
$headerParser.Close()
$headerParser.Dispose()
$this | Add-Member NoteProperty Header ([PSCustomObject]$result) -Force
} @extendStream |
Add-Member -Name ParseTrack {
$lastEventTypeByte = $null
$eventNumber = -1
:nextEvent while ($this.Position -lt $this.Length) {
$message = [Ordered]@{PSTypeName='MIDI.Event'}
$message.DeltaTime = $eventTime = $this.ReadVarInt()
$eventType = $this.ReadByte()
$eventNumber++
$isSystemEvent = ($eventType -band 0xf0) -eq 0xf0
if (-not $isSystemEvent) {
$param1 = $null
# If it is not a system event, it is a channel event.
if (($eventType -band 0x80) -eq 0) {
if (-not $lastEventTypeByte) {
throw "Running status byte encountered before status byte"
}
$param1 = $eventType
$eventType = $lastEventTypeByte
$message.Running = $true
} else {
$param1 = $this.ReadByte()
$lastEventTypeByte = $eventType
}
$message.Channel = $eventType -band 0x0f
$eventType = $eventType -shr 4
switch ($eventType) {
0x08 {
$message.Type = 'noteOff'
$message.NoteNumber = $param1
$message.Velocity = $this.ReadByte()
}
0x09 {
$message.Velocity = $this.ReadByte()
$message.NoteNumber = $param1
if ($message.Velocity -gt 0) {
$message.Type = 'noteOn'
$message.byte9 = $true
} else {
$message.Type = 'noteOff'
}
}
0x0a {
$message.Type = 'noteAfterTouch'
$message.NoteNumber = $param1
$message.Amount = $this.ReadByte()
}
0x0b {
$message.Type = 'controller'
$message.ControllerType = $param1
$message.Value = $this.ReadByte()
}
0x0c {
$message.Type = 'programChange'
$message.ProgramNumber = $param1
}
0x0d {
$message.Type = 'channelAfterTouch'
$message.Amount = $param1
}
0x0e {
$message.Type = 'pitchBend'
$message.Amount = $param1 + (
([uint32]$this.ReadByte()) -shr 7
) - 0x2000
}
default {
throw "Unrecognised MIDI event type: " + $eventType
}
}
[PSCustomObject]$message
continue nextEvent
}
if ($eventType -eq 0xf0) {
$message.Type = 'sysEx'
$message.Length = $this.ReadVarInt()
$message.data = $this.ReadBytes($message.Length)
[PSCustomObject]$message
continue nextEvent
}
if ($EventType -eq 0xf7) {
$message.Type = 'endSysEx'
$message.Length = $this.ReadVarInt()
$message.data = $this.ReadBytes($message.Length)
[PSCustomObject]$message
continue nextEvent
}
if ($EventType -ne 0xff) {
continue nextEvent
}
#region System Meta Events
$message.Meta = $true
$message.MetaType = $metaTypeByte = $this.ReadByte()
$message.Length = $metaLength = $this.ReadVarInt()
$stringFields = 'text','copyrightNotice','trackName','instrumentName','lyrics','marker','cuePoint', 'deviceName'
if ($stringFields[$metaTypeByte - 1]) {
$message.Type = $stringFields[$metaTypeByte - 1]
$message.Text = $this.ReadString($metaLength)
[PSCustomObject]$message
continue nextEvent
}
switch ($metaTypeByte) {
0x00 {
$message.Type = 'sequenceNumber'
if ($metaLength -ne 2) {
throw "Expected length for sequenceNumber event is 2, not $metaLength"
}
$message.Number = $this.ReadUint16()
}
0x20 {
$message.Type = 'channelPrefix'
if ($metaLength -ne 1) {
throw "Expected length for $($message.Type) event is 1, not $metaLength"
}
$message.Channel = $this.ReadByte()
}
0x21 {
$message.Type = 'portPrefix'
if ($metaLength -ne 1) {
throw "Expected length for $($message.Type) event is 1, not $metaLength"
}
$message.Port = $this.ReadByte()
}
0x22 { $message.Type = 'endOfTrack' }
0x51 {
$message.Type = 'setTempo'
$message.microsecondsPerBeat = $this.ReadUInt24()
}
0x54 {
$message.Type = 'smtpeOffset'
if ($metaLength -ne 5) {
throw "Expected length for $($message.Type) event is 5, not $metaLength"
}
$hourByte = $this.ReadByte()
$frameRates = @{0x00=24;0x20=25;0x40=29; 0x60=30}
$message.Framerate = $frameRates[$hourByte -band 0x60]
$message.Hour = $hourByte -band 0x1f
foreach ($timeSegment in 'Minute', 'Second', 'Frame', 'Subframe') {
$message.$timeSegment = $this.ReadByte()
}
}
0x58 {
$message.Type = 'timeSignature'
$message.Numerator = $this.ReadByte()
$message.Denominator = 1 -shl $this.ReadByte()
$message.Metronome, $message.ThirtySeconds =
if ($metaLength -eq 4) {
$this.ReadByte(), $this.ReadByte()
} else {
0x24, 0x08
}
}
0x59 {
$message.Type = 'keySignature'
$message.Key = $this.ReadInt8()
$message.Scale = $this.ReadByte()
}
0x7c {
$message.Type = 'sequencerSpecific'
$message.Data = $this.ReadBytes($metaLength)
}
default {
$message.Type = 'unknownMeta'
if ($metaLength) {
$message.Data = $this.ReadBytes($metaLength)
}
}
}
[PSCustomObject]$message
continue nextEvent
#endregion System Meta Events
}
} @extendStream
}
$midiParser = midiByteParser $MidiBuffer
$midiParser.ParseMidi()
$midiParser |
Select Header, Tracks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment