/// <summary>
 /// Converts CSV representation of a MIDI file to <see cref="MidiFile"/> reading CSV data from a file.
 /// </summary>
 /// <remarks>
 /// Note that CSV representation of a MIDI file can be different. You can specify expected CSV layout
 /// via <paramref name="settings"/> using <see cref="MidiFileCsvConversionSettings.CsvLayout"/> property.
 /// </remarks>
 /// <param name="filePath">Path of the file with CSV representation of a MIDI file.</param>
 /// <param name="settings">Settings according to which CSV data must be converted.</param>
 /// <returns>An instance of the <see cref="MidiFile"/> representing a MIDI file written in CSV format.</returns>
 /// <exception cref="ArgumentException"><paramref name="filePath"/> is a zero-length string,
 /// contains only white space, or contains one or more invalid characters as defined by
 /// <see cref="Path.InvalidPathChars"/>.</exception>
 /// <exception cref="ArgumentNullException"><paramref name="filePath"/> is null.</exception>
 /// <exception cref="PathTooLongException">The specified path, file name, or both exceed the system-defined
 /// maximum length. For example, on Windows-based platforms, paths must be less than 248 characters,
 /// and file names must be less than 260 characters.</exception>
 /// <exception cref="DirectoryNotFoundException">The specified path is invalid, (for example,
 /// it is on an unmapped drive).</exception>
 /// <exception cref="IOException">An I/O error occurred while reading the file.</exception>
 /// <exception cref="NotSupportedException"><paramref name="filePath"/> is in an invalid format.</exception>
 /// <exception cref="UnauthorizedAccessException">This operation is not supported on the current platform. -or-
 /// <paramref name="filePath"/> specified a directory. -or- The caller does not have the required permission.</exception>
 public MidiFile ConvertCsvToMidiFile(string filePath, MidiFileCsvConversionSettings settings = null)
 {
     using (var fileStream = FileUtilities.OpenFileForRead(filePath))
     {
         return(ConvertCsvToMidiFile(fileStream, settings));
     }
 }
Beispiel #2
0
        private static void WriteNote(Note note,
                                      CsvWriter csvWriter,
                                      int trackNumber,
                                      long time,
                                      MidiFileCsvConversionSettings settings,
                                      TempoMap tempoMap)
        {
            var formattedNote = settings.NoteNumberFormat == NoteNumberFormat.NoteNumber
                ? (object)note.NoteNumber
                : note;

            var formattedLength = TimeConverter.ConvertTo(note.Length, settings.NoteLengthType, tempoMap);

            WriteRecord(csvWriter,
                        trackNumber,
                        time,
                        DryWetMidiRecordTypes.Note,
                        settings,
                        tempoMap,
                        note.Channel,
                        formattedNote,
                        formattedLength,
                        note.Velocity,
                        note.OffVelocity);
        }
        private static object FormatNoteNumber(SevenBitNumber noteNumber, MidiFileCsvConversionSettings settings)
        {
            if (settings.CsvLayout == MidiFileCsvLayout.MidiCsv)
            {
                return(noteNumber);
            }

            return(NoteCsvConversionUtilities.FormatNoteNumber(noteNumber, settings.NoteNumberFormat));
        }
Beispiel #4
0
        private static Record ReadRecord(
            StreamReader streamReader,
            int lineNumber,
            MidiFileCsvConversionSettings settings)
        {
            var line = Enumerable.Range(0, int.MaxValue)
                       .Select(i => new
            {
                LineNumber = lineNumber + i,
                Line       = streamReader.ReadLine()?.Trim()
            })
                       .FirstOrDefault(l => l.Line != string.Empty);

            if (string.IsNullOrEmpty(line.Line))
            {
                return(null);
            }

            var lineNumberOffset = 0;
            var parts            = CsvUtilities.SplitCsvValues(line.Line, settings.CsvDelimiter, () =>
            {
                var nextLine = streamReader.ReadLine();
                if (nextLine != null)
                {
                    lineNumberOffset++;
                }

                return(nextLine);
            });

            if (parts.Length < 3)
            {
                ThrowBadFormat(line.LineNumber, "Missing required parameters.");
            }

            int parsedTrackNumber;
            var trackNumber = int.TryParse(parts[0], out parsedTrackNumber)
                ? (int?)parsedTrackNumber
                : null;

            ITimeSpan time = null;

            TimeSpanUtilities.TryParse(parts[1], settings.TimeType, out time);

            var recordType = parts[2];

            if (string.IsNullOrEmpty(recordType))
            {
                ThrowBadFormat(line.LineNumber, "Record type isn't specified.");
            }

            var parameters = parts.Skip(3).ToArray();

            lineNumber = line.LineNumber + lineNumberOffset;

            return(new Record(line.LineNumber, trackNumber, time, recordType, parameters));
        }
        private static HeaderChunk ParseHeader(Record record, MidiFileCsvConversionSettings settings)
        {
            var parameters = record.Parameters;

            var format       = default(MidiFileFormat?);
            var timeDivision = default(short);

            switch (settings.CsvLayout)
            {
            case MidiFileCsvLayout.DryWetMidi:
                {
                    if (parameters.Length < 2)
                    {
                        CsvError.ThrowBadFormat(record.LineNumber, "Parameters count is invalid.");
                    }

                    MidiFileFormat formatValue;
                    if (Enum.TryParse(parameters[0], true, out formatValue))
                    {
                        format = formatValue;
                    }

                    if (!short.TryParse(parameters[1], out timeDivision))
                    {
                        CsvError.ThrowBadFormat(record.LineNumber, "Invalid time division.");
                    }
                }
                break;

            case MidiFileCsvLayout.MidiCsv:
            {
                if (parameters.Length < 3)
                {
                    CsvError.ThrowBadFormat(record.LineNumber, "Parameters count is invalid.");
                }

                ushort formatValue;
                if (ushort.TryParse(parameters[0], out formatValue) && Enum.IsDefined(typeof(MidiFileFormat), formatValue))
                {
                    format = (MidiFileFormat)formatValue;
                }

                if (!short.TryParse(parameters[2], out timeDivision))
                {
                    CsvError.ThrowBadFormat(record.LineNumber, "Invalid time division.");
                }
            }
            break;
            }

            return(new HeaderChunk
            {
                FileFormat = format != null ? (ushort)format.Value : ushort.MaxValue,
                TimeDivision = TimeDivisionFactory.GetTimeDivision(timeDivision)
            });
        }
        /// <summary>
        /// Converts CSV representation of a MIDI file to <see cref="MidiFile"/> readong CSV data from a stream.
        /// </summary>
        /// <param name="stream">Stream to read MIDI file from.</param>
        /// <param name="settings">Settings according to which CSV data must be converted.</param>
        /// <returns>An instance of the <see cref="MidiFile"/> representing a MIDI file written in CSV format.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="stream"/> doesn't support reading.</exception>
        /// <exception cref="IOException">An I/O error occurred while reading from the stream.</exception>
        /// <exception cref="ObjectDisposedException"><paramref name="stream"/> is disposed.</exception>
        public MidiFile ConvertCsvToMidiFile(Stream stream, MidiFileCsvConversionSettings settings = null)
        {
            ThrowIfArgument.IsNull(nameof(stream), stream);

            if (!stream.CanRead)
            {
                throw new ArgumentException("Stream doesn't support reading.", nameof(stream));
            }

            return(CsvToMidiFileConverter.ConvertToMidiFile(stream, settings ?? new MidiFileCsvConversionSettings()));
        }
        /// <summary>
        /// Converts the specified <see cref="MidiFile"/> to CSV represenattion and writes it to a stream.
        /// </summary>
        /// <remarks>
        /// Note that <see cref="MidiFile"/> can be converted to different CSV representations. You can specify desired
        /// CSV layout via <paramref name="settings"/> using <see cref="MidiFileCsvConversionSettings.CsvLayout"/> property.
        /// </remarks>
        /// <param name="midiFile"><see cref="MidiFile"/> to convert to CSV.</param>
        /// <param name="stream">Stream to write CSV representation to.</param>
        /// <param name="settings">Settings according to which <paramref name="midiFile"/> must be converted.</param>
        /// <exception cref="ArgumentNullException"><paramref name="midiFile"/> is null. -or-
        /// <paramref name="stream"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="stream"/> doesn't support writing.</exception>
        /// <exception cref="IOException">An I/O error occurred while writing to the stream.</exception>
        /// <exception cref="ObjectDisposedException"><paramref name="stream"/> is disposed.</exception>
        public void ConvertMidiFileToCsv(MidiFile midiFile, Stream stream, MidiFileCsvConversionSettings settings = null)
        {
            ThrowIfArgument.IsNull(nameof(midiFile), midiFile);
            ThrowIfArgument.IsNull(nameof(stream), stream);

            if (!stream.CanWrite)
            {
                throw new ArgumentException("Stream doesn't support writing.", nameof(stream));
            }

            MidiFileToCsvConverter.ConvertToCsv(midiFile, stream, settings ?? new MidiFileCsvConversionSettings());
        }
        private static TimedMidiEvent[] ParseNote(Record record, MidiFileCsvConversionSettings settings)
        {
            if (record.TrackNumber == null)
            {
                CsvError.ThrowBadFormat(record.LineNumber, "Invalid track number.");
            }

            if (record.Time == null)
            {
                CsvError.ThrowBadFormat(record.LineNumber, "Invalid time.");
            }

            var parameters = record.Parameters;

            if (parameters.Length < 5)
            {
                CsvError.ThrowBadFormat(record.LineNumber, "Invalid number of parameters provided.");
            }

            var i = -1;

            try
            {
                var channel    = (FourBitNumber)TypeParser.FourBitNumber(parameters[++i], settings);
                var noteNumber = (SevenBitNumber)TypeParser.NoteNumber(parameters[++i], settings);

                ITimeSpan length = null;
                TimeSpanUtilities.TryParse(parameters[++i], settings.NoteLengthType, out length);

                var velocity    = (SevenBitNumber)TypeParser.SevenBitNumber(parameters[++i], settings);
                var offVelocity = (SevenBitNumber)TypeParser.SevenBitNumber(parameters[++i], settings);

                return(new[]
                {
                    new TimedMidiEvent(record.Time, new NoteOnEvent(noteNumber, velocity)
                    {
                        Channel = channel
                    }),
                    new TimedMidiEvent(record.Time.Add(length, TimeSpanMode.TimeLength), new NoteOffEvent(noteNumber, offVelocity)
                    {
                        Channel = channel
                    }),
                });
            }
            catch
            {
                CsvError.ThrowBadFormat(record.LineNumber, $"Parameter ({i}) is invalid.");
            }

            return(null);
        }
Beispiel #9
0
        private static void WriteFileEnd(CsvWriter csvWriter,
                                         MidiFileCsvConversionSettings settings,
                                         TempoMap tempoMap)
        {
            switch (settings.CsvLayout)
            {
            case MidiFileCsvLayout.DryWetMidi:
                return;

            case MidiFileCsvLayout.MidiCsv:
                WriteRecord(csvWriter, 0, 0, MidiCsvRecordTypes.File.FileEnd, settings, tempoMap);
                break;
            }
        }
Beispiel #10
0
        private static void WriteTrackChunkStart(CsvWriter csvWriter,
                                                 int trackNumber,
                                                 MidiFileCsvConversionSettings settings,
                                                 TempoMap tempoMap)
        {
            switch (settings.CsvLayout)
            {
            case MidiFileCsvLayout.DryWetMidi:
                break;

            case MidiFileCsvLayout.MidiCsv:
                WriteRecord(csvWriter, trackNumber, 0, MidiCsvRecordTypes.File.TrackChunkStart, settings, tempoMap);
                break;
            }
        }
Beispiel #11
0
        private static void WriteRecord(CsvWriter csvWriter,
                                        int?trackNumber,
                                        long?time,
                                        string type,
                                        MidiFileCsvConversionSettings settings,
                                        TempoMap tempoMap,
                                        params object[] parameters)
        {
            var convertedTime = time == null
                ? null
                : TimeConverter.ConvertTo(time.Value, settings.TimeType, tempoMap);

            var processedParameters = parameters.SelectMany(ProcessParameter);

            csvWriter.WriteRecord(new object[] { trackNumber, convertedTime, type }.Concat(processedParameters));
        }
        private static void WriteTrackChunkEnd(
            StreamWriter streamWriter,
            int trackNumber,
            long time,
            MidiFileCsvConversionSettings settings,
            TempoMap tempoMap)
        {
            switch (settings.CsvLayout)
            {
            case MidiFileCsvLayout.DryWetMidi:
                return;

            case MidiFileCsvLayout.MidiCsv:
                WriteRecord(streamWriter, trackNumber, time, MidiCsvRecordTypes.File.TrackChunkEnd, settings, tempoMap);
                break;
            }
        }
Beispiel #13
0
        public static void ConvertToCsv(MidiFile midiFile, Stream stream, MidiFileCsvConversionSettings settings)
        {
            using (var csvWriter = new CsvWriter(stream, settings.CsvSettings))
            {
                var trackNumber = 0;
                var tempoMap    = midiFile.GetTempoMap();

                WriteHeader(csvWriter, midiFile, settings, tempoMap);

                foreach (var trackChunk in midiFile.GetTrackChunks())
                {
                    WriteTrackChunkStart(csvWriter, trackNumber, settings, tempoMap);

                    var time         = 0L;
                    var timedEvents  = trackChunk.GetTimedEvents();
                    var timedObjects = settings.CsvLayout == MidiFileCsvLayout.MidiCsv || settings.NoteFormat == NoteFormat.Events
                        ? timedEvents
                        : timedEvents.GetTimedEventsAndNotes();

                    foreach (var timedObject in timedObjects)
                    {
                        time = timedObject.Time;

                        var timedEvent = timedObject as TimedEvent;
                        if (timedEvent != null)
                        {
                            WriteTimedEvent(timedEvent, csvWriter, trackNumber, time, settings, tempoMap);
                        }
                        else
                        {
                            var note = timedObject as Note;
                            if (note != null)
                            {
                                WriteNote(note, csvWriter, trackNumber, time, settings, tempoMap);
                            }
                        }
                    }

                    WriteTrackChunkEnd(csvWriter, trackNumber, time, settings, tempoMap);

                    trackNumber++;
                }

                WriteFileEnd(csvWriter, settings, tempoMap);
            }
        }
        public static void ConvertToCsv(MidiFile midiFile, Stream stream, MidiFileCsvConversionSettings settings)
        {
            using (var streamWriter = new StreamWriter(stream))
            {
                var trackNumber = 0;
                var tempoMap    = midiFile.GetTempoMap();

                WriteHeader(streamWriter, midiFile, settings, tempoMap);

                foreach (var trackChunk in midiFile.GetTrackChunks())
                {
                    WriteTrackChunkStart(streamWriter, trackNumber, settings, tempoMap);

                    var time = 0L;

                    foreach (var timedEvent in trackChunk.GetTimedEvents())
                    {
                        time = timedEvent.Time;
                        var midiEvent = timedEvent.Event;
                        var eventType = midiEvent.GetType();

                        var eventNameGetter = EventNameGetterProvider.Get(eventType, settings.CsvLayout);
                        var recordType      = eventNameGetter(midiEvent);

                        var eventParametersGetter = EventParametersGetterProvider.Get(eventType);
                        var recordParameters      = eventParametersGetter(midiEvent, settings);

                        WriteRecord(streamWriter,
                                    trackNumber,
                                    time,
                                    recordType,
                                    settings,
                                    tempoMap,
                                    recordParameters);
                    }

                    WriteTrackChunkEnd(streamWriter, trackNumber, time, settings, tempoMap);

                    trackNumber++;
                }

                WriteFileEnd(streamWriter, settings, tempoMap);
            }
        }
        private static void WriteRecord(
            StreamWriter streamWriter,
            int?trackNumber,
            long?time,
            string type,
            MidiFileCsvConversionSettings settings,
            TempoMap tempoMap,
            params object[] parameters)
        {
            var convertedTime = time == null
                ? null
                : TimeConverter.ConvertTo(time.Value, settings.TimeType, tempoMap);

            var processedParameters = parameters.SelectMany(ProcessParameter);

            streamWriter.WriteLine(CsvUtilities.MergeCsvValues(
                                       settings.CsvDelimiter,
                                       new object[] { trackNumber, convertedTime, type }.Concat(processedParameters)));
        }
        private static void WriteHeader(
            StreamWriter streamWriter,
            MidiFile midiFile,
            MidiFileCsvConversionSettings settings,
            TempoMap tempoMap)
        {
            MidiFileFormat?format = null;

            try
            {
                format = midiFile.OriginalFormat;
            }
            catch { }

            var trackChunksCount = midiFile.GetTrackChunks().Count();

            switch (settings.CsvLayout)
            {
            case MidiFileCsvLayout.DryWetMidi:
                WriteRecord(streamWriter,
                            null,
                            null,
                            DryWetMidiRecordTypes.File.Header,
                            settings,
                            tempoMap,
                            format,
                            midiFile.TimeDivision.ToInt16());
                break;

            case MidiFileCsvLayout.MidiCsv:
                WriteRecord(streamWriter,
                            0,
                            0,
                            MidiCsvRecordTypes.File.Header,
                            settings,
                            tempoMap,
                            format != null ? (ushort)format.Value : (trackChunksCount > 1 ? 1 : 0),
                            trackChunksCount,
                            midiFile.TimeDivision.ToInt16());
                break;
            }
        }
        private static RecordType?GetRecordType(string recordType, MidiFileCsvConversionSettings settings)
        {
            var csvLayout = settings.CsvLayout;

            var recordTypes = csvLayout == MidiFileCsvLayout.DryWetMidi
                ? RecordTypes_DryWetMidi
                : RecordTypes_MidiCsv;
            var eventsNames = EventsNamesProvider.Get(csvLayout);

            RecordType result;

            if (recordTypes.TryGetValue(recordType, out result))
            {
                return(result);
            }

            if (eventsNames.Contains(recordType, StringComparer.OrdinalIgnoreCase))
            {
                return(RecordType.Event);
            }

            return(null);
        }
Beispiel #18
0
        private static void WriteTimedEvent(TimedEvent timedEvent,
                                            CsvWriter csvWriter,
                                            int trackNumber,
                                            long time,
                                            MidiFileCsvConversionSettings settings,
                                            TempoMap tempoMap)
        {
            var midiEvent = timedEvent.Event;
            var eventType = midiEvent.GetType();

            var eventNameGetter = EventNameGetterProvider.Get(eventType, settings.CsvLayout);
            var recordType      = eventNameGetter(midiEvent);

            var eventParametersGetter = EventParametersGetterProvider.Get(eventType);
            var recordParameters      = eventParametersGetter(midiEvent, settings);

            WriteRecord(csvWriter,
                        trackNumber,
                        time,
                        recordType,
                        settings,
                        tempoMap,
                        recordParameters);
        }
        private static MidiEvent ParseEvent(Record record, MidiFileCsvConversionSettings settings)
        {
            if (record.TrackNumber == null)
            {
                CsvError.ThrowBadFormat(record.LineNumber, "Invalid track number.");
            }

            if (record.Time == null)
            {
                CsvError.ThrowBadFormat(record.LineNumber, "Invalid time.");
            }

            var eventParser = EventParserProvider.Get(record.RecordType, settings.CsvLayout);

            try
            {
                return(eventParser(record.Parameters, settings));
            }
            catch (FormatException ex)
            {
                CsvError.ThrowBadFormat(record.LineNumber, "Invalid format of event record.", ex);
                return(null);
            }
        }
        private static Record ReadRecord(CsvReader csvReader, int lineNumber, MidiFileCsvConversionSettings settings)
        {
            var record = csvReader.ReadRecord();

            if (record == null)
            {
                return(null);
            }

            var values = record.Values;

            if (values.Length < 3)
            {
                CsvError.ThrowBadFormat(record.LineNumber, "Missing required parameters.");
            }

            int parsedTrackNumber;
            var trackNumber = int.TryParse(values[0], out parsedTrackNumber)
                ? (int?)parsedTrackNumber
                : null;

            ITimeSpan time = null;

            TimeSpanUtilities.TryParse(values[1], settings.TimeType, out time);

            var recordType = values[2];

            if (string.IsNullOrEmpty(recordType))
            {
                CsvError.ThrowBadFormat(record.LineNumber, "Record type isn't specified.");
            }

            var parameters = values.Skip(3).ToArray();

            return(new Record(record.LineNumber, trackNumber, time, recordType, parameters));
        }
Beispiel #21
0
        /// <summary>
        /// Converts the specified <see cref="MidiFile"/> to CSV represenattion and writes it to a file.
        /// </summary>
        /// <remarks>
        /// Note that <see cref="MidiFile"/> can be converted to different CSV representations. You can specify desired
        /// CSV layout via <paramref name="settings"/> using <see cref="MidiFileCsvConversionSettings.CsvLayout"/> property.
        /// </remarks>
        /// <param name="midiFile"><see cref="MidiFile"/> to convert to CSV.</param>
        /// <param name="filePath">Path of the output CSV file.</param>
        /// <param name="overwriteFile">If true and file specified by <paramref name="filePath"/> already
        /// exists it will be overwritten; if false and the file exists exception will be thrown.</param>
        /// <param name="settings">Settings according to which <paramref name="midiFile"/> must be converted.</param>
        /// <exception cref="ArgumentNullException"><paramref name="midiFile"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="filePath"/> is a zero-length string,
        /// contains only white space, or contains one or more invalid characters as defined by
        /// <see cref="Path.InvalidPathChars"/>.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="filePath"/> is null.</exception>
        /// <exception cref="PathTooLongException">The specified path, file name, or both exceed the system-defined
        /// maximum length. For example, on Windows-based platforms, paths must be less than 248 characters,
        /// and file names must be less than 260 characters.</exception>
        /// <exception cref="DirectoryNotFoundException">The specified path is invalid, (for example,
        /// it is on an unmapped drive).</exception>
        /// <exception cref="IOException">An I/O error occurred while writing the file.</exception>
        /// <exception cref="NotSupportedException"><paramref name="filePath"/> is in an invalid format.</exception>
        /// <exception cref="UnauthorizedAccessException">This operation is not supported on the current platform.-or-
        /// <paramref name="filePath"/> specified a directory.-or- The caller does not have the required permission.</exception>
        public void ConvertMidiFileToCsv(MidiFile midiFile, string filePath, bool overwriteFile = false, MidiFileCsvConversionSettings settings = null)
        {
            ThrowIfArgument.IsNull(nameof(midiFile), midiFile);

            using (var fileStream = FileUtilities.OpenFileForWrite(filePath, overwriteFile))
            {
                ConvertMidiFileToCsv(midiFile, fileStream, settings);
            }
        }
        public static MidiFile ConvertToMidiFile(Stream stream, MidiFileCsvConversionSettings settings)
        {
            var midiFile = new MidiFile();
            var events   = new Dictionary <int, List <TimedMidiEvent> >();

            using (var csvReader = new CsvReader(stream, settings.CsvDelimiter))
            {
                var    lineNumber = 0;
                Record record;

                while ((record = ReadRecord(csvReader, lineNumber, settings)) != null)
                {
                    var recordType = GetRecordType(record.RecordType, settings);
                    if (recordType == null)
                    {
                        CsvError.ThrowBadFormat(lineNumber, "Unknown record.");
                    }

                    switch (recordType)
                    {
                    case RecordType.Header:
                    {
                        var headerChunk = ParseHeader(record, settings);
                        midiFile.TimeDivision   = headerChunk.TimeDivision;
                        midiFile.OriginalFormat = (MidiFileFormat)headerChunk.FileFormat;
                    }
                    break;

                    case RecordType.TrackChunkStart:
                    case RecordType.TrackChunkEnd:
                    case RecordType.FileEnd:
                        break;

                    case RecordType.Event:
                    {
                        var midiEvent        = ParseEvent(record, settings);
                        var trackChunkNumber = record.TrackNumber.Value;

                        AddTimedEvents(events, trackChunkNumber, new TimedMidiEvent(record.Time, midiEvent));
                    }
                    break;

                    case RecordType.Note:
                    {
                        var noteEvents       = ParseNote(record, settings);
                        var trackChunkNumber = record.TrackNumber.Value;

                        AddTimedEvents(events, trackChunkNumber, noteEvents);
                    }
                    break;
                    }

                    lineNumber = record.LineNumber + 1;
                }
            }

            if (!events.Keys.Any())
            {
                return(midiFile);
            }

            var tempoMap = GetTempoMap(events.Values.SelectMany(e => e), midiFile.TimeDivision);

            var trackChunks = new TrackChunk[events.Keys.Max() + 1];

            for (int i = 0; i < trackChunks.Length; i++)
            {
                List <TimedMidiEvent> timedMidiEvents;
                trackChunks[i] = events.TryGetValue(i, out timedMidiEvents)
                    ? timedMidiEvents.Select(e => new TimedEvent(e.Event, TimeConverter.ConvertFrom(e.Time, tempoMap))).ToTrackChunk()
                    : new TrackChunk();
            }

            midiFile.Chunks.AddRange(trackChunks);

            return(midiFile);
        }