DryWetMIDI: High-Level Processing of MIDI Files

Overview of how to use the DryWetMIDI library for high-level managing of a MIDI file data

Introduction

DryWetMIDI is a .NET library to work with Standard MIDI Files (SMF) – read, write, create and modify them, and also to work with MIDI devices (which is described in DryWetMIDI: Working with MIDI Devices article). In this article we'll discuss processing MIDI files.

Although there are a lot of .NET libraries which provide parsing of MIDI files, there are some features that make the DryWetMIDI special:

Ability to read files with some corruptions like missed End of Track event

Ability to finely adjust process of reading and writing which allows, for example, to specify Encoding of text stored in text-based meta events like Lyric

Set of high-level classes that allow to manage content of a MIDI file in a more understandable way like Note or MusicalTimeSpan

The last point is the most important part of the library since many users ask about managing notes or conversion of MIDI time and length to more human understandable representation like seconds. To solve these problems, they are forced to write the same code again and again. DryWetMIDI frees them from the necessity to reinvent the wheel providing built-in tools to perform described tasks.

This article gives a quick overview of high-level data managing capabilities provided by the DryWetMIDI. It is not an API reference. The library also has a low-level layer of interaction with MIDI file content, but it is not a subject of the article.

There are examples at the end of the article that show how you can solve real tasks using features provided by the DryWetMIDI. So you can move to them right now if you want to get an overall impression of the library.

Contents

Absolute Time

All events inside a MIDI file have a delta-time attached to them. Delta-time is an offset from the previous event. Units of this offset defined by the time division of the file. According to SMF specification, there are two possible time divisions:

ticks per quarter note defines amount of time the quarter note lasts (this time division used by 99.999% of all MIDI files so we can assume that all files have it)

SMPTE time division defines times as numbers of subdivisions of SMPTE frame along with the specified frame rate

In practice, it is often more convenient to operate by absolute time rather than relative one. DryWetMIDI provides TimedEventsManager class designed to manage events by their absolute times:

After exiting from the using section, all events contained in the managing track chunk will be replaced with ones contained in the events collection updating all delta-times. Also, you can call SaveChanges method of the TimedEventsManager to save all changes. This method is especially useful if you are working with TimedEventsManager across multiple methods:

Other managers also have utility methods similar to those for timed events. It is worth taking a look at classes where these extensions methods are placed. Also, all managers provided by the DryWetMIDI can be obtained via constructor rather than via utility methods for low-level entities.

Notes

To present notes, a MIDI file uses pairs of Note On and Note Off events. But often people want to work with notes without messing with low-level MIDI events. DryWetMIDI provides NotesManager class for this purpose:

As with timed events, there are useful utilities for notes managing which are contained in the NotesManagingUtilities class. One of the most useful ones is GetNotes method that allows to get all notes contained in a track chunk or entire MIDI file:

IEnumerable<Note> notes = midiFile.GetNotes();

Note that if you'll do any changes on returned notes, they will not be applied. All manipulations with notes must be done via NotesManager or you can use ProcessNotes method from the NotesManagingUtilities. For example, to transpose all F notes up by one octave, you can use this code:

Or you can iterate through collection of TimedEvent and get collection of ITimedObject where an element either Note ot TimedEvent. Note will be returned for every pair of TimedEvent that represent Note On/Note Off events. MakeNotes method does this processing.

Chords

Chord is just a group of notes. To work with chords, you need to use ChordsManager class:

using (ChordsManager chordsManager = midiFile.GetTrackChunks()
.First()
.ManageChords(100)) // 100 is a notes tolerance// that defines maximum distance// of notes from the start of the// first note of a chord. Notes// within this tolerance will be// considered as a chord
{
// Get chords ordered by time
ChordsCollection chords = chordsManager.Chords;
// Get all chords that have C# note
IEnumerable<Chord> cSharpChords = chords.Where(c => c.Notes
.Any(n => n.NoteName == NoteName.CSharp));
}

You can see that ManageChords (and ChordsManager's constructor) can take notes tolerance that will be taken into an account to build chords. For example, two notes where first one has time of 10 and second one has time of 100 will represent a chord if the tolerance will be greater than or equal to 90. For smaller values of the tolerance, the second note will be out of it and will not fall into the chord. The default tolerance is 0 which means notes must start at the same time to make up a chord.

As with managing of notes, there are utility methods for chords manipulations. No prizes for guessing the name of the class that holds these methods. It is ChordsManagingUtilities.

Tempo Map

Tempo map is a list of all changes of the tempo and time signature in a MIDI file. With TempoMapManager, you can set new values of these parameters at the specified time and obtain current tempo map:

using (TempoMapManager tempoMapManager = midiFile.ManageTempoMap())
{
// Get current tempo map
TempoMap tempoMap = tempoMapManager.TempoMap;
// Get time signature at 2000
TimeSignature timeSignature = tempoMap.TimeSignatureLine.AtTime(2000);
// Set new tempo (230,000 microseconds per quarter note) at the time of// 20 seconds from the start of the file. See "Time representations"// section below to learn about time classes
tempoMapManager.SetTempo(new MetricTimeSpan(0, 0, 20),
new Tempo(230000));
}

TempoMap also holds an instance of TimeDivision in order to use it for time and length conversions. To get tempo map of a MIDI file, just call GetTempoMap extension method from the TempoMapManagingUtilities:

TempoMap tempoMap = midiFile.GetTempoMap();

Also, you can easily replace the tempo map of a MIDI file with another one using ReplaceTempoMap method. For example, to change tempo of a file to the 50 BPM and time signature to 5/8, you can write this code:

Time and Length Representations

As you could notice, all times and lengths in code samples above are presented as some long values in units defined by the time division of a file. In practice, it is much more convenient to operate by "human understandable" representations like seconds or bars/beats. In fact, there is no difference between time and length since time within a MIDI file is just a length that always starts at zero. So we will use the time span term to describe both time and length. DryWetMIDI provides the following classes to represent time span:

MetricTimeSpan for time span in terms of microseconds

BarBeatTimeSpan for time span in terms of number of bars, beats and ticks

MusicalTimeSpan for time span in terms of a fraction of the whole note length

MidiTimeSpan exists for unification purposes and simply holds long value in units defined by the time division of a file

All time span classes implement ITimeSpan interface. To convert time span between different representations, you should use TimeConverter or LengthConverter classes:

// Tempo map is needed in order to perform time span conversions
TempoMap tempoMap = midiFile.GetTempoMap();
// =================================================================================================// Time conversion// -------------------------------------------------------------------------------------------------// You can use LengthConverter as well but with the TimeConverter you don't need to specify time// where time span starts since it is always zero.// =================================================================================================// Some time in MIDI ticks (we assume time division of a MIDI file is "ticks per quarter note")long ticks = 123;
// Convert ticks to metric time
MetricTimeSpan metricTime = TimeConverter.ConvertTo<MetricTimeSpan>(ticks, tempoMap);
// Convert ticks to musical time
MusicalTimeSpan musicalTimeFromTicks = TimeConverter.ConvertTo<MusicalTimeSpan>(ticks, tempoMap);
// Convert metric time to musical time
MusicalTimeSpan musicalTimeFromMetric = TimeConverter.ConvertTo<MusicalTimeSpan>(metricTime, tempoMap);
// Convert metric time to bar/beat time
BarBeatTimeSpan barBeatTimeFromMetric = TimeConverter.ConvertTo<BarBeatTimeSpan>(metricTime, tempoMap);
// Convert musical time back to tickslong ticksFromMusical = TimeConverter.ConvertFrom(musicalTimeFromTicks, tempoMap);
// =================================================================================================// Length conversion// -------------------------------------------------------------------------------------------------// Length conversion is the same as time conversion but you need to specify the time where// a time span starts.// =================================================================================================// Convert ticks to metric length
MetricTimeSpan metricLength = LengthConverter.ConvertTo<MetricTimeSpan>(ticks, time, tempoMap);
// Convert metric length to musical length using metric time
MusicalTimeSpan musicalLengthFromMetric = LengthConverter.ConvertTo<MusicalTimeSpan>(metricLength,
metricTime,
tempoMap);
// Convert musical length back to tickslong ticksFromMetricLength = LengthConverter.ConvertFrom(metricLength, time, tempoMap);

You could notice that LengthConverter's methods take a time. In general case, MIDI file has changes of the tempo and time signature. Thus, the same long value can represent different amount of seconds, for example, depending on the time of an object with length of this value. The methods above can take time either as long or as ITimeSpan.

There are some useful methods in the TimedObjectUtilities class. This class contains extension methods for types that implement the ITimedObject interface – TimedEvent, Note and Chord. For example, you can get time of a timed event in hours, minutes, seconds with TimeAs method:

var metricTime = timedEvent.TimeAs<MetricTimeSpan>(tempoMap);

Or you can find all notes of a MIDI file that start at time of 10 bars and 4 beats:

Also, there is the LengthedObjectUtilities class. This class contains extension methods for types that implement the ILengthedObject interface – Note and Chord. For example, you can get length of a note as a fraction of the whole note with LengthAs method:

var musicalLength = note.LengthAs<MusicalTimeSpan>(tempoMap);

Or you can get all notes of a MIDI file that end exactly at 30 seconds from the start of the file:

You need to specify mode of the operation. In the example above, TimeLength is used which means that first time span represents a time and the second one represents a length. This information is needed for conversion engine when operands are of different types. There are also TimeTime and LengthLength modes.

If operands of the same type, result time span will be of this type too. But if you sum or subtract time spans of different types, the type of a result time span will be MathTimeSpan which holds operands along with operation (addition or subtraction) and mode.

Both TimeAs and LengthAs methods have non-generic versions where the desired type of result should be passed as an argument of the TimeSpanType type. Also generic methods in TimeConverter and LengthConverter classes have non-generic versions that take TimeSpanType too.

Pattern

For purpose of simple MIDI file creation that allows you to focus on the music, there is the PatternBuilder class. This class provides a fluent interface to build a musical composition that can be exported to a MIDI file. A quick example of what you can do with the builder:

It is only a small part of the PatternBuilder features. It has much more ones including specifying note velocity, inserting of chords, setting time anchors, moving to specific time and repeating previous actions. So the Pattern is a sort of music programming that is bound to MIDI. See example at the end of the article that shows how to build the first four bars of the Beethoven's "Moonlight Sonata".

Also, there are useful methods in NotesSplitterUtilities and ChordsSplitterUtilities classes which allow to split objects inside TrackChunk or MidiFile without the necessity to work with collection of notes or chords directly.

Quantizer

To quantize timed events, notes or chords there are TimedEventsQuantizer, NotesQuantizer and ChordsQuantizer classes respectively. All these classes have Quantize method that do the job. Notes and chords can be quantized either by start time or by end time.

Also, there are TimedEventsQuantizerUtilities, NotesQuantizerUtilities and ChordsQuantizerUtilities classes that contain useful methods to quantize objects inside TrackChunk and MidiFile without the necessity to work with collection of timed events, notes or chords directly.

Randomizer

To randomize time of timed events, notes and chords, there are TimedEventsRandomizer, NotesRandomizer and ChordsRandomizer classes. All these classes have Randomize method that do the job. Similar to quantizing, notes and chords can be randomized either by start time or by end time.

CSV Converter

DryWetMIDI provides a way to convert MIDI objects to CSV representation and read them back. CSV allows you to edit MIDI data, for example, via Microsoft Excel or to write it to database. CsvConverter class performs such conversions. Let's see what methods it provides:

License

Share

About the Author

My primary skills are C#, WPF and ArcObjects/ArcGIS Pro SDK. Currently I'm working on autotests in Kaspersky Lab.

Also I'm writing music which led me to starting the DryWetMIDI project on the GitHub. DryWetMIDI is an open source .NET library written in C# for managing MIDI files. The library is currently actively developing.

Also I actively help people on Code Review Stack Exchange to improve their C# code and have some answers on WPF related questions on Stack Overflow.

I also noticed some ambiguity issues between your software and Windows and also in your software itself

Ambiguity:
InputDevice: Exists in Windows 10 - need to use Melanchall.DryWetMidi.Devices.InputDevice
Note: exists in Melanchall.DryWetMidi.Smf.Interaction and also
Note: exists in Melanchall.DryWetMidi.MusicTheory

Including Chords, Notes, different instruments etc.
It will help me vary much to understand how to work with the library.
I need example of writing a file, not of reading from a file.
Thank You for the useful code!