Utilities/MidiFile.cs
namespace Clover.Utilities;
// https://github.com/davidluzgouveia/midi-parser
// MIT License
//
// Copyright (c) 2018 David Gouveia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
public class MidiFile
{
public readonly int Format;
public readonly int TicksPerQuarterNote;
public readonly MidiTrack[] Tracks;
public readonly int TracksCount;
public MidiFile( Stream stream )
: this( Reader.ReadAllBytesFromStream( stream ) )
{
}
public MidiFile( string path )
: this( FileSystem.Data.ReadAllBytes( path ).ToArray() )
{
}
public MidiFile( byte[] data )
{
var position = 0;
if ( Reader.ReadString( data, ref position, 4 ) != "MThd" )
{
throw new FormatException( "Invalid file header (expected MThd)" );
}
if ( Reader.Read32( data, ref position ) != 6 )
{
throw new FormatException( "Invalid header length (expected 6)" );
}
this.Format = Reader.Read16( data, ref position );
this.TracksCount = Reader.Read16( data, ref position );
this.TicksPerQuarterNote = Reader.Read16( data, ref position );
if ( (this.TicksPerQuarterNote & 0x8000) != 0 )
{
throw new FormatException( "Invalid timing mode (SMPTE timecode not supported)" );
}
this.Tracks = new MidiTrack[this.TracksCount];
for ( var i = 0; i < this.TracksCount; i++ )
{
this.Tracks[i] = ParseTrack( i, data, ref position );
}
}
private static bool ParseMetaEvent(
byte[] data,
ref int position,
byte metaEventType,
ref byte data1,
ref byte data2 )
{
switch ( metaEventType )
{
case (byte)MetaEventType.Tempo:
var mspqn = (data[position + 1] << 16) | (data[position + 2] << 8) | data[position + 3];
data1 = (byte)(60000000.0 / mspqn);
position += 4;
return true;
case (byte)MetaEventType.TimeSignature:
data1 = data[position + 1];
data2 = (byte)Math.Pow( 2.0, data[position + 2] );
position += 5;
return true;
case (byte)MetaEventType.KeySignature:
data1 = data[position + 1];
data2 = data[position + 2];
position += 3;
return true;
// Ignore Other Meta Events
default:
var length = Reader.ReadVarInt( data, ref position );
position += length;
return false;
}
}
private static MidiTrack ParseTrack( int index, byte[] data, ref int position )
{
if ( Reader.ReadString( data, ref position, 4 ) != "MTrk" )
{
throw new FormatException( "Invalid track header (expected MTrk)" );
}
var trackLength = Reader.Read32( data, ref position );
var trackEnd = position + trackLength;
var track = new MidiTrack { Index = index };
var time = 0;
var status = (byte)0;
while ( position < trackEnd )
{
time += Reader.ReadVarInt( data, ref position );
var peekByte = data[position];
// If the most significant bit is set then this is a status byte
if ( (peekByte & 0x80) != 0 )
{
status = peekByte;
++position;
}
// If the most significant nibble is not an 0xF this is a channel event
if ( (status & 0xF0) != 0xF0 )
{
// Separate event type from channel into two
var eventType = (byte)(status & 0xF0);
var channel = (byte)((status & 0x0F) + 1);
var data1 = data[position++];
// If the event type doesn't start with 0b110 it has two bytes of data (i.e. except 0xC0 and 0xD0)
var data2 = (eventType & 0xE0) != 0xC0 ? data[position++] : (byte)0;
// Convert NoteOn events with 0 velocity into NoteOff events
if ( eventType == (byte)MidiEventType.NoteOn && data2 == 0 )
{
eventType = (byte)MidiEventType.NoteOff;
}
track.MidiEvents.Add(
new MidiEvent
{
Time = time,
Type = eventType,
Arg1 = channel,
Arg2 = data1,
Arg3 = data2
} );
}
else
{
if ( status == 0xFF )
{
// Meta Event
var metaEventType = Reader.Read8( data, ref position );
// There is a group of meta event types reserved for text events which we store separately
if ( metaEventType >= 0x01 && metaEventType <= 0x0F )
{
var textLength = Reader.ReadVarInt( data, ref position );
var textValue = Reader.ReadString( data, ref position, textLength );
var textEvent = new TextEvent { Time = time, Type = metaEventType, Value = textValue };
track.TextEvents.Add( textEvent );
}
else
{
var data1 = (byte)0;
var data2 = (byte)0;
// We only handle the few meta events we care about and skip the rest
if ( ParseMetaEvent( data, ref position, metaEventType, ref data1, ref data2 ) )
{
track.MidiEvents.Add(
new MidiEvent
{
Time = time,
Type = status,
Arg1 = metaEventType,
Arg2 = data1,
Arg3 = data2
} );
}
}
}
else if ( status == 0xF0 || status == 0xF7 )
{
// SysEx event
var length = Reader.ReadVarInt( data, ref position );
position += length;
}
else
{
++position;
}
}
}
return track;
}
private static class Reader
{
public static int Read16( byte[] data, ref int i )
{
return (data[i++] << 8) | data[i++];
}
public static int Read32( byte[] data, ref int i )
{
return (data[i++] << 24) | (data[i++] << 16) | (data[i++] << 8) | data[i++];
}
public static byte Read8( byte[] data, ref int i )
{
return data[i++];
}
public static byte[] ReadAllBytesFromStream( Stream input )
{
var buffer = new byte[16 * 1024];
using ( var ms = new MemoryStream() )
{
int read;
while ( (read = input.Read( buffer, 0, buffer.Length )) > 0 )
{
ms.Write( buffer, 0, read );
}
return ms.ToArray();
}
}
public static string ReadString( byte[] data, ref int i, int length )
{
var result = Encoding.ASCII.GetString( data, i, length );
i += length;
return result;
}
public static int ReadVarInt( byte[] data, ref int i )
{
var result = (int)data[i++];
if ( (result & 0x80) == 0 )
{
return result;
}
result &= 0x7F;
for ( var j = 0; j < 3; j++ )
{
var value = (int)data[i++];
result = (result << 7) | (value & 0x7F);
if ( (value & 0x80) == 0 )
{
break;
}
}
return result;
}
}
}
public class MidiTrack
{
public int Index;
public List<MidiEvent> MidiEvents = new List<MidiEvent>();
public List<TextEvent> TextEvents = new List<TextEvent>();
}
public struct MidiEvent
{
public int Time;
public byte Type;
public byte Arg1;
public byte Arg2;
public byte Arg3;
public MidiEventType MidiEventType => (MidiEventType)this.Type;
public MetaEventType MetaEventType => (MetaEventType)this.Arg1;
public int Channel => this.Arg1;
public int Note => this.Arg2;
public int Velocity => this.Arg3;
public ControlChangeType ControlChangeType => (ControlChangeType)this.Arg2;
public int Value => this.Arg3;
}
public struct TextEvent
{
public int Time;
public byte Type;
public string Value;
public TextEventType TextEventType => (TextEventType)this.Type;
}
public enum MidiEventType : byte
{
NoteOff = 0x80,
NoteOn = 0x90,
KeyAfterTouch = 0xA0,
ControlChange = 0xB0,
ProgramChange = 0xC0,
ChannelAfterTouch = 0xD0,
PitchBendChange = 0xE0,
MetaEvent = 0xFF
}
public enum ControlChangeType : byte
{
BankSelect = 0x00,
Modulation = 0x01,
Volume = 0x07,
Balance = 0x08,
Pan = 0x0A,
Sustain = 0x40
}
public enum TextEventType : byte
{
Text = 0x01,
TrackName = 0x03,
Lyric = 0x05,
}
public enum MetaEventType : byte
{
Tempo = 0x51,
TimeSignature = 0x58,
KeySignature = 0x59
}