using System;
using System.IO;
namespace NAudio.Midi
{
///
/// Represents an individual MIDI event
///
public class MidiEvent
: ICloneable
{
/// The MIDI command code
private MidiCommandCode commandCode;
private int channel;
private int deltaTime;
private long absoluteTime;
///
/// Creates a MidiEvent from a raw message received using
/// the MME MIDI In APIs
///
/// The short MIDI message
/// A new MIDI Event
public static MidiEvent FromRawMessage(int rawMessage)
{
long absoluteTime = 0;
int b = rawMessage & 0xFF;
int data1 = (rawMessage >> 8) & 0xFF;
int data2 = (rawMessage >> 16) & 0xFF;
MidiCommandCode commandCode;
int channel = 1;
if ((b & 0xF0) == 0xF0)
{
// both bytes are used for command code in this case
commandCode = (MidiCommandCode)b;
}
else
{
commandCode = (MidiCommandCode)(b & 0xF0);
channel = (b & 0x0F) + 1;
}
MidiEvent me;
switch (commandCode)
{
case MidiCommandCode.NoteOn:
case MidiCommandCode.NoteOff:
case MidiCommandCode.KeyAfterTouch:
if (data2 > 0 && commandCode == MidiCommandCode.NoteOn)
{
me = new NoteOnEvent(absoluteTime, channel, data1, data2, 0);
}
else
{
me = new NoteEvent(absoluteTime, channel, commandCode, data1, data2);
}
break;
case MidiCommandCode.ControlChange:
me = new ControlChangeEvent(absoluteTime,channel,(MidiController)data1,data2);
break;
case MidiCommandCode.PatchChange:
me = new PatchChangeEvent(absoluteTime,channel,data1);
break;
case MidiCommandCode.ChannelAfterTouch:
me = new ChannelAfterTouchEvent(absoluteTime,channel,data1);
break;
case MidiCommandCode.PitchWheelChange:
me = new PitchWheelChangeEvent(absoluteTime, channel, data1 + (data2 << 7));
break;
case MidiCommandCode.TimingClock:
case MidiCommandCode.StartSequence:
case MidiCommandCode.ContinueSequence:
case MidiCommandCode.StopSequence:
case MidiCommandCode.AutoSensing:
me = new MidiEvent(absoluteTime,channel,commandCode);
break;
//case MidiCommandCode.MetaEvent:
//case MidiCommandCode.Sysex:
default:
throw new FormatException(String.Format("Unsupported MIDI Command Code for Raw Message {0}", commandCode));
}
return me;
}
///
/// Constructs a MidiEvent from a BinaryStream
///
/// The binary stream of MIDI data
/// The previous MIDI event (pass null for first event)
/// A new MidiEvent
public static MidiEvent ReadNextEvent(BinaryReader br, MidiEvent previous)
{
int deltaTime = ReadVarInt(br);
MidiCommandCode commandCode;
int channel = 1;
byte b = br.ReadByte();
if((b & 0x80) == 0)
{
// a running command - command & channel are same as previous
commandCode = previous.CommandCode;
channel = previous.Channel;
br.BaseStream.Position--; // need to push this back
}
else
{
if((b & 0xF0) == 0xF0)
{
// both bytes are used for command code in this case
commandCode = (MidiCommandCode) b;
}
else
{
commandCode = (MidiCommandCode) (b & 0xF0);
channel = (b & 0x0F) + 1;
}
}
MidiEvent me;
switch(commandCode)
{
case MidiCommandCode.NoteOn:
me = new NoteOnEvent(br);
break;
case MidiCommandCode.NoteOff:
case MidiCommandCode.KeyAfterTouch:
me = new NoteEvent(br);
break;
case MidiCommandCode.ControlChange:
me = new ControlChangeEvent(br);
break;
case MidiCommandCode.PatchChange:
me = new PatchChangeEvent(br);
break;
case MidiCommandCode.ChannelAfterTouch:
me = new ChannelAfterTouchEvent(br);
break;
case MidiCommandCode.PitchWheelChange:
me = new PitchWheelChangeEvent(br);
break;
case MidiCommandCode.TimingClock:
case MidiCommandCode.StartSequence:
case MidiCommandCode.ContinueSequence:
case MidiCommandCode.StopSequence:
me = new MidiEvent();
break;
case MidiCommandCode.Sysex:
me = SysexEvent.ReadSysexEvent(br);
break;
case MidiCommandCode.MetaEvent:
me = MetaEvent.ReadMetaEvent(br);
break;
default:
throw new FormatException(String.Format("Unsupported MIDI Command Code {0:X2}",(byte) commandCode));
}
me.channel = channel;
me.deltaTime = deltaTime;
me.commandCode = commandCode;
return me;
}
///
/// Converts this MIDI event to a short message (32 bit integer) that
/// can be sent by the Windows MIDI out short message APIs
/// Cannot be implemented for all MIDI messages
///
/// A short message
public virtual int GetAsShortMessage()
{
return (channel - 1) + (int)commandCode;
}
///
/// Default constructor
///
protected MidiEvent()
{
}
///
/// Creates a MIDI event with specified parameters
///
/// Absolute time of this event
/// MIDI channel number
/// MIDI command code
public MidiEvent(long absoluteTime, int channel, MidiCommandCode commandCode)
{
this.absoluteTime = absoluteTime;
Channel = channel;
this.commandCode = commandCode;
}
///
/// Creates a deep clone of this MIDI event.
///
public virtual MidiEvent Clone() => (MidiEvent)MemberwiseClone();
object ICloneable.Clone() => Clone();
///
/// The MIDI Channel Number for this event (1-16)
///
public virtual int Channel
{
get => channel;
set
{
if ((value < 1) || (value > 16))
{
throw new ArgumentOutOfRangeException("value", value,
String.Format("Channel must be 1-16 (Got {0})",value));
}
channel = value;
}
}
///
/// The Delta time for this event
///
public int DeltaTime
{
get
{
return deltaTime;
}
}
///
/// The absolute time for this event
///
public long AbsoluteTime
{
get
{
return absoluteTime;
}
set
{
absoluteTime = value;
}
}
///
/// The command code for this event
///
public MidiCommandCode CommandCode
{
get
{
return commandCode;
}
}
///
/// Whether this is a note off event
///
public static bool IsNoteOff(MidiEvent midiEvent)
{
if (midiEvent != null)
{
if (midiEvent.CommandCode == MidiCommandCode.NoteOn)
{
NoteEvent ne = (NoteEvent)midiEvent;
return (ne.Velocity == 0);
}
return (midiEvent.CommandCode == MidiCommandCode.NoteOff);
}
return false;
}
///
/// Whether this is a note on event
///
public static bool IsNoteOn(MidiEvent midiEvent)
{
if (midiEvent != null)
{
if (midiEvent.CommandCode == MidiCommandCode.NoteOn)
{
NoteEvent ne = (NoteEvent)midiEvent;
return (ne.Velocity > 0);
}
}
return false;
}
///
/// Determines if this is an end track event
///
public static bool IsEndTrack(MidiEvent midiEvent)
{
if (midiEvent != null)
{
MetaEvent me = midiEvent as MetaEvent;
if (me != null)
{
return me.MetaEventType == MetaEventType.EndTrack;
}
}
return false;
}
///
/// Displays a summary of the MIDI event
///
/// A string containing a brief description of this MIDI event
public override string ToString()
{
if(commandCode >= MidiCommandCode.Sysex)
return String.Format("{0} {1}",absoluteTime,commandCode);
else
return String.Format("{0} {1} Ch: {2}", absoluteTime, commandCode, channel);
}
///
/// Utility function that can read a variable length integer from a binary stream
///
/// The binary stream
/// The integer read
public static int ReadVarInt(BinaryReader br)
{
int value = 0;
byte b;
for(int n = 0; n < 4; n++)
{
b = br.ReadByte();
value <<= 7;
value += (b & 0x7F);
if((b & 0x80) == 0)
{
return value;
}
}
throw new FormatException("Invalid Var Int");
}
///
/// Writes a variable length integer to a binary stream
///
/// Binary stream
/// The value to write
public static void WriteVarInt(BinaryWriter writer, int value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("value", value, "Cannot write a negative Var Int");
}
if (value > 0x0FFFFFFF)
{
throw new ArgumentOutOfRangeException("value", value, "Maximum allowed Var Int is 0x0FFFFFFF");
}
int n = 0;
byte[] buffer = new byte[4];
do
{
buffer[n++] = (byte)(value & 0x7F);
value >>= 7;
} while (value > 0);
while (n > 0)
{
n--;
if(n > 0)
writer.Write((byte) (buffer[n] | 0x80));
else
writer.Write(buffer[n]);
}
}
///
/// Exports this MIDI event's data
/// Overriden in derived classes, but they should call this version
///
/// Absolute time used to calculate delta.
/// Is updated ready for the next delta calculation
/// Stream to write to
public virtual void Export(ref long absoluteTime, BinaryWriter writer)
{
if (this.absoluteTime < absoluteTime)
{
throw new FormatException("Can't export unsorted MIDI events");
}
WriteVarInt(writer,(int) (this.absoluteTime - absoluteTime));
absoluteTime = this.absoluteTime;
int output = (int) commandCode;
if (commandCode != MidiCommandCode.MetaEvent)
{
output += (channel - 1);
}
writer.Write((byte)output);
}
}
}