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.
[^] top
Background
It is recommended to be familiar with SMF format but not necessary if you are going to work only with high-level API of the DryWetMIDI
.
[^] top
Contents
- Absolute Time
- Notes
- Chords
- GetObjects
- Tempo Map
- Time and Length Representations
- Pattern
- Tools
- Examples
- Links
- History
[^] top
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. The following code shows how you can manage events by their absolute times with DryWetMIDI
:
using Melanchall.DryWetMidi.Core;
using Melanchall.DryWetMidi.Interaction;
var midiFile = MidiFile.Read("Cool song.mid");
using (var objectsManager = midiFile.Chunks
.OfType<TrackChunk>()
.First()
.ManageTimedEvents())
{
var events = objectsManager.Objects;
var firstLyricEvent = events.FirstOrDefault(e => e.Event is LyricEvent);
if (firstLyricEvent != null)
firstLyricEvent.Time = 2000;
events.Add(new TimedEvent(new PitchBendEvent(8000), 3000));
}
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 objects manager to save all changes. This method is especially useful if you are working with the manager across multiple methods:
private TimedObjectsManager<TimedEvent> _timedEventsManager;
private void BeginManageEvents(TrackChunk trackChunk)
{
_timedEventsManager = trackChunk.ManageTimedEvents();
}
private void ShiftEvents(long shift)
{
foreach (TimedEvent timedEvent in _timedEventsManager.Objects)
{
timedEvent.Time += shift;
}
}
private void EndManageEvents()
{
_timedEventsManager.SaveChanges();
}
All other managers described below have the same saving logic. Please read the Objects managers article of the library documentation.
Also, there are some useful extension methods contained in the TimedEventsManagingUtilities
. For example, you can easily remove all System Exclusive events with time of 400 from a file:
midiFile.RemoveTimedEvents(e => e.Event is SysExEvent && e.Time == 400);
Or you can divide times of all events by 2 to shrink a MIDI file:
midiFile.ProcessTimedEvents(e => e.Time /= 2);
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.
[^] top
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:
using (var objectsManager = midiFile.GetTrackChunks()
.First()
.ManageNotes())
{
var notes = objectsManager.Objects;
var cSharpNotes = notes.Where(n => n.NoteName == NoteName.CSharp);
foreach (var note in cSharpNotes)
{
note.Length -= 100;
}
notes.Add(new Note(NoteName.CSharp, 2)
{
Channel = (FourBitNumber)2,
Velocity = (SevenBitNumber)95
});
}
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 make any changes on returned notes, they will not be applied. All manipulations with notes must be done via objects manager 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:
f.ProcessNotes(n => n.NoteNumber += (SevenBitNumber)12,
n => n.NoteName == NoteName.F);
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. Please see docs on the GetObjectsUtilities class.
[^] top
Chords
Chord
is just a group of notes. To work with chords:
using (var objectsManager = midiFile.GetTrackChunks()
.First()
.ManageChords())
{
var chords = objectsManager.Objects;
var cSharpChords = chords.Where(c => c.Notes
.Any(n => n.NoteName == NoteName.CSharp));
}
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
.
[^] top
GetObjects
There is the class which provides methods to get objects of different types at once – GetObjectsUtilities. In conjunction with the methods within the RestsUtilities class you can build rests along with objects described above.
Please see following articles of the library documentation to learn more:
[^] top
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())
{
TempoMap tempoMap = tempoMapManager.TempoMap;
TimeSignature timeSignature = tempoMap.TimeSignatureLine.AtTime(2000);
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:
midiFile.ReplaceTempoMap(TempoMap.Create(Tempo.FromBeatsPerMinute(50),
new TimeSignature(5, 8)));
Tempo map is a very important object since it allows to perform time and length conversions as you'll see in the next sections.
[^] top
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 BarBeatTicksTimeSpan
for time span in terms of number of bars, beats and ticks BarBeatFractionTimeSpan
for time span in terms of number of bars and fractional beats 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:
TempoMap tempoMap = midiFile.GetTempoMap();
long ticks = 123;
MetricTimeSpan metricTime = TimeConverter.ConvertTo<MetricTimeSpan>(ticks, tempoMap);
MusicalTimeSpan musicalTimeFromTicks = TimeConverter.ConvertTo<MusicalTimeSpan>
(ticks, tempoMap);
MusicalTimeSpan musicalTimeFromMetric = TimeConverter.ConvertTo<MusicalTimeSpan>
(metricTime, tempoMap);
BarBeatTicksTimeSpan barBeatTicksTimeFromMetric =
TimeConverter.ConvertTo<BarBeatTicksTimeSpan>(metricTime, tempoMap);
long ticksFromMusical = TimeConverter.ConvertFrom(musicalTimeFromTicks, tempoMap);
MetricTimeSpan metricLength = LengthConverter.ConvertTo<MetricTimeSpan>
(ticks, time, tempoMap);
MusicalTimeSpan musicalLengthFromMetric =
LengthConverter.ConvertTo<MusicalTimeSpan>(metricLength,
metricTime,
tempoMap);
long 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:
TempoMap tempoMap = midiFile.GetTempoMap();
IEnumerable<Note> notes = midiFile.GetNotes()
.AtTime(new BarBeatTicksTimeSpan(10, 4), tempoMap);
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:
var tempoMap = midiFile.GetTempoMap();
var notesAt30sec = midiFile.GetNotes()
.EndAtTime(new MetricTimeSpan(0, 0, 30), tempoMap);
Some examples of how you can create an instance of specific time span class:
var metricTimeSpan1 = new MetricTimeSpan(100000 );
var metricTimeSpan2 = new MetricTimeSpan(0, 1, 55);
var metricTimeSpan3 = new MetricTimeSpan();
var barBeatTicksTimeSpan = new BarBeatTicksTimeSpan(2, 7);
var musicalTimeSpan1 = MusicalTimeSpan.Eighth.Triplet();
var musicalTimeSpan2 = 5 * new MusicalTimeSpan(5, 17);
If you want, for example, to know length of a MIDI file in minutes and seconds, you can use this code:
var tempoMap = midiFile.GetTempoMap();
var midiFileDuration = midiFile.GetTimedEvents()
.LastOrDefault(e => e.Event is NoteOffEvent)
?.TimeAs<MetricTimeSpan>(tempoMap)
?? new MetricTimeSpan();
ITimeSpan
interface has several methods to perform arithmetic operations on time spans. For example, to add metric length to metric time, you can write:
var timeSpan1 = new MetricTimeSpan(0, 2, 20);
var timeSpan2 = new MetricTimeSpan(0, 0, 10);
ITimeSpan result = timeSpan1.Add(timeSpan2, TimeSpanMode.TimeLength);
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.
You can also subtract one time span from another one:
var timeSpan1 = new MetricTimeSpan(0, 10, 0);
var timeSpan2 = new MusicalTimeSpan(3, 8);
ITimeSpan result = timeSpan1.Subtract(timeSpan2, TimeSpanMode.TimeTime);
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.
To stretch or shrink a time span, use Multiply
or Divide
methods:
ITimeSpan stretchedTimeSpan = new MetricTimeSpan(0, 0, 10).Multiply(2.5);
ITimeSpan shrinkedTimeSpan = new BarBeatTicksTimeSpan(0, 2).Divide(2);
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.
Also please take a look at Time and length - Overview article.
[^] top
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:
var patternBuilder = new PatternBuilder()
.StepForward(new MetricTimeSpan(0, 0, 5))
.Note(Octave.Get(4).CSharp, MusicalTimeSpan.Eighth)
.SetNoteLength(MusicalTimeSpan.Eighth.Triplet())
.SetOctave(Octave.Get(5))
.Note(NoteName.A)
.Note(NoteName.B)
.Note(NoteName.GSharp);
Build
method will return an instance of the Pattern
class containing all actions performed with the builder. Pattern
then can be exported to a MIDI file:
Pattern pattern = patternBuilder.Build();
MidiFile midiFile = pattern.ToFile(TempoMap.Default);
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".
[^] top
Tools
There are several classes in the DryWetMIDI
which can help to solve complex tasks such as notes quantizing. Let's take a short overview of tools aimed to do these tasks.
You can find detailed description of all such tools in the library documentation.
[^] top
Examples
Let's see how you can use the DryWetMIDI
for some real tasks.
Notes merging
Sometimes, in real MIDI files, notes can overlap each other. Although DryWetMIDI
already provides the tool for this purpose – Merger – we'll try to implement a simple version. The following method merges overlapping notes of the same channel and pitch:
public static void MergeNotes(MidiFile midiFile)
{
foreach (var trackChunk in midiFile.GetTrackChunks())
{
MergeNotes(trackChunk);
}
}
private static void MergeNotes(TrackChunk trackChunk)
{
using (var notesManager = trackChunk.ManageNotes())
{
var notes = notesManager.Objects;
var currentNotes = new Dictionary<FourBitNumber,
Dictionary<SevenBitNumber, Note>>();
foreach (var note in notes.ToList())
{
var channel = note.Channel;
if (!currentNotes.TryGetValue(channel, out var currentNotesByNoteNumber))
currentNotes.Add(channel, currentNotesByNoteNumber =
new Dictionary<SevenBitNumber, Note>());
if (!currentNotesByNoteNumber.TryGetValue
(note.NoteNumber, out var currentNote))
{
currentNotesByNoteNumber.Add(note.NoteNumber, currentNote = note);
continue;
}
var currentEndTime = currentNote.Time + currentNote.Length;
if (note.Time <= currentEndTime)
{
var endTime = Math.Max(note.Time + note.Length, currentEndTime);
currentNote.Length = endTime - currentNote.Time;
notes.Remove(note);
}
else
currentNotesByNoteNumber[note.NoteNumber] = note;
}
}
}
If we take input file like that:
and perform merging with the method above, we'll get the following result:
MergeNotes(midiFile);
[^] top
Building of "Moonlight Sonata"
This example shows how you can use Pattern
to build a musical composition. First, four bars of the Beethoven's "Moonlight Sonata" will help us with this.
using Melanchall.DryWetMidi.MusicTheory;
using Melanchall.DryWetMidi.Interaction;
using Melanchall.DryWetMidi.Composing;
public static Pattern BuildMoonlightSonata()
{
var bassChord = new[] { Interval.Twelve };
return new PatternBuilder()
.SetNoteLength(MusicalTimeSpan.Eighth.Triplet())
.Anchor()
.SetRootNote(Octave.Get(3).GSharp)
.Note(Interval.Zero)
.Note(Interval.Five)
.Note(Interval.Eight)
.Repeat(3, 7)
.Note(Interval.One)
.Note(Interval.Five)
.Note(Interval.Eight)
.Repeat(3, 1)
.Note(Interval.One)
.Note(Interval.Six)
.Note(Interval.Ten)
.Repeat(3, 1)
.Note(Interval.Zero)
.Note(Interval.Four)
.Note(Interval.Ten)
.Note(Interval.Zero)
.Note(Interval.Five)
.Note(Interval.Eight)
.Note(Interval.Zero)
.Note(Interval.Five)
.Note(Interval.Seven)
.Note(-Interval.Two)
.Note(Interval.Four)
.Note(Interval.Seven)
.MoveToFirstAnchor()
.SetNoteLength(MusicalTimeSpan.Whole)
.Chord(bassChord, Octave.Get(2).CSharp)
.Chord(bassChord, Octave.Get(1).B)
.SetNoteLength(MusicalTimeSpan.Half)
.Chord(bassChord, Octave.Get(1).A)
.Chord(bassChord, Octave.Get(1).FSharp)
.Chord(bassChord, Octave.Get(1).GSharp)
.Repeat()
.Build();
}
[^] top
Links
[^] top
History
- 19th July, 2024
- Small fixes in the code samples and text
- 19th August, 2023
- Small fixes in the code samples
- 9th June, 2022
- Article updated to reflect changes introduced in the
DryWetMIDI
6.1.0
- 26th June, 2021
- Replaced links to Wiki with links to the library documentation
- 23d November, 2019
- Article updated to reflect breaking changes introduced in the
DryWetMIDI
5.0.0
- 2nd February, 2019
- Article updated to reflect changes introduced in the
DryWetMIDI
4.0.0: new methods and classes
- 11th June, 2018
- Article updated to reflect changes introduced in the
DryWetMIDI
3.0.0: new methods and classes
- 7th November, 2017
- Article updated to reflect changes introduced in the
DryWetMIDI
2.0.0: time and length classes were generalized with ITimeSpan
- 24th August, 2017
My primary skills are C#, WPF and ArcObjects/ArcGIS Pro SDK. Currently I'm working on autotests in Kaspersky Lab as a development team lead.
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 and MIDI devices. The library is currently actively developing.