Created
September 14, 2026 04:45
-
-
Save StartAutomating/c373e50042d25754090269fee8406001 to your computer and use it in GitHub Desktop.
Gist get midi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .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