using System; using System.Collections.Generic; using NAudio.Utils; namespace NAudio.Midi { /// /// A helper class to manage collection of MIDI events /// It has the ability to organise them in tracks /// public class MidiEventCollection : IEnumerable> { private int midiFileType; private readonly List> trackEvents; /// /// Creates a new Midi Event collection /// /// Initial file type /// Delta Ticks Per Quarter Note public MidiEventCollection(int midiFileType, int deltaTicksPerQuarterNote) { this.midiFileType = midiFileType; DeltaTicksPerQuarterNote = deltaTicksPerQuarterNote; StartAbsoluteTime = 0; trackEvents = new List>(); } /// /// The number of tracks /// public int Tracks => trackEvents.Count; /// /// The absolute time that should be considered as time zero /// Not directly used here, but useful for timeshifting applications /// public long StartAbsoluteTime { get; set; } /// /// The number of ticks per quarter note /// public int DeltaTicksPerQuarterNote { get; } /// /// Gets events on a specified track /// /// Track number /// The list of events public IList GetTrackEvents(int trackNumber) { return trackEvents[trackNumber]; } /// /// Gets events on a specific track /// /// Track number /// The list of events public IList this[int trackNumber] => trackEvents[trackNumber]; /// /// Adds a new track /// /// The new track event list public IList AddTrack() { return AddTrack(null); } /// /// Adds a new track /// /// Initial events to add to the new track /// The new track event list public IList AddTrack(IList initialEvents) { List events = new List(); if (initialEvents != null) { events.AddRange(initialEvents); } trackEvents.Add(events); return events; } /// /// Removes a track /// /// Track number to remove public void RemoveTrack(int track) { trackEvents.RemoveAt(track); } /// /// Clears all events /// public void Clear() { trackEvents.Clear(); } /// /// The MIDI file type /// public int MidiFileType { get => midiFileType; set { if (midiFileType != value) { // set MIDI file type before calling flatten or explode functions midiFileType = value; if (value == 0) { FlattenToOneTrack(); } else { ExplodeToManyTracks(); } } } } /// /// Adds an event to the appropriate track depending on file type /// /// The event to be added /// The original (or desired) track number /// When adding events in type 0 mode, the originalTrack parameter /// is ignored. If in type 1 mode, it will use the original track number to /// store the new events. If the original track was 0 and this is a channel based /// event, it will create new tracks if necessary and put it on the track corresponding /// to its channel number public void AddEvent(MidiEvent midiEvent, int originalTrack) { if (midiFileType == 0) { EnsureTracks(1); trackEvents[0].Add(midiEvent); } else { if(originalTrack == 0) { // if its a channel based event, lets move it off to // a channel track of its own switch (midiEvent.CommandCode) { case MidiCommandCode.NoteOff: case MidiCommandCode.NoteOn: case MidiCommandCode.KeyAfterTouch: case MidiCommandCode.ControlChange: case MidiCommandCode.PatchChange: case MidiCommandCode.ChannelAfterTouch: case MidiCommandCode.PitchWheelChange: EnsureTracks(midiEvent.Channel + 1); trackEvents[midiEvent.Channel].Add(midiEvent); break; default: EnsureTracks(1); trackEvents[0].Add(midiEvent); break; } } else { // put it on the track it was originally on EnsureTracks(originalTrack + 1); trackEvents[originalTrack].Add(midiEvent); } } } private void EnsureTracks(int count) { for (int n = trackEvents.Count; n < count; n++) { trackEvents.Add(new List()); } } private void ExplodeToManyTracks() { IList originalList = trackEvents[0]; Clear(); foreach (MidiEvent midiEvent in originalList) { AddEvent(midiEvent, 0); } PrepareForExport(); } private void FlattenToOneTrack() { bool eventsAdded = false; for (int track = 1; track < trackEvents.Count; track++) { foreach (MidiEvent midiEvent in trackEvents[track]) { if (!MidiEvent.IsEndTrack(midiEvent)) { trackEvents[0].Add(midiEvent); eventsAdded = true; } } } for (int track = trackEvents.Count - 1; track > 0; track--) { RemoveTrack(track); } if (eventsAdded) { PrepareForExport(); } } /// /// Sorts, removes empty tracks and adds end track markers /// public void PrepareForExport() { var comparer = new MidiEventComparer(); // 1. sort each track foreach (var list in trackEvents) { MergeSort.Sort(list, comparer); // 2. remove all End track events except one at the very end int index = 0; while (index < list.Count - 1) { if(MidiEvent.IsEndTrack(list[index])) { list.RemoveAt(index); } else { index++; } } } int track = 0; // 3. remove empty tracks and add missing while (track < trackEvents.Count) { var list = trackEvents[track]; if (list.Count == 0) { RemoveTrack(track); } else { if(list.Count == 1 && MidiEvent.IsEndTrack(list[0])) { RemoveTrack(track); } else { if(!MidiEvent.IsEndTrack(list[list.Count-1])) { list.Add(new MetaEvent(MetaEventType.EndTrack, 0, list[list.Count - 1].AbsoluteTime)); } track++; } } } } /// /// Gets an enumerator for the lists of track events /// public IEnumerator> GetEnumerator() { return trackEvents.GetEnumerator(); } /// /// Gets an enumerator for the lists of track events /// System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return trackEvents.GetEnumerator(); } } }