Skip to content

Commit

Permalink
[Docs] Articles on new tools
Browse files Browse the repository at this point in the history
  • Loading branch information
melanchall committed Jun 26, 2023
1 parent fd6ae29 commit de3e6d6
Show file tree
Hide file tree
Showing 25 changed files with 384 additions and 113 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ x86/
bld/
[Bb]in/
[Oo]bj/
[Oo]bj */
[Ll]og/

# Visual Studio 2015 cache/options directory
Expand Down Expand Up @@ -199,6 +200,7 @@ ClientBin/
node_modules/
orleans.codegen.cs
_site/
_site */

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
Expand Down
3 changes: 3 additions & 0 deletions Docs/articles/toc.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@
### [MIDI file splitting](tools/MIDI-file-splitting.md)
## [Quantizer](tools/Quantizer.md)
## [Merger](tools/Merger.md)
### [Objects merging](tools/Objects-merging.md)
### [MIDI files merging](tools/MIDI-files-merging.md)
## [Resizer](tools/Resizer.md)
## [Repeater](tools/Repeater.md)
## [Sanitizer](tools/Sanitizer.md)
## [CSV converter](xref:Melanchall.DryWetMidi.Tools.CsvConverter)

# Composing
Expand Down
111 changes: 111 additions & 0 deletions Docs/articles/tools/MIDI-files-merging.md
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
});
```
109 changes: 4 additions & 105 deletions Docs/articles/tools/Merger.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,110 +4,9 @@ uid: a_merger

# Merger

To merge nearby objects into one DryWetMIDI provides [Merger](xref:Melanchall.DryWetMidi.Tools.Merger) class. Quick example of merging in action:
You can merge MIDI objects in different ways using extension methods from the [Merger](xref:Melanchall.DryWetMidi.Tools.Merger) class. In following subsections we'll look at some available methods:

![Objects merging](images/Merger/MergeObjects.png)
* [objects merging](Objects-merging.md);
* [MIDI files merging](MIDI-files-merging.md).

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-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-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-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.
Please note that these articles doesn't cover all possible methods and their settings. Please read API documentation on [Merger](xref:Melanchall.DryWetMidi.Tools.Merger) to get complete information.
113 changes: 113 additions & 0 deletions Docs/articles/tools/Objects-merging.md
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.
Loading

0 comments on commit de3e6d6

Please sign in to comment.