|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Contents
IntroductionThis is the fifth version of my .NET MIDI toolkit. I had thought that the previous version was the final one, but I have made many changes that have warranted a new version. This version takes a more traditional C#/.NET approach to flow-based programming, which I'll describe below. I wasn't comfortable with version four's implementation along these lines, so I took a step back and made changes that keep the flow-based approach while remaining within C#/.NET accepted idioms. I'm hoping that this will make the toolkit easier to use and understand. The toolkit has seen many revisions over the past two to three years. Each revision has been an almost total rewrite of the previous one. When writing software, it is usually a bad idea to make updates that break software using previous versions. However, my goal in creating this toolkit has been to provide the best design possible. As I have grown as a programmer, I have improved my skills and understanding of software design. This has led me to revise the earlier designs of the toolkit without regard to how these revisions will break code. Not exactly the attitude one wants to adopt in a professional setting, but since the toolkit is free and since I have used it as a learning experience to improve my craft, my priorities are different. Flow-based programmingBefore I get into the specifics of the toolkit, I would like to talk about its architecture. With each version of the toolkit, I have struggled with how to structure the flow of messages through the system. I wanted an approach that would be versatile and allow customization. It would be nice, I thought, if users could plug their own classes into the flow of MIDI messages to do whatever they want. For example, say you wanted to write a harmonizer, a class capable of transposing notes in a specified key to create harmony parts automatically. It should be easy to simply plug the harmonizer into the toolkit without affecting other classes. In other words, the toolkit should be customizable and configurable. Investigating this problem led me to J. Paul Morrison's excellent website on flow-based programming. He has written a book on the subject, which can be found on his website as well as at Amazon. The idea is simple and will probably seem familiar to most: Data flows through a network of components. Each component can do something interesting with the data before passing it along to the next component. In design pattern terms, this approach is most like the Pipe and Filter pattern and is also similar to the Chain of Responsibility pattern. Please check out J. Paul Morrison's excellent book for more information. (Just to be clear: when I say "component," I'm not necessarily talking about classes
that implement the Below is a very basic network of components designed to handle the flow of MIDI channel messages: The flow of messages begins with the input device. An input device receives MIDI messages from an external source. Next, the messages flow through a user component. This component might want to do something like change the MIDI channel, transpose note messages, or change the messages in some way. Then the messages pass through the channel stopper. This component simply keeps track of all currently sounding notes. When the message flow stops, the channel stopper can turn off all sounding notes so that none of them hang. Finally, the messages reach the output device. Here they are sent to an external MIDI device. Implementing flow-based programming in C#Well, this is something I really struggled with. You can read a bit about the different ways I tried to achieve this by reading my blog. I found myself going round in circles on this. In version four of the toolkit, I settled on the idea of using a source/sink abstraction. I created an interface representing "Sources." A source represents a source of MIDI messages. "Sinks" were represented by delegates that could be connected to sources; a sink is simply a method capable of receiving a MIDI message. This worked well but it was a little confusing because the implementation looked a little "funny." That is to say, a C# programmer looking at the code for the first time might be confused as to what is going on. I decided to do away with the sink/source infrastructure and use something more
idiomatic. Sources of MIDI messages raise events when they have messages to
send. Instead of implementing an interface and having How about sinks, those objects that can receive MIDI messages? A sink can be
anything. It's just an object that has a method that can receive a MIDI
message. In version four, I had a Let's look at an example. Say that we're using an InputDevice inDevice = new InputDevice(0);
ChannelStopper stopper = new ChannelStopper();
inDevice.ChannelMessageReceived += delegate(object sender, ChannelMessageEventArgs e)
{
stopper.Process(e.Message);
};
inDevice.StartRecording();
// ...
In this example, an anonymous method adapts events raised by the MIDI messagesThere are several categories of MIDI messages: Channel, System Exclusive, Meta, etc. In designing a MIDI toolkit, the challenge is to decide how to represent these messages. One approach is to create two or three general MIDI classes and have specific types of MIDI messages represented through the properties of those classes. The Java MIDI API takes this route. Another approach is to create a large collection of finely grained classes to represent all of the different types of MIDI messages. For example, there are many types of Channel messages such as the Note-on, Note-off, Program change, and Pitch Bend messages. The fine grained approach would create a class for each of those message types. My approach was to take a middle ground. I created classes for the general categories of MIDI messages but left the specific types of messages as properties within those classes. This kept the class hierarchy lightweight and manageable while providing enough specialization to make working with MIDI messages easy. Here is the hierarchy of MIDI message classes in the MIDI toolkit:
Specific types of messages are represented through properties. For example, the Message buildersAll message classes are immutable. This makes sharing messages throughout an application safe. To create messages, you pass the desired property values to their constructor. Additionally, the toolkit provides a set of builder classes for making message creation more convenient. The toolkit provides the following message builders:
The Here is an example of creating a ChannelMessageBuilder builder = new ChannelMessageBuilder();
builder.Command = ChannelCommand.NoteOn;
builder.MidiChannel = 0;
builder.Data1 = 60;
builder.Data2 = 127;
builder.Build();
Console.WriteLine(builder.Result);
After the builder has been initialized with the desired properties, the MIDI message
is built with a call to the There are several builder classes for creating specific types of meta messages. For
example, to create a key signature meta message, you use the KeySignatureBuilder builder = new KeySignatureBuilder();
builder.Key = Key.CMajor;
builder.Build();
Console.WriteLine(builder.Result);
MessageDispatcher classOften there is a need to process a collection of The ClocksMIDI playback is driven by ticks that occur periodically. The source of these ticks are MIDI clocks. MIDI clocks come in all shapes and sizes. For example, playback can be driven by an internal or external clock. Also, the way in which the ticks are generated depends on whether the MIDI sequence has pulses per quarter note resolution or SMPTE resolution. For the vast majority of situations, an internal clock generating ticks with pulses per quarter note resolution is all you need. The public interface IClock
{
event EventHandler Tick;
event EventHandler Started;
event EventHandler Continued;
event EventHandler Stopped;
bool IsRunning
{
get;
}
}
The You may notice that there are no methods in the interface for starting and stopping
a clock. That is because with clocks that are driven by an external source, the
source is responsible for starting and stopping the clock. The clocks receive
messages via MIDI and based on those messages starts or stops generating ticks.
Since all MIDI clocks implement At this time, the toolkit provides only one clock class, the The MidiEvent classA MIDI file is made up of several tracks. Each track contains one or more
timestamped MIDI messages. The timestamp represents the number of ticks since
the last message was played. This timestamp is called delta ticks. The
The The The In addition there are two internal properties, one which points to the previous Track classThe In addition to providing functionality for adding and removing MIDI events, the Perhaps the most useful iterator is the one that when advanced moves forward only
one tick at a time. The iterator keeps track of its tick position in the Sequence classThe Every MIDI devicesThere are several MIDI device classes in the toolkit. Each device class is
derived directly or indirectly from the abstract There can be more than one of these devices present on your computer. To determine
the number of input devices present, for example, you would query the Each MIDI device has its own unique ID. This is simply an integer value representing the device's order in the list of devices available. For example, the first output device on your system would have an ID of 0. The second output device would have an ID of 1, and so on. The same is true for the input devices. When you create a MIDI device, you pass it the ID of the device you wish to use to its constructor. If there was an error in opening the device, an exception is thrown. To find out the capabilities of a device, you query the class' Let's describe each device class in detail: InputDevice classThe OutputDeviceBase classThe OutputDevice classThe The following code creates an using(OutputDevice outDevice = new OutputDevice(0))
{
ChannelMessageBuilder builder = new ChannelMessageBuilder();
builder.Command = ChannelCommand.NoteOn;
builder.MidiChannel = 0;
builder.Data1 = 60;
builder.Data2 = 127;
builder.Build();
outDevice.Send(builder.Result);
Thread.Sleep(1000);
builder.Command = ChannelCommand.NoteOff;
builder.Data2 = 0;
builder.Build();
outDevice.Send(builder.Result);
}
OutputStream classThe To play MIDI messages, you call Sequencer ClassThe DependenciesThe MIDI toolkit depends on the ConclusionThis article has provided an overview of my .NET MIDI toolkit. My hope is that it will be a useful and powerful tool for writing MIDI applications. It has been a lot of fun to write. Each version has represented the very best of my skill and knowledge as a programmer. I welcome feedback and any bug reports you may have. Take care and thanks for your time. History
| ||||||||||||||||||||