Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

Parsing Serial MIDI Messages with the Netduino

Pete Brown - 20 January 2011

In my previous post on this topic, I set up the Netduino to receive MIDI messages via the serial input on the board. In that example, I simply blinked an LED when the messages arrived. This time, I want to actually parse the messages.

The MidiMessageReceiver Class

The first thing I need to do is refactor the code so I have a properly encapsulated MIDI class. This is a relatively low-level class that doesn't have much intelligence around the messages other than what's required to parse them.

using System;
using System.IO.Ports;
using System.Collections;
using PeteBrown.NetduinoMidiTest.Messages;

namespace PeteBrown.NetduinoMidiTest
{
class MidiMessageReceiver : IDisposable
{
private SerialPort _midiIn =
new SerialPort("COM1", 31250, Parity.None, 8, StopBits.One);

private Queue _dataBuffer = new Queue();

public event MidiMessageReceivedEventHandler MessageReceived;

public void Open()
{
_midiIn.DataReceived += new SerialDataReceivedEventHandler(OnDataReceived);
_midiIn.ErrorReceived +=new SerialErrorReceivedEventHandler(OnErrorReceived);
_midiIn.Open();
}

public void Close()
{
if (_midiIn.IsOpen)
{
_midiIn.Close();
_midiIn.DataReceived -= OnDataReceived;
_midiIn.ErrorReceived -= OnErrorReceived;
}
}

public void Dispose()
{
Close();
}



private void OnErrorReceived(object sender, SerialErrorReceivedEventArgs e)
{

}

private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{

// buffer for the data. May be more efficient to
// preallocate a larger fixed-size buffer and
// read bytes into that (memory vs. cpu)
byte[] readBuffer = new byte[_midiIn.BytesToRead];

// Read the bytes from the buffer. There's no way
// to read bytes without allocating a buffer, so
// we'll allocate a temp buffer before putting the
// results in the queue
_midiIn.Read(readBuffer, 0, readBuffer.Length);


foreach (byte b in readBuffer)
{
_dataBuffer.Enqueue(b);
}

ParseMidiStream();
}

// reference: http://www.tonalsoft.com/pub/pitch-bend/pitch.2005-08-24.17-00.aspx
// Channel messages are the primary messages used for controlling synthesizers
// and for receiving input from MIDI controllers. Channel messages require two
// or three bytes, depending on the specific message. The first byte is always
// divided into two nibbles (4-bits). The first nibble contains the message
// number, and the second nibble contains the channel number. Channels have a
// value between 0 and 15.
//
// reference: http://www.cs.cf.ac.uk/Dave/Multimedia/node158.html
// - MIDI message includes a status byte and up to two data bytes.
// - Status byte
// - The most significant bit of status byte is set to 1.
// - The 4 low-order bits identify which channel it belongs to (four bits produce 16 possible channels).
// - The 3 remaining bits identify the message.
// - The most significant bit of data byte is set to 0.
private bool DequeueCompleteMessage(ref MidiMessage message)
{
// peek lets you take a look without removing from queue
byte firstByte = (byte)_dataBuffer.Peek();

// parse the message and channel number
int messageNumber = firstByte >> 4;
int channelNumber = firstByte & (byte)0x0F;

int expectedDataByteCount = 0;

switch ((MidiMessageNumber)messageNumber)
{
case MidiMessageNumber.NoteOff:
case MidiMessageNumber.NoteOn:
case MidiMessageNumber.PolyphonicAftertouch:
case MidiMessageNumber.ControlChange:
case MidiMessageNumber.PitchWheelChange:
expectedDataByteCount = 2;
break;

case MidiMessageNumber.ProgramChange:
case MidiMessageNumber.ChannelPressureAftertouch:
expectedDataByteCount = 1;
break;
}

if (_dataBuffer.Count >= expectedDataByteCount + 1)
{
_dataBuffer.Dequeue(); // remove the message/channel

message.MessageNumber = (MidiMessageNumber)messageNumber;
message.Channel = (MidiChannel)channelNumber;

if (expectedDataByteCount >= 1)
message.DataByte1 = (byte)_dataBuffer.Dequeue();

if (expectedDataByteCount >= 2)
{
message.DataByte2 = (byte)_dataBuffer.Dequeue();
message.IsDataByte2Used = true;
}

// special case the pitch bend which stores a 16 bit value
if (message.MessageNumber == MidiMessageNumber.PitchWheelChange)
{
message.IsCombinedData = true;
message.DataBytesCombined = ((int)message.DataByte1 << 8) | (message.DataByte2);
}

return true;
}
else
{
return false;
}
}




private void ParseMidiStream()
{
bool moreData = _dataBuffer.Count != 0;

while (moreData)
{
MidiMessage message = new MidiMessage();

if (DequeueCompleteMessage(ref message))
{
if (MessageReceived != null)
MessageReceived(this,
new MidiMessageReceivedEventArgs() { Message = message });

// potentially more actionable data
moreData = _dataBuffer.Count != 0;
}
else
{
// no more actionable data
moreData = false;
}
}
}
}
}

A more feature-rich class might have different events for key messages, and map things like a NoteOn with a velocity of 0 to a NoteOff event, hiding some of the ugly innards of MIDI.

The way the class works is as follows:

  • The client instantiates the class, wires up a handler for the event, and calls the Open method
  • The class opens the serial port connection and listens for data
  • When data arrives, the class adds the data to the internal queue
  • The class then checks to see if that data, combined with what was previously received but unprocessed, constitutes at least one complete message in the queue. If so, it parses it.
    • For each complete message, a friendlier version of the message is created and the event is raised.
    • This continues for as long as valid messages exist in the queue.
  • The client waits forever, allowing the class to continue to receive events from the serial port

Support Classes, Structs and Enums

There are a number of classes, structs and enums used here to help keep the code relatively clean.

image

MidiMessage Struct

I used a struct rather than a class, as I felt it would be a bit easier on the GC. I haven't done any tests at all to see if that is even remotely true on the .NET MF :)

namespace PeteBrown.NetduinoMidiTest.Messages
{
public struct MidiMessage
{
public MidiMessageNumber MessageNumber;
public MidiChannel Channel;
public byte DataByte1;
public byte DataByte2;
public int DataBytesCombined;

public bool IsDataByte2Used;
public bool IsCombinedData;

public override string ToString()
{
// no string.format in the NETMF
string result = "Ch: " + Channel +
" Msg: " + MessageNumber.ToString();

if (IsCombinedData)
result += " DataWord: " + DataBytesCombined;
else
{
result += " DataByte1:" + (int)DataByte1;

if (IsDataByte2Used)
result += " DataByte2:" + (int)DataByte2;
}

return result;
}
}
}

MidiMessageNumber Enum

This enum is a mapping of midi messages (only a few are handled so far) to their message number.

namespace PeteBrown.NetduinoMidiTest.Messages
{
public enum MidiMessageNumber
{

// Channel Messages
NoteOff = 0x8,
NoteOn = 0x9,
PolyphonicAftertouch = 0xA,
ControlChange = 0xB,
ProgramChange = 0xC,
ChannelPressureAftertouch = 0xD,
PitchWheelChange = 0xE
}
}

MidiChannel Enum

I created this enum so I could include Omni (receive on all channels) should I decide to add channel filtering later. I may trash this in the final version, opting for a simple number instead. TBD

namespace PeteBrown.NetduinoMidiTest
{
public enum MidiChannel
{
Channel00 = 0,
Channel01,
Channel02,
Channel03,
Channel04,
Channel05,
Channel06,
Channel07,
Channel08,
Channel09,
Channel10,
Channel11,
Channel12,
Channel13,
Channel14,
Channel15,
Omni
}
}

MidiMessageReceivedEventArgs

The .NET Micro Framework doesn't include generics or the base EventArgs class. All event handlers are created from scratch and have their own delegate.

using PeteBrown.NetduinoMidiTest.Messages;

namespace PeteBrown.NetduinoMidiTest
{

public delegate void MidiMessageReceivedEventHandler(
object sender, MidiMessageReceivedEventArgs args);

public class MidiMessageReceivedEventArgs
{
public MidiMessage Message { get; set; }
}
}

The Program

The main program for this example displays debug output for the message, and also turns on the LED when a NoteOn is received, and turns it off when a NoteOff is received. This doesn't take into account polyphony, so you could have a key down and see the LED off. Nevertheless, it still serves as a simple activity indicator.

using System;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.Netduino;
using System.IO.Ports;


namespace PeteBrown.NetduinoMidiTest
{
public class Program
{
private static OutputPort _onboardLed =
new OutputPort(Pins.ONBOARD_LED, false);

private static MidiMessageReceiver _midiIn = new MidiMessageReceiver();

public static void Main()
{
_midiIn.MessageReceived += new MidiMessageReceivedEventHandler(OnMidiMessageReceived);
_midiIn.Open();

// wait forever
Thread.Sleep(Timeout.Infinite);
}

static void OnMidiMessageReceived(object sender, MidiMessageReceivedEventArgs args)
{
Debug.Print(args.Message.ToString());

if (args.Message.MessageNumber == Messages.MidiMessageNumber.NoteOn)
{
if (args.Message.DataByte2 == 0)
{
// a note on with velocity of zero is really a note off. LAME
_onboardLed.Write(false);
}
else
{
_onboardLed.Write(true);
}
}

if (args.Message.MessageNumber == Messages.MidiMessageNumber.NoteOff)
_onboardLed.Write(false);
}
}
}

 

Debug and Device Output

The output from the debug window looks something like this (obviously differs based on what keys you hit, and if your keyboard has aftertouch)

Ch: 0 Msg: 9 DataByte1:55 DataByte2:43
Ch: 0 Msg: 9 DataByte1:55 DataByte2:0
Ch: 0 Msg: 9 DataByte1:58 DataByte2:99
Ch: 0 Msg: 9 DataByte1:58 DataByte2:0
Ch: 0 Msg: 9 DataByte1:60 DataByte2:94
Ch: 0 Msg: 9 DataByte1:60 DataByte2:0
Ch: 0 Msg: 9 DataByte1:58 DataByte2:111
Ch: 0 Msg: 9 DataByte1:58 DataByte2:0
Ch: 0 Msg: 9 DataByte1:55 DataByte2:82
Ch: 0 Msg: 13 DataByte1:30
Ch: 0 Msg: 13 DataByte1:0
Ch: 0 Msg: 9 DataByte1:55 DataByte2:0
Ch: 0 Msg: 9 DataByte1:58 DataByte2:106
Ch: 0 Msg: 9 DataByte1:58 DataByte2:0
Ch: 0 Msg: 9 DataByte1:60 DataByte2:97
Ch: 0 Msg: 13 DataByte1:40
Ch: 0 Msg: 13 DataByte1:0
Ch: 0 Msg: 9 DataByte1:60 DataByte2:0

So far, this is working nicely. The next step is to build a synthesizer engine, mostly in software, but with some hardware components. I have a good chunk of that code done as I'll snag it from my Silverlight synthesizer prototype.

Here's a quick video of the current progress.

The source code is included with this post. Please refer back to my previous post for information on the hardware setup.

             

Source Code and Related Media

Download /media/73449/petebrown.netduinomiditest.zip
posted by Pete Brown on Thursday, January 20, 2011
filed under:              

2 comments for “Parsing Serial MIDI Messages with the Netduino”

  1. Petesays:
    Quick update:

    Turns out the Netduino isn't really fast enough for real-time sound generation, so I'm looking at some other approaches using the Netduino as the core and UI, and farming the sound generation out to other chips.

    In truth, that's not surprising, since most sound generators these days are assembly on higher-powered specialized chips. It's pretty intensive.

    Pete

Comment on this Post

Remember me