Quantizer
DryWetMIDI provides a tool to perform quantization of objects of different types by the specified grid. The class aimed to solve this task is Quantizer. Sections below describe usage of the tool in detail.
Note that quantizing routine modifies passed objects instead of returning new ones with quantized times. So be sure you've cloned input objects if you want to save them. All classes implementing ITimedObject as well as MidiFile and TrackChunk have Clone
method you can use for that purpose.
Also there are QuantizerUtilities class that contains useful methods to quantize objects inside TrackChunk and MidiFile without necessity to work with objects collections directly.
Please note that the article doesn't cover all settings and use cases so please read API documentation on Quantizer to get complete information.
General information
First of all let's see how arbitrary timed objects quantized:
Quantizing can be adjusted in many ways by the QuantizingSettings. Please read documentation on the class to see all available properties. For example, image below shows quantizing with different values of QuantizingLevel:
Quantizing ILengthedObject
An arbitrary object implements ITimedObject and thus its Time property gets quantized. But if an object implements ILengthedObject interface, you have several options:
- quantize start time;
- quantize end time;
- quantize both start and end times.
You choose the desired option specifying QuantizingSettings.Target property.
By default if an object is quantized, it will be entirely moved to a grid position. So if you quantize start time, end time can be changed since the object will be moved. You can see the process in action on the first image of the article. Of course this behavior can be altered. Just set FixOppositeEnd to true
to prevent changing of time that is not the target of quantizing. The following image illustrates quantizing of start time with the property set to true
:
Of course this property works in case of end time quantizing too.
When the start time of an object is not fixed, there is a chance that the object's end time will be quantized in such a way that the start time will be negative due to the object is moved to the left. Negative time is invalid so you can set QuantizingSettings.QuantizingBeyondZeroPolicy property to desired value to handle this situation. The image below shows how quantizing works if the property set to FixAtZero:
Also if one side (start or end) of an object is fixed, there is a chance that the object's opposite time will be quantized in such a way that the object will be reversed resulting to negative length. You can handle this situation with the QuantizingSettings.QuantizingBeyondFixedEndPolicy property. The image below shows some options in action when start time is being quantized beyond the end one:
Custom quantizing
You can derive from the Quantizer class and override its OnObjectQuantizing method. Inside this method you can decide whether quantizing for an object should be performed or not and if yes, what new time should be set.
Information about what the quantizer is going to do with an object is passed via the quantizedTime
parameter of QuantizedTime type. Image below shows what information is held within this class:
A: GridTime
Grid time that was selected for an object as the nearest one.
B: NewTime
The new time of an object that was calculated during quantizing.
The distance between an object's current time and the nearest grid time. There is also ConvertedDistanceToGridTime which holds the distance as time span of the type specified by DistanceCalculationType property of quantization settings.
D: Shift
The distance an object is going to be moved on toward the new time. If QuantizingLevel is less than 1.0
, D
will be less than C
.
Let's create a simple custom quantizer. We will call it SoftQuantizer
:
public sealed class SoftQuantizer : Quantizer
{
protected override TimeProcessingInstruction OnObjectQuantizing(
ITimedObject obj,
QuantizedTime quantizedTime,
IGrid grid,
LengthedObjectTarget target,
TempoMap tempoMap,
QuantizingSettings settings)
{
return (MusicalTimeSpan)quantizedTime.ConvertedDistanceToGridTime > MusicalTimeSpan.Eighth
? TimeProcessingInstruction.Skip
: base.OnObjectQuantizing(obj, quantizedTime, grid, target, tempoMap, settings);
}
}
What does it do? If the distance between an object and the nearest grid time is greater than 1/8
, just don't quantize the object. Otherwise do base quantizing.
Our small program to test the tool:
class Program
{
static void Main(string[] args)
{
var tempoMap = TempoMap.Default;
var midiFile = new PatternBuilder()
.SetNoteLength(MusicalTimeSpan.Eighth)
.StepForward(MusicalTimeSpan.Sixteenth)
.Note("A5")
.StepForward(MusicalTimeSpan.Quarter)
.Note("B2")
.StepForward(new MusicalTimeSpan(3, 8))
.Note("C#3")
.Build()
.ToFile(tempoMap);
Console.WriteLine("Notes before quantizing:");
PrintNotes(midiFile);
midiFile.QuantizeObjects(
new SoftQuantizer(),
ObjectType.Note,
new SteppedGrid(MusicalTimeSpan.Whole),
new QuantizingSettings
{
DistanceCalculationType = TimeSpanType.Musical
});
Console.WriteLine("Notes after quantizing:");
PrintNotes(midiFile);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
static void PrintNotes(MidiFile midiFile)
{
var notes = midiFile.GetNotes();
var tempoMap = midiFile.GetTempoMap();
foreach (var note in notes)
{
var time = note.TimeAs<MusicalTimeSpan>(tempoMap);
var length = note.LengthAs<MusicalTimeSpan>(tempoMap);
Console.WriteLine($"Note [{note}]: time = [{time}], length = [{length}]");
}
}
}
If we run the program, we'll get following output:
Notes before quantizing:
Note [A5]: time = [1/16], length = [1/8]
Note [B2]: time = [7/16], length = [1/8]
Note [C#3]: time = [15/16], length = [1/8]
Notes after quantizing:
Note [A5]: time = [0/1], length = [1/8]
Note [B2]: time = [7/16], length = [1/8]
Note [C#3]: time = [1/1], length = [1/8]
Press any key to exit...
So all works as expected, the middle note is not quantized since it's too far from grid times.