-
-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fd6ae29
commit de3e6d6
Showing
25 changed files
with
384 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
--- | ||
uid: a_files_merging | ||
--- | ||
|
||
# MIDI files merging | ||
|
||
[Merger](xref:Melanchall.DryWetMidi.Tools.Merger) provides two ways of merging MIDI files: | ||
|
||
* **sequentially** – placing files one after other; | ||
* **simultaneously** – placing files "one below other". | ||
|
||
Both routines are extension methods for `IEnumerable<MidiFile>`. Please see sections below to learn more about each method. | ||
|
||
## MergeSequentially | ||
|
||
Image below shows a quick overview of what you'll get with the [MergeSequentially](xref:Melanchall.DryWetMidi.Tools.Merger.MergeSequentially*) method: | ||
|
||
![MergeSequentially](images/Merger/MergeFiles/MergeSequentially.png) | ||
|
||
Here we're merging three files each of two track chunks. So as you can see events of the second file are placed right after last event of the first file, and events of the third file are placed directly after last event of the second one. In the code it looks like that: | ||
|
||
```csharp | ||
midiFiles.MergeSequentially(); | ||
``` | ||
|
||
There is one important note about how merging works. Obviously files can have different time divisions. If shortly, a MIDI tick (single unit of time) can have different length in milliseconds. Just placing files one after other and using time division of the first file, for example, we'll ruine timings of MIDI events of the files are merged. | ||
|
||
To preserve correct timings of all MIDI events of all input files in the result one [MergeSequentially](xref:Melanchall.DryWetMidi.Tools.Merger.MergeSequentially*) does following steps: | ||
|
||
1. calculates new time division as the least common multiple of TPQNs ([ticks per quarter note](xref:Melanchall.DryWetMidi.Core.TicksPerQuarterNoteTimeDivision)) of the input files; | ||
2. for each input file calculates a scale factor every delta-time in the file should be multiplied by; | ||
3. writes MIDI events in the result file using the new time division and scaled delta-times. | ||
|
||
If you want to add some spacing between files, you can set its length using [SequentialMergingSettings](xref:Melanchall.DryWetMidi.Tools.SequentialMergingSettings) and the [DelayBetweenFiles](xref:Melanchall.DryWetMidi.Tools.SequentialMergingSettings.DelayBetweenFiles) property: | ||
|
||
```csharp | ||
midiFiles.MergeSequentially(new SequentialMergingSettings | ||
{ | ||
DelayBetweenFiles = MusicalTimeSpan.Quarter | ||
}); | ||
``` | ||
|
||
![MergeSequentially with DelayBetweenFiles](images/Merger/MergeFiles/MergeSequentially-DelayBetweenFiles.png) | ||
|
||
We may want also to align starts of the input files to a grid. It would be better to describe the task with an example: | ||
|
||
```csharp | ||
var midiFile = new PatternBuilder() | ||
.SetNoteLength(MusicalTimeSpan.Quarter) | ||
.StepForward(MusicalTimeSpan.Half) | ||
.Note("A4") | ||
.Build() | ||
.ToFile(TempoMap.Default); | ||
``` | ||
|
||
Here we have the file with a single A4 note of quarter length. The note starts at 1/2. So we have a space (`S`) of quarter length from the end of the note (`N`) to the end of a bar: | ||
|
||
```text | ||
0 4/4 | ||
|----| | ||
| AS| | ||
``` | ||
|
||
And now we want to merge this file with another one starting the second file at the bar line. For this purpose there is the [FileDurationRoundingStep](xref:Melanchall.DryWetMidi.Tools.SequentialMergingSettings.FileDurationRoundingStep) property: | ||
|
||
```csharp | ||
midiFiles.MergeSequentially(new SequentialMergingSettings | ||
{ | ||
FileDurationRoundingStep = new BarBeatTicksTimeSpan(1) | ||
}); | ||
``` | ||
|
||
Here we state that duartion of each file should be [rounded up](xref:Melanchall.DryWetMidi.Interaction.TimeSpanRoundingPolicy.RoundUp) to 1 bar and then a next file can be placed: | ||
|
||
![MergeSequentially with FileDurationRoundingStep](images/Merger/MergeFiles/MergeSequentially-FileDurationRoundingStep.png) | ||
|
||
If both [DelayBetweenFiles](xref:Melanchall.DryWetMidi.Tools.SequentialMergingSettings.DelayBetweenFiles) and [FileDurationRoundingStep](xref:Melanchall.DryWetMidi.Tools.SequentialMergingSettings.FileDurationRoundingStep) are set, the method will first round duration and then add a delay. | ||
|
||
In all previous examples you can see that a result file has number of track chunks that is the sum of all input files' track chunks numbers. So a track chunk of an input MIDI file will be written to a separate track chunk in a result MIDI file. We can change this default behavior setting [ResultTrackChunksCreationPolicy](xref:Melanchall.DryWetMidi.Tools.SequentialMergingSettings.ResultTrackChunksCreationPolicy) to the [ResultTrackChunksCreationPolicy](xref:Melanchall.DryWetMidi.Tools.ResultTrackChunksCreationPolicy.MinimizeCount) value: | ||
|
||
```csharp | ||
midiFiles.MergeSequentially(new SequentialMergingSettings | ||
{ | ||
ResultTrackChunksCreationPolicy = ResultTrackChunksCreationPolicy.MinimizeCount | ||
}); | ||
``` | ||
|
||
![MergeSequentially with ResultTrackChunksCreationPolicy set to MinimizeCount](images/Merger/MergeFiles/MergeSequentially-MinimizeTrackChunksCount.png) | ||
|
||
## MergeSimultaneously | ||
|
||
Image below shows a quick overview of how the [MergeSimultaneously](xref:Melanchall.DryWetMidi.Tools.Merger.MergeSimultaneously*) method works: | ||
|
||
![MergeSimultaneously](images/Merger/MergeFiles/MergeSimultaneously.png) | ||
|
||
So with this code: | ||
|
||
```csharp | ||
midiFiles.MergeSimultaneously(); | ||
``` | ||
|
||
track chunks of all input files will be "stacked" in the result MIDI file. | ||
|
||
This method uses the same logic to preserve timings of the input files as the [MergeSequentially](#mergesequentially) one. But [MergeSimultaneously](xref:Melanchall.DryWetMidi.Tools.Merger.MergeSimultaneously*) has a limitation by default: tempo maps of all input files after this logic applied must be equal. It means that all files must have the same tempo changes and time signature changes and at the same times. You can turn off this check with the [IgnoreDifferentTempoMaps](xref:Melanchall.DryWetMidi.Tools.SimultaneousMergingSettings.IgnoreDifferentTempoMaps) property set to `true`: | ||
|
||
```csharp | ||
midiFiles.MergeSimultaneously(new SimultaneousMergingSettings | ||
{ | ||
IgnoreDifferentTempoMaps = true | ||
}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
--- | ||
uid: a_obj_merging | ||
--- | ||
|
||
# Objects merging | ||
|
||
To merge nearby objects into one DryWetMIDI provides [Merger](xref:Melanchall.DryWetMidi.Tools.Merger) class. Quick example of merging in action: | ||
|
||
![Objects merging](images/Merger/MergeObjects/MergeObjects.png) | ||
|
||
Process of merging can be adjusted via [ObjectsMergingSettings](xref:Melanchall.DryWetMidi.Tools.ObjectsMergingSettings). By default two objects should have no gap between them to be merged. But you can specify any desired tolerance via settings: | ||
|
||
```csharp | ||
var newObjects = objects.MergeObjects( | ||
TempoMap.Default, | ||
new ObjectsMergingSettings | ||
{ | ||
Tolerance = new MetricTimeSpan(0, 0, 1) | ||
}); | ||
``` | ||
|
||
Now objects will be merged if the distance between them from `0` to `1` second. So tolerance is maximum distance between two objects to consider them as nearby. Please take a look at how tolerance (`T`) affects process of merging: | ||
|
||
![Objects merging tolerance](images/Merger/MergeObjects/MergeObjects-Tolerance.png) | ||
|
||
Of course merging available not for objects collections only. You can use also [MergeObjects](xref:Melanchall.DryWetMidi.Tools.Merger.MergeObjects*) methods on [MidiFile](xref:Melanchall.DryWetMidi.Core.MidiFile) and [TrackChunk](xref:Melanchall.DryWetMidi.Core.TrackChunk): | ||
|
||
```csharp | ||
midiFile.MergeObjects( | ||
ObjectType.Note | ObjectType.Chord, | ||
new ObjectsMergingSettings | ||
{ | ||
Filter = obj => obj.Time > 100 | ||
}, | ||
new ObjectDetectionSettings | ||
{ | ||
ChordDetectionSettings = new ChordDetectionSettings | ||
{ | ||
NotesMinCount = 3 | ||
} | ||
}); | ||
``` | ||
|
||
The tool need to determine somehow whether two objects have the same "key" or not to take decision about merging them. For example, if we have a `C` note and `D` one, by default such notes are different in terms of their keys and thus won't be merged. To understand what the key is, please read [MIDI file splitting: SplitByObjects](xref:a_file_splitting#splitbyobjects) article. | ||
|
||
Of course you can customize how objects are merged. For example, following picture shows how chords are merged using the default merging logic: | ||
|
||
![Chords merging using default logic](images/Merger/MergeObjects/MergeObjects-Chords.png) | ||
|
||
Now let's change the logic: chords can be merged only if there are notes in them without gap. Also notes in result chord need to start at the same time and have the same length. Following image shows how chords will be merged: | ||
|
||
![Chords merging using custom logic](images/Merger/MergeObjects/MergeObjects-Chords-Custom.png) | ||
|
||
We need to derive from the [ObjectsMerger](xref:Melanchall.DryWetMidi.Tools.ObjectsMerger) class to implement these rules: | ||
|
||
```csharp | ||
private sealed class ChordsMerger : ObjectsMerger | ||
{ | ||
public ChordsMerger(ILengthedObject obj) | ||
: base(obj) | ||
{ } | ||
|
||
public override bool CanAddObject(ILengthedObject obj, TempoMap tempoMap, ObjectsMergingSettings settings) | ||
{ | ||
if (!base.CanAddObject(obj, tempoMap, settings)) | ||
return false; | ||
|
||
var chordNotes = ((Chord)obj).Notes.ToArray(); | ||
var lastChordNotes = ((Chord)_objects.Last()).Notes.ToArray(); | ||
|
||
return Enumerable | ||
.Range(0, lastChordNotes.Length) | ||
.Any(i => lastChordNotes[i].EndTime == chordNotes[i].Time); | ||
} | ||
|
||
public override ILengthedObject MergeObjects(ObjectsMergingSettings settings) | ||
{ | ||
var result = (Chord)base.MergeObjects(settings); | ||
var time = result.Time; | ||
var length = result.Length; | ||
|
||
foreach (var note in result.Notes) | ||
{ | ||
note.Time = time; | ||
note.Length = length; | ||
} | ||
|
||
return result; | ||
} | ||
} | ||
``` | ||
|
||
And now we can merge objects using this class: | ||
|
||
```csharp | ||
midiFile.MergeObjects( | ||
ObjectType.Chord | ObjectType.Note, | ||
new ObjectsMergingSettings | ||
{ | ||
ObjectsMergerFactory = obj => obj is Chord | ||
? new ChordsMerger(obj) | ||
: new ObjectsMerger(obj) | ||
}, | ||
new ObjectDetectionSettings | ||
{ | ||
ChordDetectionSettings = new ChordDetectionSettings | ||
{ | ||
NotesTolerance = 100 | ||
} | ||
}); | ||
``` | ||
|
||
So if the tool encounters a chord, it uses our custom merger; for any other object's type – default one. |
Oops, something went wrong.