Processing objects
Important
Please read the Getting objects article before reading this one.
This article describes ways to process different objects (like timed events or notes) within MIDI files and track chunks. Processing means changing properties of the objects, including time or length.
ProcessTimedEvents
Let's dive into the ProcessTimedEvents methods immediately. Well, we have such a file:
var midiFile = new MidiFile(
new TrackChunk(
new TextEvent("A"),
new NoteOnEvent(),
new NoteOffEvent()),
new TrackChunk(
new TextEvent("B")));
And now we want to change the text of all Text events to "C"
:
midiFile.ProcessTimedEvents(
e => ((TextEvent)e.Event).Text = "C",
e => e.Event.EventType == MidiEventType.Text);
After this instruction executed, the file will be equal to this one:
var midiFile = new MidiFile(
new TrackChunk(
new TextEvent("C"),
new NoteOnEvent(),
new NoteOffEvent()),
new TrackChunk(
new TextEvent("C")));
So the first argument (e => ((TextEvent)e.Event).Text = "C"
) is what we want to do and the second one (e => e.Event.EventType == MidiEventType.Text
) defines what objects to process. We can also use the overload without predicate to process all timed events:
midiFile.ProcessTimedEvents(e => e.Time += 10);
So the first event in each track chunk will have DeltaTime of 10
now so the absolute time of each event increases by 10
:
var midiFile = new MidiFile(
new TrackChunk(
new TextEvent("C") { DeltaTime = 10 },
new NoteOnEvent(),
new NoteOffEvent()),
new TrackChunk(
new TextEvent("C") { DeltaTime = 10 }));
Please examine the TimedEventsManagingUtilities class to see other ProcessTimedEvents
overloads.
By the way, what if we want to process only events with even indices? First of all we need to define subclass of the TimedEvent:
private sealed class CustomTimedEvent : TimedEvent
{
public CustomTimedEvent(MidiEvent midiEvent, long time, int eventIndex)
: base(midiEvent, time)
{
EventIndex = eventIndex;
}
public int EventIndex { get; }
}
And now we are ready to increase the time of even-indexed events by 10
ticks:
midiFile.ProcessTimedEvents(
e => e.Time += 10,
e => ((CustomTimedEvent)e).EventIndex % 2 == 0,
new TimedEventDetectionSettings
{
Constructor = data => new CustomTimedEvent(data.Event, data.Time, data.EventIndex)
});
New file will be:
var midiFile = new MidiFile(
new TrackChunk(
new NoteOnEvent(),
new TextEvent("C") { DeltaTime = 10 },
new NoteOffEvent()),
new TrackChunk(
new TextEvent("C") { DeltaTime = 10 }));
Looks right. In the first track chunk Text and Note Off events are even-indexed ones (0
and 2
), so the time of each one will be 10
. Note On event will be left untouched so its time remains 0
. As for the second track chunk, the only event is even-indexed of course and thus is processed.
Optionally you can pass TimedEventProcessingHint to set a hint for the processing engine which can improve performance. Please read about available options by the specified link. For example, if you know that time of events won't be changed, you can set TimedEventProcessingHint.None option to eliminate events resorting (which is required if times can be changed) and to reduce the time needed to perform the processing:
midiFile.ProcessTimedEvents(
e => ((TextEvent)e.Event).Text = "C",
e => e.Event.EventType == MidiEventType.Text,
hint: TimedEventProcessingHint.None);
All ProcessTimedEvents methods return count of processed objects.
ProcessNotes
Use ProcessNotes methods to process notes. There is nothing tricky here and the approach is the same as for timed events processing (see above): you just work with notes instead of timed events.
Please read GetNotes: Settings article to learn more about how to customize notes detection and building. Be aware that note is in fact a pair of Note On / Note Off events. So to have full control on the notes construction process, ProcessNotes methods accept TimedEventDetectionSettings along with NoteDetectionSettings.
As with timed events you can create subclass of the Note class and use it to customize the matching logic:
private sealed class CustomNote : Note
{
public CustomNote(TimedEvent timedNoteOnEvent, TimedEvent timedNoteOffEvent, bool isNoteOnEventUnevenIndexed)
: base(timedNoteOnEvent, timedNoteOffEvent)
{
IsNoteOnEventUnevenIndexed = isNoteOnEventUnevenIndexed;
}
public bool IsNoteOnEventUnevenIndexed { get; }
}
// ...
midiFile.ProcessNotes(
n => n.Velocity += (SevenBitNumber)10,
n => ((CustomNote)n).IsNoteOnEventUnevenIndexed,
new NoteDetectionSettings
{
Constructor = data => new CustomNote(
data.TimedNoteOnEvent,
data.TimedNoteOffEvent,
((CustomTimedEvent)data.TimedNoteOnEvent).EventIndex % 2 != 0)
},
new TimedEventDetectionSettings
{
Constructor = data => new CustomTimedEvent(
data.Event,
data.Time,
data.EventIndex)
});
So the velocity of a note will be increased by 10
if the note's Note On event is uneven-indexed.
And of course you can manage performance with notes processing too via NoteProcessingHint. By default it's set to the value allowing you to change time and length. But you can reduce execution time via the NoteProcessingHint.None flag. With this option any changes of time or length of a note won't be saved.
ProcessChords
Obviously you can process chords too – ProcessChords is what you need. It's redundant to describe how to use these methods since usage is the same as for timed events and notes.
A chord is in fact a collection of notes, each one is a pair of Note On / Note Off events (please read about notes processing above). So to have full control on the chord construction process, ProcessChords methods accept TimedEventDetectionSettings and NoteDetectionSettings along with ChordDetectionSettings.
As with timed events and notes you can create a subclass of the Chord class and use it to customize the matching logic.
But for chords processing there are two additional options to control performance of the methods. These flags from ChordProcessingHint are:
- NoteTimeOrLengthCanBeChanged: time or length of a note within a chord's notes can be changed;
- NotesCollectionCanBeChanged: chord's notes collection can be changed, for example, a note can be added or removed.
By default those two options are not enabled. So if you want to say the engine that all chord's properties can be changed:
midiFile.ProcessChords(
c => c.Velocity += (SevenBitNumber)10,
hint: ChordProcessingHint.AllPropertiesCanBeChanged);
or if you want more precise control:
midiFile.ProcessChords(
c => c.Velocity += (SevenBitNumber)10,
hint: ChordProcessingHint.TimeOrLengthCanBeChanged | ChordProcessingHint.NoteTimeOrLengthCanBeChanged);
ProcessObjects
All methods we saw before process objects of the same type. So you can process only either notes or chords or timed events. But there is a way to process objects of different types at the same time – ProcessObjects. For example, if you want to increase velocity of notes that are not a part of some chord:
midiFile.ProcessObjects(
ObjectType.Note | ObjectType.Chord,
obj => ((Note)obj).Velocity += (SevenBitNumber)10,
obj => obj is Note);
Or we can increase velocity based on the type of an object:
midiFile.ProcessObjects(
ObjectType.Note | ObjectType.Chord,
obj =>
{
if (obj is Note note)
note.Velocity += (SevenBitNumber)10;
else
((Chord)obj).Velocity += (SevenBitNumber)20;
});
Performance of the ProcessObjects methods can be managed too – via passing ObjectProcessingHint. Possible values and how they affect the processing is the subject already described in previous sections.
If you need to process objects of several types simultaneously, ProcessObjects will be much faster than consecutive calls of methods for specific object types. I.e. this instruction
midiFile.ProcessObjects(
ObjectType.Note | ObjectType.Chord,
/*...*/);
will execute faster than
midiFile.ProcessChords(/*...*/);
midiFile.ProcessNotes(/*...*/);
More than that, some cases can be covered with ProcessObjects only like the example described above – increase velocity of notes that are not a part of some chord. Of course you can always use objects managers to perform any transformations you want, but processing objects in that way is much slower.