This article demonstrates how to use the open-source library DryWetMIDI to quantize notes in a MIDI file with advanced features like specifying snapping points, groove quantization, and randomization. It includes detailed examples, implementation explanations, and usage instructions for building automated MIDI processing scripts.
Introduction
The purpose of this article is to show how you can quantize notes of an MIDI file using DryWetMIDI – open-source library for managing MIDI files. We are going to write a console application that will perform this task.
In the previous article, an example of simple quantization was provided. But the application we are going to write now will be much more advanced, allowing, for example, to specify area near snapping point to randomize a note's start or end within; or to specify set of grid steps to perform groove quantization.
[^] top
Contents
- Usage
- Examples
- Implementation
- Links
- History
[^] top
Usage
First of all, we should describe the usage of our program, i.e., the name of the program, its arguments and valid combinations of them. Every time a user calls the program in a wrong way, we will show the following help:
USAGE:
quantize <input_path> <grid> [-o <output_path>]
[-s | -e]
[-ke | -ks]
[-t <tolerance>]
Parameters:
<input_path> Input MIDI file path.
<grid> Steps of grid to quantize by.
-o <output_path> Output file path.
-s Quantize starts of notes.
-e Quantize ends of notes.
-ke Keep ends of notes.
-ks Keep starts of notes.
-t <tolerance> Tolerance to randomize note's start or end.
where <...>
means mandatory argument, [...]
means optional argument and |
means a choice (only one of arguments separated by |
can be used at once).
So the program will be named as quantize. Let's discuss its arguments.
<input_path>
The path of a MIDI file to quantize notes of.
<grid>
Sequence of steps of the grid to quantize notes by. Each step should be separated from another one by comma. Steps will be parsed to instances of classes implementing ITimeSpan
interface. Let's see valid string
s that can be parsed to those instances.
MetricTimeSpan
Represents a step in terms of microseconds. String
s that can be parsed as an instance of the MetricTimeSpan
have to be in the following format:
[Hours :] Minutes : Seconds [: Milliseconds]
Examples of valid string
s:
"0:0:0:500"
"0:4"
"8:20:30"
Every component should be a nonnegative long
number.
MusicalTimeSpan
Represents a step in terms of a fraction of the whole note. String
s that can be parsed as an instance of the MusicalTimeSpan
have to be in the following format:
<Fraction | FractionMnemonic> [Tuplet | TupletMnemonic] [.+]
where:
Fraction should be in form of Numerator / Denominator where Numerator can be omitted if it equals 1.
FractionMnemonic can be one of the following strings: w (whole), h (half), q (quarter), e (eighth) or s (sixteenth).
Tuplet should be in form of [NotesCount : SpaceSize] which defines a tuplet with NotesCount notes in space of SpaceSize notes. For example, [3:2] means triplet.
TupletMnemonic can be one of the following strings: t (triplet) or d (duplet).
.+ means one or more dots.
Examples of valid string
s:
"5/8"
"q"
"e.."
"/4t"
"wd."
"1/9[3:5]"
BarBeatTicksTimeSpan
Represents a step in terms of number of bars, beats and ticks. String
s that can be parsed as an instance of the BarBeatTicksTimeSpan
have to be in the following format:
Bars.Beats.Ticks
Examples of valid string
s:
"0.1.8"
"10.0.0"
"0.0.5"
BarBeatFractionTimeSpan
Represents a step in terms of number of bars and fractional beats. String
s that can be parsed as an instance of the BarBeatFractionTimeSpan
have to be in the following format:
Bars_BeatsIntegerPart.BarsFractionalPart
Examples of valid string
s:
"0_0.0"
"1_0.0"
"0_10.5"
"100_20.2"
MidiTimeSpan
Represents a step in terms of either ticks (in case of ticks per quarter note time division of a MIDI file) or subdivisions of frame (in case of SMPTE time division). String
s that can be parsed as an instance of the MidiTimeSpan
have to be in the following format:
TimeSpan
Examples of valid string
s:
"300"
MidiTimeSpan
exists in the library for unification purposes and nearly useless for musicians.
-o <output_path>
The path of the output MIDI file. If this argument is not specified, an input file will be rewritten.
-s
This option instructs the program to quantize start times of notes. Start time is the default target of quantization routine. If -ke
option is not specified, length of a note will not be changed.
-e
This option instructs the program to quantize end times of notes. If -ks
option is not specified, length of a note will not be changed.
-ke
This option instructs the program to keep notes ends untouched in case of start times quantization. The length of a note can be changed in general case when this option is used.
-ks
This option instructs the program to keep notes starts untouched in case of end times quantization. The length of a note can be changed in general case when this option is used.
-t <tolerance>
Specifies the tolerance to randomize start or end time of a note within. In other words, it is the size of area around a nearest grid point where note's time should be placed. <tolerance>
is any valid string
that can be parsed to an instance of a class implementing the ITimeSpan
(see description of the <grid>
argument above to know what string
s can be used for tolerance).
[^] top
Examples
Let's look at some examples of what our program can do. We will use input MIDI file with the following notes:
This file named input.mid is inside the archive files.zip attached to the article along with sources.zip archive with source code. As you can see, it is just a one bar with random notes. Now we are going to execute quantize program with different arguments and see the results.
We will start with a simple case – quantization of notes starts by grid of eighth step (-s can be omitted since the start time of a note is the default target of quantization):
quantize input.mid 1/8 -s
Bottom bar shows the grid used to quantize notes. Gray rectangles show original notes so we can check the correctness of processing visually.
Now we will try to perform quantization by grid of variable step (aka groove quantization). q,e,e defines a grid where steps are 1/4, 1/8, 1/8, 1/4, 1/8, 1/8, ... so the pattern is [1/4, 1/8, 1/8]:
quantize input.mid q,e,e -s
Let's add more groove! [1/16, 1/8, 1/16, 1/4, triplet 1/8, triplet 1/8, triplet 1/8, dotted 1/8, 1/16] pattern will help us with this:
quantize input.mid s,e,s,q,et,et,et,e.,s
By default, quantization of note's start time keeps note length untouched, so entire note is moved to another time. We can use -ke argument to keep ends of notes unchanged, so length can be changed during the quantization.
quantize input.mid q,e,e -s -ke
If we want to quantize end time of a note, -e argument should be used:
quantize input.mid q.,e -e
As with quantization of note's start time, we can keep opposite side of the note unchanged. Quantizing end time -ks argument should be used to keep start time untouched.
quantize input.mid q.,e -e -ks
Also, the tolerance can be specified with help of -t argument. Tolerance is the size of area around grid points where target (start or end time) can be randomly placed during quantization process. This is one of the approaches to "humanize" MIDI music. The following example shows quantization of start times to the grid with step of quarter length using tolerance of thirty-second length:
quantize input.mid q -t 1/32
[^] top
Implementation
It's time to take a look at the code of the program. The core class is Quantizer
with one public
method – Quantize
:
internal static class Quantizer
{
public static void Quantize(QuantizerArguments arguments)
{
var midiFile = MidiFile.Read(arguments.InputPath);
switch (arguments.Target)
{
case QuantizationTarget.Start:
QuantizeStart(midiFile, arguments.Grid,
arguments.Tolerance, arguments.KeepEnd);
break;
case QuantizationTarget.End:
QuantizeEnd(midiFile, arguments.Grid,
arguments.Tolerance, arguments.KeepStart);
break;
}
midiFile.Write(arguments.OutputPath, true);
}
}
QuantizerArguments
just holds arguments of the program:
internal sealed class QuantizerArguments
{
public string InputPath { get; }
public string OutputPath { get; }
public IEnumerable<ITimeSpan> Grid { get; }
public QuantizationTarget Target { get; }
public bool KeepEnd { get; }
public bool KeepStart { get; }
public ITimeSpan Tolerance { get; }
}
We will not discuss how an instance of the QuantizerArguments
is created since it is not a subject of the article. You can take a look at source code attached to the article if you are interested in it.
Now we are going to discuss implementation of QuantizeStart
and QuantizeEnd
methods of the Quantizer
class.
private static void QuantizeStart(MidiFile midiFile,
IEnumerable<ITimeSpan> gridSteps,
ITimeSpan tolerance,
bool keepEnd)
{
if (midiFile == null)
throw new ArgumentNullException(nameof(midiFile));
if (gridSteps == null)
throw new ArgumentNullException(nameof(gridSteps));
var tempoMap = midiFile.GetTempoMap();
var grid = BuildGrid(midiFile, gridSteps, tempoMap).ToList();
if (!grid.Any())
return;
var random = new Random();
midiFile.ProcessNotes(n =>
{
var startTime = n.Time;
var endTime = startTime + n.Length;
var newStartTime = FindNearestTime(grid, startTime);
newStartTime = RandomizeTime(newStartTime, tolerance, random, tempoMap);
if (keepEnd)
n.Length = Math.Max(0, endTime - newStartTime);
n.Time = newStartTime;
});
}
ProcessNotes
extension method from Melanchall.DryWetMidi.Interaction.NotesManagingUtilities
allows to modify notes inside a MidiFile
in an easy way. Algorithm of a note's start quantization is:
- create a grid to quantize by
- find a point of the grid nearest to a note's start
- get random time within the specified tolerance of the grid point found on previous step
- if note's end should be untouched (
-ke
option is specified), calculate new length of the note - set new time of the note calculated on step 3
BuildGrid
creates a grid to quantize by. Result grid is a collection of times represented as long
values which is internal representation of time and length within a MIDI file. We are going through the specified steps of the grid and calling TimeConverter.ConvertFrom
to convert ITimeSpan
to a long
value. Note that we need TempoMap
object to perform such conversion since a MIDI file can contain tempo and time signature changes and thus they need to be taken into an account to turn, for example, seconds into ticks (long
value). Code of the method is presented below:
private static IEnumerable<long> BuildGrid(MidiFile midiFile,
IEnumerable<ITimeSpan> gridSteps,
TempoMap tempoMap)
{
var lastNote = midiFile.GetNotes().LastOrDefault();
if (lastNote == null)
yield break;
var time = 0L;
var lastNoteTime = lastNote.Time;
while (true)
{
foreach (var step in gridSteps)
{
if (time > lastNoteTime)
yield break;
time = TimeConverter.ConvertFrom(((MidiTimeSpan)time).Add
(step, TimeSpanMode.TimeLength),
tempoMap);
yield return time;
}
}
}
Since Time
property of the Note
class is already has long
type, search of a grid's time nearest to a note's start is quite simple. We should just loop through the grid and take a point where difference between grid's time and note's time is smallest:
private static long FindNearestTime(IEnumerable<long> grid, long time)
{
var difference = long.MaxValue;
var nearestTime = 0L;
foreach (var gridTime in grid)
{
var timeDelta = Math.Abs(time - gridTime);
if (timeDelta >= difference)
break;
difference = timeDelta;
nearestTime = gridTime;
}
return nearestTime;
}
To randomize time within the specified tolerance, we need to calculate minimum and maximum times which define boundaries of the allowable area. Then we should just take a random number between those times:
private static long RandomizeTime
(long time, ITimeSpan tolerance, Random random, TempoMap tempoMap)
{
if (tolerance == null)
return time;
var minTime = CalculateBoundaryTime
(time, tolerance, MathOperation.Subtract, tempoMap);
var maxTime = CalculateBoundaryTime(time, tolerance, MathOperation.Add, tempoMap);
return GetRandomTime(minTime - 1, maxTime, random) + 1;
}
private static long CalculateBoundaryTime(long time,
ITimeSpan tolerance,
MathOperation operation,
TempoMap tempoMap)
{
ITimeSpan boundaryTime = (MidiTimeSpan)time;
switch (operation)
{
case MathOperation.Add:
boundaryTime = boundaryTime.Add(tolerance, TimeSpanMode.TimeLength);
break;
case MathOperation.Subtract:
boundaryTime = boundaryTime.Subtract(tolerance, TimeSpanMode.TimeLength);
break;
}
return Math.Max(0, TimeConverter.ConvertFrom(boundaryTime, tempoMap));
}
private static long GetRandomTime(long minTime, long maxTime, Random random)
{
var difference = (int)Math.Abs(maxTime - minTime);
return minTime + random.Next(difference);
}
QuantizeEnd
utilizes the same methods. But obviously changes end time of a note instead of the start one. Implementation of the method has nothing mystical:
private static void QuantizeEnd(MidiFile midiFile,
IEnumerable<ITimeSpan> gridSteps,
ITimeSpan tolerance,
bool keepStart)
{
if (midiFile == null)
throw new ArgumentNullException(nameof(midiFile));
if (gridSteps == null)
throw new ArgumentNullException(nameof(gridSteps));
var tempoMap = midiFile.GetTempoMap();
var grid = BuildGrid(midiFile, gridSteps, tempoMap).ToList();
if (!grid.Any())
return;
var random = new Random();
midiFile.ProcessNotes(n =>
{
var startTime = n.Time;
var endTime = startTime + n.Length;
var newEndTime = FindNearestTime(grid, endTime);
newEndTime = RandomizeTime(newEndTime, tolerance, random, tempoMap);
if (keepStart)
n.Length = Math.Max(0, newEndTime - startTime);
n.Time = newEndTime - n.Length;
});
}
And that's all! Our quantizer can now process files which allow to build automated scripts that can, for example, prepare MIDI files for further manipulations with another script or DAW.
Last thing that can be interested is how we get grid as IEnumerable<ITimeSpan>
from the string
representation passed to the program as an argument. It is super easy:
IEnumerable<ITimeSpan> grid = gridAsString.Split(new char[] { ',' },
StringSplitOptions.RemoveEmptyEntries)
.Select(TimeSpanUtilities.Parse);
TimeSpanUtilities.Parse
method takes a string
and returns correct implementation of the ITimeSpan
or throws error if the string
has invalid format.
[^] top
Links
[^] top
History
- 20th October, 2021
- Fixed format strings to parse
BarBeatFractionTimeSpan
- 23d November, 2019
- Article updated to reflect changes introduced in the
DryWetMIDI
5.0.0
- 26th March, 2018