diff --git a/DryWetMidi.Tests.Common/FileOperations.cs b/DryWetMidi.Tests.Common/FileOperations.cs index c0269672e..2c7cbae29 100644 --- a/DryWetMidi.Tests.Common/FileOperations.cs +++ b/DryWetMidi.Tests.Common/FileOperations.cs @@ -27,6 +27,6 @@ public static string[] ReadAllFileLines(string filePath) => File.ReadAllLines(filePath); public static string GetTempFilePath() => - Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Path.Combine(Path.GetTempPath(), $"dwmtest_{Path.GetRandomFileName()}"); } } diff --git a/DryWetMidi.Tests/Tools/CsvConverter/CsvConverterTests.cs b/DryWetMidi.Tests/Tools/CsvConverter/CsvConverterTests.cs deleted file mode 100644 index f6dc5bd64..000000000 --- a/DryWetMidi.Tests/Tools/CsvConverter/CsvConverterTests.cs +++ /dev/null @@ -1,906 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Core; -using Melanchall.DryWetMidi.Interaction; -using Melanchall.DryWetMidi.Tests.Common; -using Melanchall.DryWetMidi.Tests.Utilities; -using Melanchall.DryWetMidi.Tools; -using NUnit.Framework; - -namespace Melanchall.DryWetMidi.Tests.Tools -{ - [TestFixture] - public sealed class CsvConverterTests - { - #region Nested classes - - private sealed class NoteWithCustomTimeAndLength - { - #region Constructor - - public NoteWithCustomTimeAndLength(byte noteNumber, - byte channel, - byte velocity, - byte offVelocity, - ITimeSpan time, - ITimeSpan length) - { - NoteNumber = (SevenBitNumber)noteNumber; - Channel = (FourBitNumber)channel; - Velocity = (SevenBitNumber)velocity; - OffVelocity = (SevenBitNumber)offVelocity; - Time = time; - Length = length; - } - - #endregion - - #region Properties - - public SevenBitNumber NoteNumber { get; } - - public FourBitNumber Channel { get; } - - public SevenBitNumber Velocity { get; } - - public SevenBitNumber OffVelocity { get; } - - public ITimeSpan Time { get; } - - public ITimeSpan Length { get; } - - #endregion - - #region Methods - - public Note GetNote(TempoMap tempoMap) - { - return new Note(NoteNumber) - { - Channel = Channel, - Velocity = Velocity, - OffVelocity = OffVelocity, - Time = TimeConverter.ConvertFrom(Time, tempoMap), - Length = LengthConverter.ConvertFrom(Length, Time, tempoMap) - }; - } - - #endregion - } - - #endregion - - #region Constants - - private static readonly NoteMethods _noteMethods = new NoteMethods(); - - #endregion - - #region Test methods - - #region Convert MIDI files to/from CSV - - [Test] - public void ConvertMidiFileToFromCsv() - { - var settings = new MidiFileCsvConversionSettings(); - - ConvertMidiFileToFromCsv(settings); - } - - #endregion - - #region CsvToMidiFile - - [Test] - public void ConvertCsvToMidiFile_StreamIsNotDisposed() - { - var settings = new MidiFileCsvConversionSettings(); - - var csvConverter = new CsvConverter(); - - using (var streamToWrite = new MemoryStream()) - { - csvConverter.ConvertMidiFileToCsv(new MidiFile(), streamToWrite, settings); - - using (var streamToRead = new MemoryStream()) - { - var midiFile = csvConverter.ConvertCsvToMidiFile(streamToRead, settings); - Assert.DoesNotThrow(() => { var l = streamToRead.Length; }); - } - } - } - - [TestCase((object)new[] { ",,Header,MultiTrack,1000" })] - public void ConvertCsvToMidiFile_NoEvents(string[] csvLines) - { - var midiFile = ConvertCsvToMidiFile(TimeSpanType.Midi, csvLines); - - Assert.AreEqual(MidiFileFormat.MultiTrack, midiFile.OriginalFormat, "File format is invalid."); - Assert.AreEqual(new TicksPerQuarterNoteTimeDivision(1000), midiFile.TimeDivision, "Time division is invalid."); - } - - [TestCase((object)new[] { "0,0,Set Tempo,100000" })] - public void ConvertCsvToMidiFile_NoHeader(string[] csvLines) - { - var midiFile = ConvertCsvToMidiFile(TimeSpanType.Midi, csvLines); - - Assert.AreEqual(new TicksPerQuarterNoteTimeDivision(TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote), - midiFile.TimeDivision, - "Time division is invalid."); - Assert.Throws(() => { var format = midiFile.OriginalFormat; }); - } - - [TestCase(true, new[] - { - "0, 0, Note On, 10, 50, 120", - "0, 0, Text, \"Test\"", - "0, 100, Note On, 7, 50, 110", - "0, 250, Note Off, 10, 50, 70", - "0, 1000, Note Off, 7, 50, 80" - })] - [TestCase(false, new[] - { - "0, 0, Note On, 10, 50, 120", - "0, 0, Text, \"Test\"", - "0, 100, Note On, 7, 50, 110", - "0, 250, Note Off, 10, 50, 70", - "0, 1000, Note Off, 7, 50, 80" - })] - public void ConvertCsvToMidiFile_SingleTrackChunk(bool orderEvents, string[] csvLines) - { - if (!orderEvents) - { - var tmp = csvLines[2]; - csvLines[2] = csvLines[4]; - csvLines[4] = tmp; - } - - var midiFile = ConvertCsvToMidiFile(TimeSpanType.Midi, csvLines); - - var expectedEvents = new[] - { - new TimedEvent(new NoteOnEvent((SevenBitNumber)50, (SevenBitNumber)120) { Channel = (FourBitNumber)10 }, 0), - new TimedEvent(new TextEvent("Test"), 0), - new TimedEvent(new NoteOnEvent((SevenBitNumber)50, (SevenBitNumber)110) { Channel = (FourBitNumber)7 }, 100), - new TimedEvent(new NoteOffEvent((SevenBitNumber)50, (SevenBitNumber)70) { Channel = (FourBitNumber)10 }, 250), - new TimedEvent(new NoteOffEvent((SevenBitNumber)50, (SevenBitNumber)80) { Channel = (FourBitNumber)7 }, 1000) - }; - - Assert.AreEqual(1, midiFile.GetTrackChunks().Count(), "Track chunks count is invalid."); - MidiAsserts.AreEqual(expectedEvents, midiFile.GetTimedEvents(), false, 0, "Invalid events."); - } - - [TestCase(true, new[] - { - ", , header, singletrack, 500", - "0, 0:0:0, note on, 10, 50, 120", - "0, 0:0:0, text, \"Test\"", - "0, 0:1:0, note on, 7, 50, 110", - "", - "0, 0:1:3, set tempo, 300000", - "0, 0:1:10, note off, 10, 50, 70", - "", - "", - "0, 0:10:3, note off, 7, 50, 80" - })] - [TestCase(false, new[] - { - ", , header, singletrack, 500", - "0, 0:0:0, note on, 10, 50, 120", - "0, 0:0:0, text, \"Test\"", - "0, 0:1:0, note on, 7, 50, 110", - "", - "0, 0:1:3, set tempo, 300000", - "0, 0:1:10, note off, 10, 50, 70", - "", - "", - "0, 0:10:3, note off, 7, 50, 80" - })] - public void ConvertCsvToMidiFile_SingleTrackChunk_MetricTimes(bool orderEvents, string[] csvLines) - { - if (!orderEvents) - { - var tmp = csvLines[2]; - csvLines[2] = csvLines[5]; - csvLines[5] = tmp; - } - - var midiFile = ConvertCsvToMidiFile(TimeSpanType.Metric, csvLines); - - TempoMap expectedTempoMap; - using (var tempoMapManager = new TempoMapManager(new TicksPerQuarterNoteTimeDivision(500))) - { - tempoMapManager.SetTempo(new MetricTimeSpan(0, 1, 3), new Tempo(300000)); - expectedTempoMap = tempoMapManager.TempoMap; - } - - var expectedEvents = new[] - { - new TimeAndMidiEvent(new MetricTimeSpan(), - new NoteOnEvent((SevenBitNumber)50, (SevenBitNumber)120) { Channel = (FourBitNumber)10 }), - new TimeAndMidiEvent(new MetricTimeSpan(), - new TextEvent("Test")), - new TimeAndMidiEvent(new MetricTimeSpan(0, 1, 0), - new NoteOnEvent((SevenBitNumber)50, (SevenBitNumber)110) { Channel = (FourBitNumber)7 }), - new TimeAndMidiEvent(new MetricTimeSpan(0, 1, 3), - new SetTempoEvent(300000)), - new TimeAndMidiEvent(new MetricTimeSpan(0, 1, 10), - new NoteOffEvent((SevenBitNumber)50, (SevenBitNumber)70) { Channel = (FourBitNumber)10 }), - new TimeAndMidiEvent(new MetricTimeSpan(0, 10, 3), - new NoteOffEvent((SevenBitNumber)50, (SevenBitNumber)80) { Channel = (FourBitNumber)7 }) - } - .Select(te => new TimedEvent(te.Event, TimeConverter.ConvertFrom(te.Time, expectedTempoMap))) - .ToArray(); - - Assert.AreEqual(1, midiFile.GetTrackChunks().Count(), "Track chunks count is invalid."); - CollectionAssert.AreEqual(midiFile.GetTempoMap().GetTempoChanges(), expectedTempoMap.GetTempoChanges(), "Invalid tempo map."); - Assert.AreEqual(new TicksPerQuarterNoteTimeDivision(500), midiFile.TimeDivision, "Invalid time division."); - MidiAsserts.AreEqual(expectedEvents, midiFile.GetTimedEvents(), false, 0, "Invalid events."); - } - - [TestCase((object)new[] - { - "0, 0, Text, \"Test", - " text wi\rth ne\nw line\"", - "0, 100, Marker, \"Marker\"", - "0, 200, Text, \"Test", - " text with new line and", - " new \"\"line again\"" - })] - public void ConvertCsvToMidiFile_NewLines(string[] csvLines) - { - var midiFile = ConvertCsvToMidiFile(TimeSpanType.Midi, csvLines); - - var expectedEvents = new[] - { - new TimedEvent(new TextEvent($"Test{Environment.NewLine} text wi\rth ne\nw line"), 0), - new TimedEvent(new MarkerEvent("Marker"), 100), - new TimedEvent(new TextEvent($"Test{Environment.NewLine} text with new line and{Environment.NewLine} new \"line again"), 200), - }; - - MidiAsserts.AreEqual(expectedEvents, midiFile.GetTimedEvents(), false, 0, "Invalid events."); - } - - [TestCase(NoteNumberFormat.NoteNumber, new[] - { - "0, 0, Note, 10, 50, 250, 120, 70", - "0, 0, Text, \"Test\"", - "0, 100, Note, 7, 50, 900, 110, 80" - })] - [TestCase(NoteNumberFormat.Letter, new[] - { - "0, 0, Note, 10, D3, 250, 120, 70", - "0, 0, Text, \"Test\"", - "0, 100, Note, 7, D3, 900, 110, 80" - })] - public void ConvertCsvToMidiFile_NoteNumberFormat(NoteNumberFormat noteNumberFormat, string[] csvLines) - { - var midiFile = ConvertCsvToMidiFile(TimeSpanType.Midi, csvLines, NoteFormat.Note, noteNumberFormat); - - var expectedObjects = new ITimedObject[] - { - new Note((SevenBitNumber)50, 250, 0) - { - Channel = (FourBitNumber)10, - Velocity = (SevenBitNumber)120, - OffVelocity = (SevenBitNumber)70 - }, - new TimedEvent(new TextEvent("Test"), 0), - new Note((SevenBitNumber)50, 900, 100) - { - Channel = (FourBitNumber)7, - Velocity = (SevenBitNumber)110, - OffVelocity = (SevenBitNumber)80 - } - }; - - Assert.AreEqual(1, midiFile.GetTrackChunks().Count(), "Track chunks count is invalid."); - MidiAsserts.AreEqual( - expectedObjects, - midiFile.GetObjects(ObjectType.TimedEvent | ObjectType.Note), - false, - 0, - "Invalid objects."); - } - - [TestCase(NoteNumberFormat.NoteNumber, new[] - { - "0, 0, Note, 10, 50, 0:0:10, 120, 70", - "0, 0, Text, \"Te\"\"s\"\"\"\"t\"", - "0, 100, Note, 7, 70, 0:0:0:500, 110, 80" - })] - [TestCase(NoteNumberFormat.Letter, new[] - { - "0, 0, Note, 10, D3, 0:0:10, 120, 70", - "0, 0, Text, \"Te\"\"s\"\"\"\"t\"", - "0, 100, Note, 7, A#4, 0:0:0:500, 110, 80" - })] - public void ConvertCsvToMidiFile_NoteLength_Metric(NoteNumberFormat noteNumberFormat, string[] csvLines) - { - var midiFile = ConvertCsvToMidiFile( - TimeSpanType.Midi, - csvLines, - NoteFormat.Note, - noteNumberFormat, - TimeSpanType.Metric); - - var tempoMap = TempoMap.Default; - - var expectedObjects = new ITimedObject[] - { - new Note((SevenBitNumber)50, LengthConverter.ConvertFrom(new MetricTimeSpan(0, 0, 10), 0, tempoMap), 0) - { - Channel = (FourBitNumber)10, - Velocity = (SevenBitNumber)120, - OffVelocity = (SevenBitNumber)70 - }, - new TimedEvent(new TextEvent("Te\"s\"\"t"), 0), - new Note((SevenBitNumber)70, LengthConverter.ConvertFrom(new MetricTimeSpan(0, 0, 0, 500), 100, tempoMap), 100) - { - Channel = (FourBitNumber)7, - Velocity = (SevenBitNumber)110, - OffVelocity = (SevenBitNumber)80 - } - }; - - Assert.AreEqual(1, midiFile.GetTrackChunks().Count(), "Track chunks count is invalid."); - MidiAsserts.AreEqual( - expectedObjects, - midiFile.GetObjects(ObjectType.TimedEvent | ObjectType.Note), - false, - 0, - "Invalid objects."); - } - - #endregion - - #region MidiFileToCsv - - [Test] - public void ConvertMidiFileToCsv_StreamIsNotDisposed() - { - var settings = new MidiFileCsvConversionSettings(); - - var csvConverter = new CsvConverter(); - - using (var streamToWrite = new MemoryStream()) - { - csvConverter.ConvertMidiFileToCsv(new MidiFile(), streamToWrite, settings); - Assert.DoesNotThrow(() => { var l = streamToWrite.Length; }); - } - } - - [TestCase((object)new[] { ",,header,,96" })] - public void ConvertMidiFileToCsv_EmptyFile(string[] expectedCsvLines) - { - var midiFile = new MidiFile(); - ConvertMidiFileToCsv(midiFile, TimeSpanType.Midi, expectedCsvLines); - } - - [TestCase((object)new[] - { - ",,header,,96", - "0,0,time signature,2,8,24,8", - "0,345,text,\"Test text\"", - "0,350,note on,0,23,78", - "0,450,note off,0,23,90", - "0,800,sequencer specific,3,1,2,3" - })] - public void ConvertMidiFileToCsv_SingleTrack(string[] expectedCsvLines) - { - var timedEvents = new[] - { - new TimedEvent(new TimeSignatureEvent(2, 8), 0), - new TimedEvent(new TextEvent("Test text"), 345), - new TimedEvent(new NoteOnEvent((SevenBitNumber)23, (SevenBitNumber)78), 350), - new TimedEvent(new NoteOffEvent((SevenBitNumber)23, (SevenBitNumber)90), 450), - new TimedEvent(new SequencerSpecificEvent(new byte[] { 1, 2, 3 }), 800) - }; - - var midiFile = timedEvents.ToFile(); - - ConvertMidiFileToCsv(midiFile, TimeSpanType.Midi, expectedCsvLines); - } - - [TestCase(NoteFormat.Events, NoteNumberFormat.NoteNumber, new[] - { - ",,header,,96", - "0,0,time signature,2,8,24,8", - "0,345,text,\"Test text\"", - "0,350,note on,0,23,78", - "0,450,note off,0,23,90", - "0,800,sequencer specific,3,1,2,3", - "1,10,note on,0,30,78", - "1,20,note off,0,30,90", - })] - [TestCase(NoteFormat.Note, NoteNumberFormat.NoteNumber, new[] - { - ",,header,,96", - "0,0,time signature,2,8,24,8", - "0,345,text,\"Test text\"", - "0,350,note,0,23,100,78,90", - "0,800,sequencer specific,3,1,2,3", - "1,10,note,0,30,10,78,90", - })] - [TestCase(NoteFormat.Events, NoteNumberFormat.Letter, new[] - { - ",,header,,96", - "0,0,time signature,2,8,24,8", - "0,345,text,\"Test text\"", - "0,350,note on,0,B0,78", - "0,450,note off,0,B0,90", - "0,800,sequencer specific,3,1,2,3", - "1,10,note on,0,F#1,78", - "1,20,note off,0,F#1,90", - })] - [TestCase(NoteFormat.Note, NoteNumberFormat.Letter, new[] - { - ",,header,,96", - "0,0,time signature,2,8,24,8", - "0,345,text,\"Test text\"", - "0,350,note,0,B0,100,78,90", - "0,800,sequencer specific,3,1,2,3", - "1,10,note,0,F#1,10,78,90", - })] - public void ConvertMidiFileToCsv_MultipleTrack(NoteFormat noteFormat, NoteNumberFormat noteNumberFormat, string[] expectedCsvLines) - { - var timedEvents1 = new[] - { - new TimedEvent(new TimeSignatureEvent(2, 8), 0), - new TimedEvent(new TextEvent("Test text"), 345), - new TimedEvent(new NoteOnEvent((SevenBitNumber)23, (SevenBitNumber)78), 350), - new TimedEvent(new NoteOffEvent((SevenBitNumber)23, (SevenBitNumber)90), 450), - new TimedEvent(new SequencerSpecificEvent(new byte[] { 1, 2, 3 }), 800) - }; - - var timedEvents2 = new[] - { - new TimedEvent(new NoteOnEvent((SevenBitNumber)30, (SevenBitNumber)78), 10), - new TimedEvent(new NoteOffEvent((SevenBitNumber)30, (SevenBitNumber)90), 20) - }; - - var midiFile = new MidiFile( - timedEvents1.ToTrackChunk(), - timedEvents2.ToTrackChunk()); - - ConvertMidiFileToCsv(midiFile, TimeSpanType.Midi, expectedCsvLines, noteFormat, noteNumberFormat); - } - - #endregion - - #region CsvToNotes - - [Test] - public void CsvToNotes_NoCsv() - { - ConvertCsvToNotes( - Enumerable.Empty(), - TempoMap.Default, - TimeSpanType.Midi, - new string[0]); - } - - [Test] - public void CsvToNotes_EmptyCsvLines() - { - ConvertCsvToNotes( - Enumerable.Empty(), - TempoMap.Default, - TimeSpanType.Midi, - new[] - { - string.Empty, - string.Empty, - string.Empty - }); - } - - [TestCase(NoteNumberFormat.NoteNumber, new[] - { - "", - "100, 2, 90, 100, 80, 56", - "0, 0, 92, 10, 70, 0", - "", - "10, 0, 92, 0, 72, 30", - })] - [TestCase(NoteNumberFormat.Letter, new[] - { - "", - "100, 2, F#6, 100, 80, 56", - "0, 0, G#6, 10, 70, 0", - "", - "", - "", - "10, 0, G#6, 0, 72, 30", - })] - public void CsvToNotes_MidiTime(NoteNumberFormat noteNumberFormat, string[] csvLines) - { - ConvertCsvToNotes( - new[] - { - new NoteWithCustomTimeAndLength(90, 2, 80, 56, (MidiTimeSpan)100, (MidiTimeSpan)100), - new NoteWithCustomTimeAndLength(92, 0, 70, 0, (MidiTimeSpan)0, (MidiTimeSpan)10), - new NoteWithCustomTimeAndLength(92, 0, 72, 30, (MidiTimeSpan)10, (MidiTimeSpan)0) - }, - TempoMap.Default, - TimeSpanType.Midi, - csvLines, - noteNumberFormat); - } - - [TestCase(NoteNumberFormat.NoteNumber, new[] - { - "0:0:0:500, 2, 90, 100, 80, 56", - "0:0:0, 0, 92, 10, 70, 0", - "0:0:1, 0, 92, 0, 72, 30", - })] - [TestCase(NoteNumberFormat.Letter, new[] - { - "0:0:0:500, 2, F#6, 100, 80, 56", - "0:0:0, 0, G#6, 10, 70, 0", - "0:0:1, 0, G#6, 0, 72, 30", - })] - public void CsvToNotes_MetricTime(NoteNumberFormat noteNumberFormat, string[] csvLines) - { - ConvertCsvToNotes( - new[] - { - new NoteWithCustomTimeAndLength(90, 2, 80, 56, new MetricTimeSpan(0, 0, 0, 500), (MidiTimeSpan)100), - new NoteWithCustomTimeAndLength(92, 0, 70, 0, new MetricTimeSpan(), (MidiTimeSpan)10), - new NoteWithCustomTimeAndLength(92, 0, 72, 30, new MetricTimeSpan(0, 0, 1), (MidiTimeSpan)0) - }, - TempoMap.Default, - TimeSpanType.Metric, - csvLines, - noteNumberFormat); - } - - [TestCase(NoteNumberFormat.NoteNumber, new[] - { - "100, 2, 90, 0:0:0:500, 80, 56", - "0, 0, 92, 0:1:0:500, 70, 0", - "10, 0, 92, 0:0:0, 72, 30", - })] - [TestCase(NoteNumberFormat.Letter, new[] - { - "100, 2, F#6, 0:0:0:500, 80, 56", - "0, 0, G#6, 0:1:0:500, 70, 0", - "10, 0, G#6, 0:0:0, 72, 30", - })] - public void CsvToNotes_MetricLength(NoteNumberFormat noteNumberFormat, string[] csvLines) - { - ConvertCsvToNotes( - new[] - { - new NoteWithCustomTimeAndLength(90, 2, 80, 56, (MidiTimeSpan)100, new MetricTimeSpan(0, 0, 0, 500)), - new NoteWithCustomTimeAndLength(92, 0, 70, 0, (MidiTimeSpan)0, new MetricTimeSpan(0, 1, 0, 500)), - new NoteWithCustomTimeAndLength(92, 0, 72, 30, (MidiTimeSpan)10, new MetricTimeSpan(0, 0, 0)) - }, - TempoMap.Default, - TimeSpanType.Midi, - csvLines, - noteNumberFormat, - TimeSpanType.Metric); - } - - [Test] - public void CsvToNotes_CustomDelimiter() - { - ConvertCsvToNotes( - new[] - { - new NoteWithCustomTimeAndLength(90, 2, 80, 56, MusicalTimeSpan.Whole.SingleDotted(), new MetricTimeSpan(0, 0, 0, 500)), - new NoteWithCustomTimeAndLength(92, 0, 70, 0, (MidiTimeSpan)0, new MetricTimeSpan(0, 1, 0, 500)), - new NoteWithCustomTimeAndLength(92, 0, 72, 30, MusicalTimeSpan.Eighth, new MetricTimeSpan(0, 0, 0)) - }, - TempoMap.Default, - TimeSpanType.Musical, - new[] - { - "1/1.;2;F#6;0:0:0:500;80;56", - "0/1;0;G#6;0:1:0:500;70;0", - "1/8;0;G#6;0:0:0;72;30", - }, - NoteNumberFormat.Letter, - TimeSpanType.Metric, - ';'); - } - - #endregion - - #region NotesToCsv - - [Test] - public void NotesToCsv_NoNotes() - { - ConvertNotesToCsv( - Enumerable.Empty(), - TempoMap.Default, - TimeSpanType.Midi, - new string[0]); - } - - [TestCase(NoteNumberFormat.NoteNumber, new[] - { - "100,2,90,100,80,56", - "0,0,92,10,70,0", - "10,0,92,0,72,30", - })] - [TestCase(NoteNumberFormat.Letter, new[] - { - "100,2,F#6,100,80,56", - "0,0,G#6,10,70,0", - "10,0,G#6,0,72,30", - })] - public void NotesToCsv_MidiTime(NoteNumberFormat noteNumberFormat, string[] csvLines) - { - ConvertNotesToCsv( - new[] - { - new NoteWithCustomTimeAndLength(90, 2, 80, 56, (MidiTimeSpan)100, (MidiTimeSpan)100), - new NoteWithCustomTimeAndLength(92, 0, 70, 0, (MidiTimeSpan)0, (MidiTimeSpan)10), - new NoteWithCustomTimeAndLength(92, 0, 72, 30, (MidiTimeSpan)10, (MidiTimeSpan)0) - }, - TempoMap.Default, - TimeSpanType.Midi, - csvLines, - noteNumberFormat); - } - - [TestCase(NoteNumberFormat.NoteNumber, new[] - { - "0:0:0:500,2,90,100,80,56", - "0:0:0:0,0,92,10,70,0", - "0:0:1:0,0,92,0,72,30", - })] - [TestCase(NoteNumberFormat.Letter, new[] - { - "0:0:0:500,2,F#6,100,80,56", - "0:0:0:0,0,G#6,10,70,0", - "0:0:1:0,0,G#6,0,72,30", - })] - public void NotesToCsv_MetricTime(NoteNumberFormat noteNumberFormat, string[] csvLines) - { - ConvertNotesToCsv( - new[] - { - new NoteWithCustomTimeAndLength(90, 2, 80, 56, new MetricTimeSpan(0, 0, 0, 500), (MidiTimeSpan)100), - new NoteWithCustomTimeAndLength(92, 0, 70, 0, new MetricTimeSpan(), (MidiTimeSpan)10), - new NoteWithCustomTimeAndLength(92, 0, 72, 30, new MetricTimeSpan(0, 0, 1), (MidiTimeSpan)0) - }, - TempoMap.Default, - TimeSpanType.Metric, - csvLines, - noteNumberFormat); - } - - [TestCase(NoteNumberFormat.NoteNumber, new[] - { - "100,2,90,0:0:0:500,80,56", - "0,0,92,0:1:0:500,70,0", - "10,0,92,0:0:0:0,72,30", - })] - [TestCase(NoteNumberFormat.Letter, new[] - { - "100,2,F#6,0:0:0:500,80,56", - "0,0,G#6,0:1:0:500,70,0", - "10,0,G#6,0:0:0:0,72,30", - })] - public void NotesToCsv_MetricLength(NoteNumberFormat noteNumberFormat, string[] csvLines) - { - ConvertNotesToCsv( - new[] - { - new NoteWithCustomTimeAndLength(90, 2, 80, 56, (MidiTimeSpan)100, new MetricTimeSpan(0, 0, 0, 500)), - new NoteWithCustomTimeAndLength(92, 0, 70, 0, (MidiTimeSpan)0, new MetricTimeSpan(0, 1, 0, 500)), - new NoteWithCustomTimeAndLength(92, 0, 72, 30, (MidiTimeSpan)10, new MetricTimeSpan(0, 0, 0)) - }, - TempoMap.Default, - TimeSpanType.Midi, - csvLines, - noteNumberFormat, - TimeSpanType.Metric); - } - - [Test] - public void NotesToCsv_CustomDelimiter() - { - ConvertNotesToCsv( - new[] - { - new NoteWithCustomTimeAndLength(90, 2, 80, 56, MusicalTimeSpan.Whole.SingleDotted(), new MetricTimeSpan(0, 0, 0, 500)), - new NoteWithCustomTimeAndLength(92, 0, 70, 0, (MidiTimeSpan)0, new MetricTimeSpan(0, 1, 0, 500)), - new NoteWithCustomTimeAndLength(92, 0, 72, 30, MusicalTimeSpan.Eighth, new MetricTimeSpan(0, 0, 0)) - }, - TempoMap.Default, - TimeSpanType.Musical, - new[] - { - "3/2;2;F#6;0:0:0:500;80;56", - "0/1;0;G#6;0:1:0:500;70;0", - "1/8;0;G#6;0:0:0:0;72;30", - }, - NoteNumberFormat.Letter, - TimeSpanType.Metric, - ';'); - } - - #endregion - - #endregion - - #region Private methods - - private static void ConvertMidiFileToFromCsv(MidiFileCsvConversionSettings settings) - { - var tempPath = Path.GetTempPath(); - var outputDirectory = Path.Combine(tempPath, Guid.NewGuid().ToString()); - Directory.CreateDirectory(outputDirectory); - - try - { - foreach (var filePath in TestFilesProvider.GetValidFilesPaths()) - { - var midiFile = MidiFile.Read(filePath); - var outputFilePath = Path.Combine(outputDirectory, Path.GetFileName(Path.ChangeExtension(filePath, "csv"))); - - var csvConverter = new CsvConverter(); - csvConverter.ConvertMidiFileToCsv(midiFile, outputFilePath, true, settings); - var convertedFile = csvConverter.ConvertCsvToMidiFile(outputFilePath, settings); - - MidiAsserts.AreEqual(midiFile, convertedFile, true, $"Conversion of '{filePath}' is invalid."); - } - } - finally - { - Directory.Delete(outputDirectory, true); - } - } - - private static void ConvertMidiFileToFromCsv(MidiFile midiFile, string outputFilePath, MidiFileCsvConversionSettings settings) - { - var csvConverter = new CsvConverter(); - csvConverter.ConvertMidiFileToCsv(midiFile, outputFilePath, true, settings); - csvConverter.ConvertCsvToMidiFile(outputFilePath, settings); - } - - private static MidiFile ConvertCsvToMidiFile( - TimeSpanType timeType, - string[] csvLines, - NoteFormat noteFormat = NoteFormat.Events, - NoteNumberFormat noteNumberFormat = NoteNumberFormat.NoteNumber, - TimeSpanType noteLengthType = TimeSpanType.Midi) - { - var filePath = Path.GetTempFileName(); - FileOperations.WriteAllLinesToFile(filePath, csvLines); - - var settings = new MidiFileCsvConversionSettings - { - TimeType = timeType, - NoteFormat = noteFormat, - NoteNumberFormat = noteNumberFormat, - NoteLengthType = noteLengthType - }; - - try - { - var midiFile = new CsvConverter().ConvertCsvToMidiFile(filePath, settings); - ConvertMidiFileToFromCsv(midiFile, filePath, settings); - return midiFile; - } - finally - { - FileOperations.DeleteFile(filePath); - } - } - - private static void ConvertMidiFileToCsv( - MidiFile midiFile, - TimeSpanType timeType, - string[] expectedCsvLines, - NoteFormat noteFormat = NoteFormat.Events, - NoteNumberFormat noteNumberFormat = NoteNumberFormat.NoteNumber, - TimeSpanType noteLengthType = TimeSpanType.Midi) - { - var filePath = Path.GetTempFileName(); - - var settings = new MidiFileCsvConversionSettings - { - TimeType = timeType, - NoteFormat = noteFormat, - NoteNumberFormat = noteNumberFormat, - NoteLengthType = noteLengthType - }; - - try - { - new CsvConverter().ConvertMidiFileToCsv(midiFile, filePath, true, settings); - var actualCsvLines = FileOperations.ReadAllFileLines(filePath); - CollectionAssert.AreEqual(expectedCsvLines, actualCsvLines, StringComparer.OrdinalIgnoreCase); - } - finally - { - FileOperations.DeleteFile(filePath); - } - } - - private static void ConvertNotesToFromCsv(IEnumerable notes, TempoMap tempoMap, string outputFilePath, NoteCsvConversionSettings settings) - { - var csvConverter = new CsvConverter(); - csvConverter.ConvertNotesToCsv(notes, outputFilePath, tempoMap, true, settings); - csvConverter.ConvertCsvToNotes(outputFilePath, tempoMap, settings); - } - - private static void ConvertCsvToNotes( - IEnumerable expectedNotes, - TempoMap tempoMap, - TimeSpanType timeType, - string[] csvLines, - NoteNumberFormat noteNumberFormat = NoteNumberFormat.NoteNumber, - TimeSpanType noteLengthType = TimeSpanType.Midi, - char delimiter = ',') - { - var filePath = Path.GetTempFileName(); - FileOperations.WriteAllLinesToFile(filePath, csvLines); - - var settings = new NoteCsvConversionSettings - { - TimeType = timeType, - NoteNumberFormat = noteNumberFormat, - NoteLengthType = noteLengthType - }; - - settings.CsvSettings.CsvDelimiter = delimiter; - - try - { - var actualNotes = new CsvConverter().ConvertCsvToNotes(filePath, tempoMap, settings).ToList(); - MidiAsserts.AreEqual(expectedNotes.Select(n => n.GetNote(tempoMap)), actualNotes, "Notes are invalid."); - - ConvertNotesToFromCsv(actualNotes, tempoMap, filePath, settings); - } - finally - { - FileOperations.DeleteFile(filePath); - } - } - - private static void ConvertNotesToCsv( - IEnumerable expectedNotes, - TempoMap tempoMap, - TimeSpanType timeType, - string[] expectedCsvLines, - NoteNumberFormat noteNumberFormat = NoteNumberFormat.NoteNumber, - TimeSpanType noteLengthType = TimeSpanType.Midi, - char delimiter = ',') - { - var filePath = Path.GetTempFileName(); - - var settings = new NoteCsvConversionSettings - { - TimeType = timeType, - NoteNumberFormat = noteNumberFormat, - NoteLengthType = noteLengthType - }; - - settings.CsvSettings.CsvDelimiter = delimiter; - - try - { - new CsvConverter().ConvertNotesToCsv(expectedNotes.Select(n => n.GetNote(tempoMap)), filePath, tempoMap, true, settings); - var actualCsvLines = FileOperations.ReadAllFileLines(filePath); - CollectionAssert.AreEqual(expectedCsvLines, actualCsvLines, StringComparer.OrdinalIgnoreCase); - } - finally - { - FileOperations.DeleteFile(filePath); - } - } - - #endregion - } -} diff --git a/DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Deserialize.cs b/DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Deserialize.cs new file mode 100644 index 000000000..230ef0db1 --- /dev/null +++ b/DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Deserialize.cs @@ -0,0 +1,407 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Interaction; +using Melanchall.DryWetMidi.Tests.Utilities; +using Melanchall.DryWetMidi.Tools; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Melanchall.DryWetMidi.Tests.Tools +{ + [TestFixture] + public sealed partial class CsvSerializerTests + { + #region Test methods + + [TestCaseSource(nameof(EventsData))] + public void Deserialize_Event(MidiEvent midiEvent, string expectedCsv, CsvSerializationSettings settings) => CheckDeserialize( + csvLines: new[] { $"0,\"{midiEvent.EventType}\",0{(string.IsNullOrEmpty(expectedCsv) ? string.Empty : $",{expectedCsv}")}" }, + check: stream => + { + var objects = CsvSerializer.DeserializeObjectsFromCsv(stream, TempoMap.Default, settings).ToArray(); + Assert.AreEqual(1, objects.Length, "More than one object read."); + + var timedEvent = (TimedEvent)objects.Single(); + MidiAsserts.AreEqual(midiEvent, timedEvent.Event, false, "Invalid event."); + }); + + [Test] + public void DeserializeFile_Empty() => DeserializeFileAndChunksAndSeparateChunks( + csvLines: new[] + { + $"0,\"MThd\",0,\"Header\",1234", + }, + settings: null, + expectedMidiFile: new MidiFile { TimeDivision = new TicksPerQuarterNoteTimeDivision(1234) }); + + [Test] + public void Deserialize() => DeserializeFileAndChunksAndSeparateChunks( + csvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0,\"A\"", + $"1,\"MTrk\",1,\"Text\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote},\"B\"", + }, + settings: null, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }))); + + [Test] + public void Deserialize_TimeType() => DeserializeFileAndChunksAndSeparateChunks( + csvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0/1,\"A\"", + $"1,\"MTrk\",1,\"Text\",1/4,\"B\"", + }, + settings: new CsvSerializationSettings + { + TimeType = TimeSpanType.Musical + }, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }))); + + [Test] + public void Deserialize_MultipleTrackChunks() => DeserializeFileAndChunksAndSeparateChunks( + csvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0/1,\"A\"", + $"1,\"MTrk\",1,\"Text\",1/4,\"B\"", + $"2,\"MTrk\",0,\"NoteOn\",0/1,4,100,127", + $"2,\"MTrk\",1,\"NoteOff\",1/4,4,100,0", + }, + settings: new CsvSerializationSettings + { + TimeType = TimeSpanType.Musical + }, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }), + new TrackChunk( + new NoteOnEvent((SevenBitNumber)100, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)4 }, + new NoteOffEvent((SevenBitNumber)100, SevenBitNumber.MinValue) { Channel = (FourBitNumber)4, DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }))); + + [Test] + public void Deserialize_Notes() => DeserializeFileAndChunksAndSeparateChunks( + csvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0,\"A\"", + $"1,\"MTrk\",1,\"Text\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote},\"B\"", + $"2,\"MTrk\",0,\"Note\",0,{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote},4,100,127,0", + }, + settings: null, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }), + new TrackChunk( + new NoteOnEvent((SevenBitNumber)100, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)4 }, + new NoteOffEvent((SevenBitNumber)100, SevenBitNumber.MinValue) { Channel = (FourBitNumber)4, DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }))); + + [Test] + public void Deserialize_Notes_Letter() => DeserializeFileAndChunksAndSeparateChunks( + csvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0:0:0:0,\"A\"", + $"1,\"MTrk\",1,\"Text\",0:0:0:500,\"B\"", + $"2,\"MTrk\",0,\"Note\",0:0:0:0,1/4,4,E7,127,0", + }, + settings: new CsvSerializationSettings + { + NoteNumberFormat = CsvNoteFormat.Letter, + TimeType = TimeSpanType.Metric, + LengthType = TimeSpanType.Musical, + }, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }), + new TrackChunk( + new NoteOnEvent((SevenBitNumber)100, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)4 }, + new NoteOffEvent((SevenBitNumber)100, SevenBitNumber.MinValue) { Channel = (FourBitNumber)4, DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }))); + + [Test] + public void Deserialize_AllObjectTypes() => DeserializeFileAndChunksAndSeparateChunks( + csvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0,\"A\"", + $"1,\"MTrk\",1,\"Text\",100,\"B\"", + $"2,\"MTrk\",0,\"Note\",0,100,4,E7,127,0", + $"2,\"MTrk\",1,\"Note\",100,100,3,D3,127,0", + $"2,\"MTrk\",1,\"Note\",110,100,3,E2,127,0", + }, + settings: new CsvSerializationSettings + { + NoteNumberFormat = CsvNoteFormat.Letter, + }, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = 100 }), + new TrackChunk( + new NoteOnEvent((SevenBitNumber)100, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)4 }, + new NoteOffEvent((SevenBitNumber)100, SevenBitNumber.MinValue) { Channel = (FourBitNumber)4, DeltaTime = 100 }, + new NoteOnEvent((SevenBitNumber)50, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)3 }, + new NoteOnEvent((SevenBitNumber)40, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)3, DeltaTime = 10 }, + new NoteOffEvent((SevenBitNumber)50, SevenBitNumber.MinValue) { Channel = (FourBitNumber)3, DeltaTime = 90 }, + new NoteOffEvent((SevenBitNumber)40, SevenBitNumber.MinValue) { Channel = (FourBitNumber)3, DeltaTime = 10 }))); + + [Test] + public void Deserialize_Delimiter() => DeserializeFileAndChunksAndSeparateChunks( + csvLines: new[] + { + $"0 \"MThd\" 0 \"Header\" {TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1 \"MTrk\" 0 \"Text\" 0/1 \"A\"", + $"1 \"MTrk\" 1 \"Text\" 1/4 \"B\"", + $"1 \"MTrk\" 2 \"NormalSysEx\" 1/4 \"9 10 15 255\"", + }, + settings: new CsvSerializationSettings + { + TimeType = TimeSpanType.Musical, + Delimiter = ' ', + }, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }, + new NormalSysExEvent(new byte[] { 9, 10, 15, 255 })))); + + [Test] + public void Deserialize_BytesArrayFormat() => DeserializeFileAndChunksAndSeparateChunks( + csvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0,\"A\"", + $"1,\"MTrk\",1,\"NormalSysEx\",0,\"09 0A 0F FF\"", + }, + settings: new CsvSerializationSettings + { + BytesArrayFormat = CsvBytesArrayFormat.Hexadecimal, + }, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new NormalSysExEvent(new byte[] { 9, 10, 15, 255 })))); + + [Test] + public void Deserialize_NewlinesAndQuotes() => DeserializeFileAndChunksAndSeparateChunks( + csvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0,\"\"\"in\"\" \"\"quotes\"\"\"", + $"1,\"MTrk\",1,\"Text\",0,\"B", + $"bb\"\"in quotes\"\"\"", + $"1,\"MTrk\",2,\"Text\",0,\"C\"", + }, + settings: null, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("\"in\" \"quotes\""), + new TextEvent($"B{Environment.NewLine}bb\"in quotes\""), + new TextEvent("C"))), + checkSeparateChunks: false); + + [Test] + public void Deserialize_Objects_1() => DeserializeObjects( + csvLines: new[] + { + $"0,\"Text\",0,\"A\"", + $"1,\"Text\",100,\"B\"", + $"2,\"Note\",0,100,4,E7,127,1", + $"3,\"Note\",100,100,3,D3,127,2", + $"3,\"Note\",110,100,3,E2,125,3", + }, + settings: new CsvSerializationSettings + { + NoteNumberFormat = CsvNoteFormat.Letter, + }, + expectedObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A")), + new TimedEvent(new TextEvent("B"), 100), + new Note((SevenBitNumber)100, 100, 0) { Channel = (FourBitNumber)4, Velocity = SevenBitNumber.MaxValue, OffVelocity = (SevenBitNumber)1 }, + new Chord( + new Note((SevenBitNumber)50, 100, 100) { Channel = (FourBitNumber)3, Velocity = SevenBitNumber.MaxValue, OffVelocity = (SevenBitNumber)2 }, + new Note((SevenBitNumber)40, 100, 110) { Channel = (FourBitNumber)3, Velocity = (SevenBitNumber)125, OffVelocity = (SevenBitNumber)3 }), + }); + + [Test] + public void Deserialize_Objects_2() => DeserializeObjects( + csvLines: new[] + { + $"0,\"Text\",0,\"A\"", + $"1,\"Text\",100,\"B\"", + $"2,\"Note\",0,100,3,E7,127,1", + $"2,\"Note\",100,100,3,D3,127,2", + $"2,\"Note\",110,100,3,E2,125,3", + }, + settings: new CsvSerializationSettings + { + NoteNumberFormat = CsvNoteFormat.Letter, + }, + expectedObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A")), + new TimedEvent(new TextEvent("B"), 100), + new Chord( + new Note((SevenBitNumber)100, 100, 0) { Channel = (FourBitNumber)3, Velocity = SevenBitNumber.MaxValue, OffVelocity = (SevenBitNumber)1 }, + new Note((SevenBitNumber)50, 100, 100) { Channel = (FourBitNumber)3, Velocity = SevenBitNumber.MaxValue, OffVelocity = (SevenBitNumber)2 }, + new Note((SevenBitNumber)40, 100, 110) { Channel = (FourBitNumber)3, Velocity = (SevenBitNumber)125, OffVelocity = (SevenBitNumber)3 }), + }); + + [Test] + public void Deserialize_Objects_3() => DeserializeObjects( + csvLines: new[] + { + $"0,\"Text\",0,\"A\"", + $"1,\"Text\",100,\"B\"", + $"2,\"Note\",0,100,3,E7,127,1", + $"3,\"Note\",100,100,3,D3,127,2", + $"4,\"Note\",110,100,3,E2,125,3", + }, + settings: new CsvSerializationSettings + { + NoteNumberFormat = CsvNoteFormat.Letter, + }, + expectedObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A")), + new TimedEvent(new TextEvent("B"), 100), + new Note((SevenBitNumber)100, 100, 0) { Channel = (FourBitNumber)3, Velocity = SevenBitNumber.MaxValue, OffVelocity = (SevenBitNumber)1 }, + new Note((SevenBitNumber)50, 100, 100) { Channel = (FourBitNumber)3, Velocity = SevenBitNumber.MaxValue, OffVelocity = (SevenBitNumber)2 }, + new Note((SevenBitNumber)40, 100, 110) { Channel = (FourBitNumber)3, Velocity = (SevenBitNumber)125, OffVelocity = (SevenBitNumber)3 }, + }); + + #endregion + + #region Private methods + + private void DeserializeObjects( + string[] csvLines, + CsvSerializationSettings settings, + ICollection expectedObjects) + { + CheckDeserialize( + csvLines, + stream => + { + var actualObjects = CsvSerializer.DeserializeObjectsFromCsv(stream, TempoMap.Default, settings); + MidiAsserts.AreEqual(expectedObjects, actualObjects, "Invalid objects."); + }); + } + + private void DeserializeFileAndChunksAndSeparateChunks( + string[] csvLines, + CsvSerializationSettings settings, + MidiFile expectedMidiFile, + bool checkSeparateChunks = true) + { + DeserializeFile(csvLines, settings, expectedMidiFile); + + var tempoMap = expectedMidiFile.GetTempoMap(); + + DeserializeChunks( + csvLines.Skip(1).ToArray(), + settings, + tempoMap, + expectedMidiFile.Chunks); + + if (checkSeparateChunks) + DeserializeSeparateChunks( + csvLines.Skip(1).ToArray(), + settings, + tempoMap, + expectedMidiFile.Chunks); + } + + private void DeserializeFile( + string[] csvLines, + CsvSerializationSettings settings, + MidiFile expectedMidiFile) + { + CheckDeserialize( + csvLines, + stream => + { + var midiFile = CsvSerializer.DeserializeFileFromCsv(stream, settings); + MidiAsserts.AreEqual(expectedMidiFile, midiFile, false, "Invalid file."); + }); + } + + private void DeserializeChunks( + string[] csvLines, + CsvSerializationSettings settings, + TempoMap tempoMap, + ICollection expectedChunks) + { + CheckDeserialize( + csvLines, + stream => + { + var chunks = CsvSerializer.DeserializeChunksFromCsv(stream, tempoMap, settings); + MidiAsserts.AreEqual(expectedChunks, chunks, true, "Invalid chunks."); + }); + } + + private void DeserializeSeparateChunks( + string[] csvLines, + CsvSerializationSettings settings, + TempoMap tempoMap, + ICollection expectedChunks) + { + var i = 0; + + foreach (var expectedChunk in expectedChunks) + { + var chunkCsvLines = csvLines + .Where(l => Regex.Match(l, @"^(\d+?)").Value == (i + 1).ToString()) + .Select(l => Regex.Replace(l, @"^\d+?", m => "0")) + .ToArray(); + + CheckDeserialize( + chunkCsvLines, + stream => + { + var chunk = CsvSerializer.DeserializeChunkFromCsv(stream, tempoMap, settings); + MidiAsserts.AreEqual(expectedChunk, chunk, true, $"Invalid chunk {i}."); + }); + + i++; + } + } + + private static void CheckDeserialize( + string[] csvLines, + Action check) + { + using (var stream = new MemoryStream()) + using (var streamWriter = new StreamWriter(stream)) + { + foreach (var line in csvLines) + { + streamWriter.WriteLine(line); + } + + streamWriter.Flush(); + stream.Seek(0, SeekOrigin.Begin); + + check(stream); + } + } + + #endregion + } +} diff --git a/DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Misc.cs b/DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Misc.cs new file mode 100644 index 000000000..79b247a33 --- /dev/null +++ b/DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Misc.cs @@ -0,0 +1,124 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Tests.Common; +using Melanchall.DryWetMidi.Tests.Utilities; +using Melanchall.DryWetMidi.Tools; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; + +namespace Melanchall.DryWetMidi.Tests.Tools +{ + [TestFixture] + public sealed partial class CsvSerializerTests + { + #region Constants + + private static readonly CsvSerializationSettings DefaultSettings = new CsvSerializationSettings(); + private static readonly CsvSerializationSettings HexBytesSettings = new CsvSerializationSettings + { + BytesArrayFormat = CsvBytesArrayFormat.Hexadecimal, + }; + private static readonly CsvSerializationSettings NoteLetterSettings = new CsvSerializationSettings + { + NoteNumberFormat = CsvNoteFormat.Letter, + }; + + private static readonly object[][] EventsData = new[] + { + new object[] { new NormalSysExEvent(new byte[] { 100, 0, 123 }), "\"100 0 123\"", DefaultSettings }, + new object[] { new NormalSysExEvent(new byte[] { 100, 0, 123 }), "\"64 00 7B\"", HexBytesSettings }, + new object[] { new EscapeSysExEvent(new byte[] { 102, 10, 0 }), "\"102 10 0\"", DefaultSettings }, + new object[] { new EscapeSysExEvent(new byte[] { 102, 10, 0 }), "\"66 0A 00\"", HexBytesSettings }, + new object[] { new SequenceNumberEvent(23), "23", DefaultSettings }, + new object[] { new TextEvent("Just want"), "\"Just want\"", DefaultSettings }, + new object[] { new CopyrightNoticeEvent("to know"), "\"to know\"", DefaultSettings }, + new object[] { new SequenceTrackNameEvent("whether the tests"), "\"whether the tests\"", DefaultSettings }, + new object[] { new InstrumentNameEvent("pass or not. "), "\"pass or not. \"", DefaultSettings }, + new object[] { new LyricEvent("Here some lyric."), "\"Here some lyric.\"", DefaultSettings }, + new object[] { new MarkerEvent("But there some marker..."), "\"But there some marker...\"", DefaultSettings }, + new object[] { new CuePointEvent("Just a cue point with\r\nnewline"), "\"Just a cue point with\r\nnewline\"", DefaultSettings }, + new object[] { new ProgramNameEvent("Program? DryWetMIDI, of course!"), "\"Program? DryWetMIDI, of course!\"", DefaultSettings }, + new object[] { new DeviceNameEvent("No device"), "\"No device\"", DefaultSettings }, + new object[] { new ChannelPrefixEvent(34), "34", DefaultSettings }, + new object[] { new PortPrefixEvent(43), "43", DefaultSettings }, + new object[] { new SetTempoEvent(123456), "123456", DefaultSettings }, + new object[] { new SmpteOffsetEvent(SmpteFormat.ThirtyDrop, 5, 4, 3, 2, 1), "\"ThirtyDrop\",5,4,3,2,1", DefaultSettings }, + new object[] { new TimeSignatureEvent(3, 8, 56, 32), "3,8,56,32", DefaultSettings }, + new object[] { new KeySignatureEvent(5, 1), "5,1", DefaultSettings }, + new object[] { new SequencerSpecificEvent(new byte[] { 43, 1, 11, 56 }), "\"43 1 11 56\"", DefaultSettings }, + new object[] { new SequencerSpecificEvent(new byte[] { 43, 1, 11, 56 }), "\"2B 01 0B 38\"", HexBytesSettings }, + new object[] { new UnknownMetaEvent(100, new byte[] { 2, 0, 3, 123 }), "100,\"2 0 3 123\"", DefaultSettings }, + new object[] { new UnknownMetaEvent(100, new byte[] { 2, 0, 3, 123 }), "100,\"02 00 03 7B\"", HexBytesSettings }, + new object[] { new NoteOffEvent((SevenBitNumber)45, (SevenBitNumber)56) { Channel = (FourBitNumber)5 }, "5,45,56", DefaultSettings }, + new object[] { new NoteOffEvent((SevenBitNumber)45, (SevenBitNumber)56) { Channel = (FourBitNumber)5 }, "5,A2,56", NoteLetterSettings }, + new object[] { new NoteOnEvent((SevenBitNumber)54, (SevenBitNumber)65) { Channel = (FourBitNumber)3 }, "3,54,65", DefaultSettings }, + new object[] { new NoteOnEvent((SevenBitNumber)54, (SevenBitNumber)65) { Channel = (FourBitNumber)3 }, "3,F#3,65", NoteLetterSettings }, + new object[] { new NoteAftertouchEvent((SevenBitNumber)123, (SevenBitNumber)100) { Channel = (FourBitNumber)2 }, "2,123,100", DefaultSettings }, + new object[] { new NoteAftertouchEvent((SevenBitNumber)123, (SevenBitNumber)100) { Channel = (FourBitNumber)2 }, "2,D#9,100", NoteLetterSettings }, + new object[] { new ControlChangeEvent((SevenBitNumber)78, (SevenBitNumber)10) { Channel = (FourBitNumber)1 }, "1,78,10", DefaultSettings }, + new object[] { new ProgramChangeEvent((SevenBitNumber)98) { Channel = (FourBitNumber)11 }, "11,98", DefaultSettings }, + new object[] { new ChannelAftertouchEvent((SevenBitNumber)89) { Channel = (FourBitNumber)14 }, "14,89", DefaultSettings }, + new object[] { new PitchBendEvent(3456) { Channel = (FourBitNumber)7 }, "7,3456", DefaultSettings }, + new object[] { new TimingClockEvent(), string.Empty, DefaultSettings }, + new object[] { new StartEvent(), string.Empty, DefaultSettings }, + new object[] { new ContinueEvent(), string.Empty, DefaultSettings }, + new object[] { new StopEvent(), string.Empty, DefaultSettings }, + new object[] { new ActiveSensingEvent(), string.Empty, DefaultSettings }, + new object[] { new ResetEvent(), string.Empty, DefaultSettings }, + new object[] { new MidiTimeCodeEvent(MidiTimeCodeComponent.SecondsMsb, (FourBitNumber)3), "\"SecondsMsb\",3", DefaultSettings }, + new object[] { new SongPositionPointerEvent(13), "13", DefaultSettings }, + new object[] { new SongSelectEvent((SevenBitNumber)69), "69", DefaultSettings }, + new object[] { new TuneRequestEvent(), string.Empty, DefaultSettings }, + }; + + #endregion + + #region Test methods + + [Test] + public void SerializeDeserialize_AllEventsTypesChecked() + { + var allEventsTypes = Enum + .GetValues(typeof(MidiEventType)) + .Cast() + .Except(new[] { MidiEventType.EndOfTrack, MidiEventType.CustomMeta }) + .ToArray(); + var checkedEventsTypes = EventsData + .Select(d => ((MidiEvent)d.First()).EventType) + .Distinct() + .ToArray(); + + CollectionAssert.AreEquivalent(allEventsTypes, checkedEventsTypes, "Some events types are not checked."); + } + + [Test] + public void SerializeDeserialize_ValidFiles() + { + var tempPath = Path.GetTempPath(); + var outputDirectory = Path.Combine(tempPath, Guid.NewGuid().ToString()); + Directory.CreateDirectory(outputDirectory); + + try + { + foreach (var filePath in TestFilesProvider.GetValidFilesPaths()) + { + var midiFile = MidiFile.Read(filePath); + var outputFilePath = Path.Combine(outputDirectory, Path.GetFileName(Path.ChangeExtension(filePath, "csv"))); + + midiFile.SerializeToCsv(outputFilePath, true, null); + var convertedFile = CsvSerializer.DeserializeFileFromCsv(outputFilePath, null); + + MidiAsserts.AreEqual(midiFile, convertedFile, false, $"Conversion of '{filePath}' is invalid."); + } + } + finally + { + Directory.Delete(outputDirectory, true); + } + } + + #endregion + } +} diff --git a/DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Serialize.cs b/DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Serialize.cs new file mode 100644 index 000000000..0bb521bc1 --- /dev/null +++ b/DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Serialize.cs @@ -0,0 +1,420 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Interaction; +using Melanchall.DryWetMidi.Tests.Common; +using Melanchall.DryWetMidi.Tools; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Melanchall.DryWetMidi.Tests.Tools +{ + [TestFixture] + public sealed partial class CsvSerializerTests + { + #region Test methods + + [TestCaseSource(nameof(EventsData))] + public void Serialize_Event(MidiEvent midiEvent, string expectedCsv, CsvSerializationSettings settings) + { + using (var stream = new MemoryStream()) + { + var timedEvent = new TimedEvent(midiEvent); + new[] { timedEvent }.SerializeToCsv(stream, TempoMap.Default, settings); + + stream.Seek(0, SeekOrigin.Begin); + + using (var streamReader = new StreamReader(stream)) + { + var csv = streamReader.ReadToEnd().Trim(); + Assert.AreEqual($"0,\"{midiEvent.EventType}\",0{(string.IsNullOrEmpty(expectedCsv) ? string.Empty : $",{expectedCsv}")}", csv, "Invalid CSV."); + } + } + } + + [Test] + public void Serialize_Empty([Values(null, MidiFileFormat.MultiTrack)] MidiFileFormat? originalFormat) => Serialize( + midiFile: new MidiFile(), + originalFormat: originalFormat, + settings: null, + objectType: ObjectType.TimedEvent, + objectDetectionSettings: null, + expectedCsvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + }); + + [Test] + public void Serialize() => Serialize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote })), + originalFormat: null, + settings: null, + objectType: ObjectType.TimedEvent, + objectDetectionSettings: null, + expectedCsvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0,\"A\"", + $"1,\"MTrk\",1,\"Text\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote},\"B\"", + }); + + [Test] + public void Serialize_TimeType() => Serialize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote })), + originalFormat: null, + settings: new CsvSerializationSettings + { + TimeType = TimeSpanType.Musical + }, + objectType: ObjectType.TimedEvent, + objectDetectionSettings: null, + expectedCsvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0/1,\"A\"", + $"1,\"MTrk\",1,\"Text\",1/4,\"B\"", + }); + + [Test] + public void Serialize_MultipleTrackChunks() => Serialize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }), + new TrackChunk( + new NoteOnEvent((SevenBitNumber)100, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)4 }, + new NoteOffEvent((SevenBitNumber)100, SevenBitNumber.MinValue) { Channel = (FourBitNumber)4, DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote })), + originalFormat: null, + settings: new CsvSerializationSettings + { + TimeType = TimeSpanType.Musical + }, + objectType: ObjectType.TimedEvent, + objectDetectionSettings: null, + expectedCsvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0/1,\"A\"", + $"1,\"MTrk\",1,\"Text\",1/4,\"B\"", + $"2,\"MTrk\",0,\"NoteOn\",0/1,4,100,127", + $"2,\"MTrk\",1,\"NoteOff\",1/4,4,100,0", + }); + + [Test] + public void Serialize_Notes() => Serialize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }), + new TrackChunk( + new NoteOnEvent((SevenBitNumber)100, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)4 }, + new NoteOffEvent((SevenBitNumber)100, SevenBitNumber.MinValue) { Channel = (FourBitNumber)4, DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote })), + originalFormat: null, + settings: null, + objectType: ObjectType.TimedEvent | ObjectType.Note, + objectDetectionSettings: null, + expectedCsvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0,\"A\"", + $"1,\"MTrk\",1,\"Text\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote},\"B\"", + $"2,\"MTrk\",0,\"Note\",0,{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote},4,100,127,0", + }); + + [Test] + public void Serialize_Notes_Letter() => Serialize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }), + new TrackChunk( + new NoteOnEvent((SevenBitNumber)100, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)4 }, + new NoteOffEvent((SevenBitNumber)100, SevenBitNumber.MinValue) { Channel = (FourBitNumber)4, DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote })), + originalFormat: null, + settings: new CsvSerializationSettings + { + NoteNumberFormat = CsvNoteFormat.Letter, + TimeType = TimeSpanType.Metric, + LengthType = TimeSpanType.Musical, + }, + objectType: ObjectType.TimedEvent | ObjectType.Note, + objectDetectionSettings: null, + expectedCsvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0:0:0:0,\"A\"", + $"1,\"MTrk\",1,\"Text\",0:0:0:500,\"B\"", + $"2,\"MTrk\",0,\"Note\",0:0:0:0,1/4,4,E7,127,0", + }); + + [Test] + public void Serialize_AllObjectTypes() => Serialize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = 100 }), + new TrackChunk( + new NoteOnEvent((SevenBitNumber)100, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)4 }, + new NoteOffEvent((SevenBitNumber)100, SevenBitNumber.MinValue) { Channel = (FourBitNumber)4, DeltaTime = 100 }, + new NoteOnEvent((SevenBitNumber)50, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)3 }, + new NoteOnEvent((SevenBitNumber)40, SevenBitNumber.MaxValue) { Channel = (FourBitNumber)3, DeltaTime = 10 }, + new NoteOffEvent((SevenBitNumber)50, SevenBitNumber.MinValue) { Channel = (FourBitNumber)3, DeltaTime = 90 }, + new NoteOffEvent((SevenBitNumber)40, SevenBitNumber.MinValue) { Channel = (FourBitNumber)3, DeltaTime = 10 })), + originalFormat: null, + settings: new CsvSerializationSettings + { + NoteNumberFormat = CsvNoteFormat.Letter, + }, + objectType: ObjectType.TimedEvent | ObjectType.Note | ObjectType.Chord, + objectDetectionSettings: new ObjectDetectionSettings + { + ChordDetectionSettings = new ChordDetectionSettings + { + NotesMinCount = 2, + NotesTolerance = 10 + } + }, + expectedCsvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0,\"A\"", + $"1,\"MTrk\",1,\"Text\",100,\"B\"", + $"2,\"MTrk\",0,\"Note\",0,100,4,E7,127,0", + $"2,\"MTrk\",1,\"Note\",100,100,3,D3,127,0", + $"2,\"MTrk\",1,\"Note\",110,100,3,E2,127,0", + }); + + [Test] + public void Serialize_Delimiter() => Serialize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new TextEvent("B") { DeltaTime = TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote }, + new NormalSysExEvent(new byte[] { 9, 10, 15, 255 }))), + originalFormat: null, + settings: new CsvSerializationSettings + { + TimeType = TimeSpanType.Musical, + Delimiter = ' ', + }, + objectType: ObjectType.TimedEvent, + objectDetectionSettings: null, + expectedCsvLines: new[] + { + $"0 \"MThd\" 0 \"Header\" {TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1 \"MTrk\" 0 \"Text\" 0/1 \"A\"", + $"1 \"MTrk\" 1 \"Text\" 1/4 \"B\"", + $"1 \"MTrk\" 2 \"NormalSysEx\" 1/4 \"9 10 15 255\"", + }); + + [Test] + public void Serialize_BytesArrayFormat() => Serialize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A"), + new NormalSysExEvent(new byte[] { 9, 10, 15, 255 }))), + originalFormat: null, + settings: new CsvSerializationSettings + { + BytesArrayFormat = CsvBytesArrayFormat.Hexadecimal, + }, + objectType: ObjectType.TimedEvent, + objectDetectionSettings: null, + expectedCsvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0,\"A\"", + $"1,\"MTrk\",1,\"NormalSysEx\",0,\"09 0A 0F FF\"", + }); + + [Test] + public void Serialize_NewlinesAndQuotes() => Serialize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("\"in\" \"quotes\""), + new TextEvent($"B{Environment.NewLine}bb\"in quotes\""), + new TextEvent("C"))), + originalFormat: null, + settings: null, + objectType: ObjectType.TimedEvent, + objectDetectionSettings: null, + expectedCsvLines: new[] + { + $"0,\"MThd\",0,\"Header\",{TicksPerQuarterNoteTimeDivision.DefaultTicksPerQuarterNote}", + $"1,\"MTrk\",0,\"Text\",0,\"\"\"in\"\" \"\"quotes\"\"\"", + $"1,\"MTrk\",1,\"Text\",0,\"B", + $"bb\"\"in quotes\"\"\"", + $"1,\"MTrk\",2,\"Text\",0,\"C\"", + }, + checkSeparateChunks: false); + + #endregion + + #region Private methods + + private void Serialize( + MidiFile midiFile, + MidiFileFormat? originalFormat, + CsvSerializationSettings settings, + ObjectType objectType, + ObjectDetectionSettings objectDetectionSettings, + string[] expectedCsvLines, + bool checkSeparateChunks = true) + { + if (originalFormat != null) + midiFile = MidiFileTestUtilities.Read(midiFile, null, null, originalFormat); + + var filePath = FileOperations.GetTempFilePath(); + + try + { + Serialize_File( + midiFile, + filePath, + settings, + objectType, + objectDetectionSettings, + expectedCsvLines); + + Serialize_ChunksFromFile( + midiFile, + filePath, + settings, + objectType, + objectDetectionSettings, + expectedCsvLines); + + if (checkSeparateChunks) + { + Serialize_ChunkFromFile( + midiFile, + filePath, + settings, + objectType, + objectDetectionSettings, + expectedCsvLines); + + Serialize_ObjectsFromFile( + midiFile, + filePath, + settings, + objectType, + objectDetectionSettings, + expectedCsvLines); + } + } + finally + { + FileOperations.DeleteFile(filePath); + } + } + + private void Serialize_File( + MidiFile midiFile, + string filePath, + CsvSerializationSettings settings, + ObjectType objectType, + ObjectDetectionSettings objectDetectionSettings, + string[] expectedCsvLines) + { + midiFile.SerializeToCsv(filePath, true, settings, objectType, objectDetectionSettings); + var csvLines = GetCsvLines(filePath); + + CollectionAssert.AreEqual(expectedCsvLines, csvLines, "Invalid CSV lines for file."); + } + + private void Serialize_ChunksFromFile( + MidiFile midiFile, + string filePath, + CsvSerializationSettings settings, + ObjectType objectType, + ObjectDetectionSettings objectDetectionSettings, + string[] expectedCsvLines) + { + var tempoMap = midiFile.GetTempoMap(); + midiFile.Chunks.SerializeToCsv(filePath, true, tempoMap, settings, objectType, objectDetectionSettings); + var csvLines = GetCsvLines(filePath); + + CollectionAssert.AreEqual( + expectedCsvLines + .Skip(1) + .Select(l => Regex.Replace(l, @"^\d+?", m => $"{int.Parse(m.Value) - 1}")), + csvLines, + "Invalid CSV lines for chunks."); + } + + private void Serialize_ChunkFromFile( + MidiFile midiFile, + string filePath, + CsvSerializationSettings settings, + ObjectType objectType, + ObjectDetectionSettings objectDetectionSettings, + string[] expectedCsvLines) + { + var i = 0; + var tempoMap = midiFile.GetTempoMap(); + + foreach (var chunk in midiFile.Chunks) + { + chunk.SerializeToCsv(filePath, true, tempoMap, settings, objectType, objectDetectionSettings); + var csvLines = GetCsvLines(filePath); + + CollectionAssert.AreEqual( + expectedCsvLines + .Skip(1) + .Where(l => Regex.Match(l, @"^(\d+?)").Value == (i + 1).ToString()) + .Select(l => Regex.Replace(l, @"^\d+?", m => "0")), + csvLines, + $"Invalid CSV lines for chunk {i}."); + + i++; + } + } + + private void Serialize_ObjectsFromFile( + MidiFile midiFile, + string filePath, + CsvSerializationSettings settings, + ObjectType objectType, + ObjectDetectionSettings objectDetectionSettings, + string[] expectedCsvLines) + { + var i = 0; + var tempoMap = midiFile.GetTempoMap(); + var delimiter = settings?.Delimiter ?? ','; + + foreach (var chunk in midiFile.Chunks) + { + var objects = ((TrackChunk)chunk).GetObjects(objectType, objectDetectionSettings); + + objects.SerializeToCsv(filePath, true, tempoMap, settings); + var csvLines = GetCsvLines(filePath); + + CollectionAssert.AreEqual( + expectedCsvLines + .Skip(1) + .Where(l => Regex.Match(l, @"^(\d+?)").Value == (i + 1).ToString()) + .Select(l => Regex.Replace(l, $@"^.+?{delimiter}.+?{delimiter}", m => string.Empty)), + csvLines, + $"Invalid CSV lines for objects of chunk {i}."); + + i++; + } + } + + private static string[] GetCsvLines(string filePath) => FileOperations + .ReadAllFileLines(filePath) + .Select(l => l.Trim()) + .ToArray(); + + #endregion + } +} diff --git a/DryWetMidi.Tests/Tools/TimeAndMidiEvent.cs b/DryWetMidi.Tests/Tools/TimeAndMidiEvent.cs deleted file mode 100644 index cfed6f42f..000000000 --- a/DryWetMidi.Tests/Tools/TimeAndMidiEvent.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Melanchall.DryWetMidi.Core; -using Melanchall.DryWetMidi.Interaction; - -namespace Melanchall.DryWetMidi.Tests.Tools -{ - internal sealed class TimeAndMidiEvent - { - #region Constructor - - public TimeAndMidiEvent(ITimeSpan time, MidiEvent midiEvent) - { - Time = time; - Event = midiEvent; - } - - #endregion - - #region Properties - - public ITimeSpan Time { get; } - - public MidiEvent Event { get; } - - #endregion - } -} diff --git a/DryWetMidi.Tests/Utilities/ChordMethods.cs b/DryWetMidi.Tests/Utilities/ChordMethods.cs index 83fe1f674..4a6d51a17 100644 --- a/DryWetMidi.Tests/Utilities/ChordMethods.cs +++ b/DryWetMidi.Tests/Utilities/ChordMethods.cs @@ -1,5 +1,4 @@ -using System; -using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Common; using Melanchall.DryWetMidi.Interaction; namespace Melanchall.DryWetMidi.Tests.Utilities @@ -8,20 +7,10 @@ public sealed class ChordMethods : LengthedObjectMethods { #region Overrides - public override void SetTime(Chord obj, long time) - { - obj.Time = time; - } - - public override void SetLength(Chord obj, long length) - { - obj.Length = length; - } - public override Chord Create(long time, long length) { - var chord = new Chord(new Note((SevenBitNumber)DryWetMidi.Common.Random.Instance.Next(SevenBitNumber.MaxValue)), - new Note((SevenBitNumber)DryWetMidi.Common.Random.Instance.Next(SevenBitNumber.MaxValue))); + var chord = new Chord(new Note((SevenBitNumber)Random.Instance.Next(SevenBitNumber.MaxValue)), + new Note((SevenBitNumber)Random.Instance.Next(SevenBitNumber.MaxValue))); chord.Time = time; chord.Length = length; diff --git a/DryWetMidi.Tests/Utilities/LengthedObjectMethods.cs b/DryWetMidi.Tests/Utilities/LengthedObjectMethods.cs index e851736be..508dc92c3 100644 --- a/DryWetMidi.Tests/Utilities/LengthedObjectMethods.cs +++ b/DryWetMidi.Tests/Utilities/LengthedObjectMethods.cs @@ -1,45 +1,20 @@ -using System.Collections.Generic; -using Melanchall.DryWetMidi.Interaction; +using Melanchall.DryWetMidi.Interaction; namespace Melanchall.DryWetMidi.Tests.Utilities { - public abstract class LengthedObjectMethods : TimedObjectMethods + public abstract class LengthedObjectMethods where TObject : ILengthedObject { #region Methods - public void SetLength(TObject obj, ITimeSpan length, ITimeSpan time, TempoMap tempoMap) - { - var convertedTime = TimeConverter.ConvertFrom(time, tempoMap); - SetLength(obj, LengthConverter.ConvertFrom(length, convertedTime, tempoMap)); - } - public TObject Create(ITimeSpan time, ITimeSpan length, TempoMap tempoMap) { var convertedTime = TimeConverter.ConvertFrom(time, tempoMap); return Create(convertedTime, LengthConverter.ConvertFrom(length, convertedTime, tempoMap)); } - public IEnumerable CreateCollection(TempoMap tempoMap, params string[] timeAndLengthStrings) - { - var result = new List(); - - foreach (var timeAndLengthString in timeAndLengthStrings) - { - var parts = timeAndLengthString.Split(';'); - var time = TimeSpanUtilities.Parse(parts[0]); - var length = TimeSpanUtilities.Parse(parts[1]); - - result.Add(Create(time, length, tempoMap)); - } - - return result; - } - public abstract TObject Create(long time, long length); - public abstract void SetLength(TObject obj, long length); - #endregion } } diff --git a/DryWetMidi.Tests/Utilities/MidiAsserts.cs b/DryWetMidi.Tests/Utilities/MidiAsserts.cs index 71949e96e..ac35ee4e5 100644 --- a/DryWetMidi.Tests/Utilities/MidiAsserts.cs +++ b/DryWetMidi.Tests/Utilities/MidiAsserts.cs @@ -112,11 +112,11 @@ public static void AreEqual(EventsCollection eventsCollection1, EventsCollection Assert.IsTrue(areEqual, $"{message} {eventsComparingMessage}"); } - public static void AreEqual(TrackChunk trackChunk1, TrackChunk trackChunk2, bool compareDeltaTimes, string message = null) + public static void AreEqual(MidiChunk midiChunk1, MidiChunk midiChunk2, bool compareDeltaTimes, string message = null) { var areEqual = MidiChunkEquality.Equals( - trackChunk1, - trackChunk2, + midiChunk1, + midiChunk2, new MidiChunkEqualityCheckSettings { EventEqualityCheckSettings = new MidiEventEqualityCheckSettings diff --git a/DryWetMidi.Tests/Utilities/NoteMethods.cs b/DryWetMidi.Tests/Utilities/NoteMethods.cs index fb99fdf58..c3d090c4a 100644 --- a/DryWetMidi.Tests/Utilities/NoteMethods.cs +++ b/DryWetMidi.Tests/Utilities/NoteMethods.cs @@ -1,5 +1,4 @@ -using System; -using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Common; using Melanchall.DryWetMidi.Interaction; namespace Melanchall.DryWetMidi.Tests.Utilities @@ -8,19 +7,9 @@ public sealed class NoteMethods : LengthedObjectMethods { #region Overrides - public override void SetTime(Note obj, long time) - { - obj.Time = time; - } - - public override void SetLength(Note obj, long length) - { - obj.Length = length; - } - public override Note Create(long time, long length) { - return new Note((SevenBitNumber)DryWetMidi.Common.Random.Instance.Next(SevenBitNumber.MaxValue), length, time); + return new Note((SevenBitNumber)Random.Instance.Next(SevenBitNumber.MaxValue), length, time); } #endregion diff --git a/DryWetMidi.Tests/Utilities/TimedObjectMethods.cs b/DryWetMidi.Tests/Utilities/TimedObjectMethods.cs deleted file mode 100644 index baa2a7131..000000000 --- a/DryWetMidi.Tests/Utilities/TimedObjectMethods.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Melanchall.DryWetMidi.Interaction; - -namespace Melanchall.DryWetMidi.Tests.Utilities -{ - public abstract class TimedObjectMethods - where TObject : ITimedObject - { - #region Methods - - public void SetTime(TObject obj, ITimeSpan time, TempoMap tempoMap) - { - SetTime(obj, TimeConverter.ConvertFrom(time, tempoMap)); - } - - public TObject Clone(TObject obj) - { - return (TObject)obj.Clone(); - } - - public abstract void SetTime(TObject obj, long time); - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/Common/CsvSettings.cs b/DryWetMidi/Tools/CsvConverter/Common/CsvSettings.cs deleted file mode 100644 index a946d986a..000000000 --- a/DryWetMidi/Tools/CsvConverter/Common/CsvSettings.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Melanchall.DryWetMidi.Common; - -namespace Melanchall.DryWetMidi.Tools -{ - /// - /// Common CSV reading/writing settings. - /// - public sealed class CsvSettings - { - #region Fields - - private int _bufferSize = 1024; - - #endregion - - #region Properties - - /// - /// Gets or sets delimiter used to separate values in CSV representation. The default value is comma. - /// - public char CsvDelimiter { get; set; } = ','; - - /// - /// Gets or sets the size of buffer used to read/write CSV data. - /// - /// Value is zero or negative. - public int IoBufferSize - { - get { return _bufferSize; } - set - { - ThrowIfArgument.IsNonpositive(nameof(value), value, "Buffer size is zero or negative."); - - _bufferSize = value; - } - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/Common/CsvWriter.cs b/DryWetMidi/Tools/CsvConverter/Common/CsvWriter.cs deleted file mode 100644 index b3d23647b..000000000 --- a/DryWetMidi/Tools/CsvConverter/Common/CsvWriter.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Melanchall.DryWetMidi.Tools -{ - internal sealed class CsvWriter : IDisposable - { - #region Fields - - private readonly StreamWriter _streamWriter; - private readonly char _delimiter; - - #endregion - - #region Constructor - - public CsvWriter(Stream stream, CsvSettings settings) - { - _streamWriter = new StreamWriter(stream, new UTF8Encoding(false, true), 1024, true); - _delimiter = settings.CsvDelimiter; - } - - #endregion - - #region Methods - - public void WriteRecord(IEnumerable values) - { - _streamWriter.WriteLine(string.Join(_delimiter.ToString(), values)); - } - - #endregion - - #region IDisposable - - private bool _disposed = false; - - void Dispose(bool disposing) - { - if (_disposed) - return; - - if (disposing) - _streamWriter.Dispose(); - - _disposed = true; - } - - public void Dispose() - { - Dispose(true); - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/CsvConverter.cs b/DryWetMidi/Tools/CsvConverter/CsvConverter.cs deleted file mode 100644 index 59d340fb9..000000000 --- a/DryWetMidi/Tools/CsvConverter/CsvConverter.cs +++ /dev/null @@ -1,338 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Core; -using Melanchall.DryWetMidi.Interaction; - -namespace Melanchall.DryWetMidi.Tools -{ - /// - /// Provides methods to convert MIDI objects to CSV representation and vice versa. - /// - public sealed class CsvConverter - { - #region Methods - - /// - /// Converts the specified to CSV representation and writes it to a file. - /// - /// to convert to CSV. - /// Path of the output CSV file. - /// If true and file specified by already - /// exists it will be overwritten; if false and the file exists, exception will be thrown. - /// Settings according to which must be converted. - /// Pass null to use default settings. - /// is null. - /// is a zero-length string, - /// contains only white space, or contains one or more invalid characters as defined by - /// . - /// is null. - /// 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. - /// The specified path is invalid, (for example, - /// it is on an unmapped drive). - /// An I/O error occurred while writing the file. - /// is in an invalid format. - /// - /// One of the following errors occurred: - /// - /// - /// This operation is not supported on the current platform. - /// - /// - /// specified a directory. - /// - /// - /// The caller does not have the required permission. - /// - /// - /// - 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); - } - } - - /// - /// Converts the specified to CSV representation and writes it to a stream. - /// - /// to convert to CSV. - /// Stream to write CSV representation to. - /// Settings according to which must be converted. - /// Pass null to use default settings. - /// - /// One of the following errors occurred: - /// - /// - /// is null. - /// - /// - /// is null. - /// - /// - /// - /// doesn't support writing. - /// An I/O error occurred while writing to the stream. - /// is disposed. - 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()); - } - - /// - /// Converts CSV representation of a MIDI file to reading CSV data from a file. - /// - /// Path of the file with CSV representation of a MIDI file. - /// Settings according to which CSV data must be converted. Pass null to - /// use default settings. - /// An instance of the representing a MIDI file written in CSV format. - /// is a zero-length string, - /// contains only white space, or contains one or more invalid characters as defined by - /// . - /// is null. - /// 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. - /// The specified path is invalid, (for example, - /// it is on an unmapped drive). - /// An I/O error occurred while reading the file. - /// is in an invalid format. - /// - /// One of the following errors occurred: - /// - /// - /// This operation is not supported on the current platform. - /// - /// - /// specified a directory. - /// - /// - /// The caller does not have the required permission. - /// - /// - /// - public MidiFile ConvertCsvToMidiFile(string filePath, MidiFileCsvConversionSettings settings = null) - { - using (var fileStream = FileUtilities.OpenFileForRead(filePath)) - { - return ConvertCsvToMidiFile(fileStream, settings); - } - } - - /// - /// Converts CSV representation of a MIDI file to reading CSV data from a stream. - /// - /// Stream to read MIDI file from. - /// Settings according to which CSV data must be converted. Pass null to - /// use default settings. - /// An instance of the representing a MIDI file written in CSV format. - /// is null. - /// doesn't support reading. - /// An I/O error occurred while reading from the stream. - /// is disposed. - 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()); - } - - /// - /// Converts the specified collection of to CSV representation and writes it to a file. - /// - /// Collection of to convert to CSV. - /// Path of the output CSV file. - /// Tempo map used to convert to CSV. - /// If true and file specified by already - /// exists it will be overwritten; if false and the file exists, exception will be thrown. - /// Settings according to which must be converted. - /// Pass null to use default settings. - /// - /// One of the following errors occurred: - /// - /// - /// is null. - /// - /// - /// is null. - /// - /// - /// - /// is a zero-length string, - /// contains only white space, or contains one or more invalid characters as defined by - /// . - /// is null. - /// 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. - /// The specified path is invalid, (for example, - /// it is on an unmapped drive). - /// An I/O error occurred while writing the file. - /// is in an invalid format. - /// - /// One of the following errors occurred: - /// - /// - /// This operation is not supported on the current platform. - /// - /// - /// specified a directory. - /// - /// - /// The caller does not have the required permission. - /// - /// - /// - public void ConvertNotesToCsv(IEnumerable notes, string filePath, TempoMap tempoMap, bool overwriteFile = false, NoteCsvConversionSettings settings = null) - { - ThrowIfArgument.IsNull(nameof(notes), notes); - ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); - - using (var fileStream = FileUtilities.OpenFileForWrite(filePath, overwriteFile)) - { - ConvertNotesToCsv(notes, fileStream, tempoMap, settings); - } - } - - /// - /// Converts the specified collection of to CSV representation and writes it to a stream. - /// - /// Collection of to convert to CSV. - /// Stream to write CSV representation to. - /// Tempo map used to convert to CSV. - /// Settings according to which must be converted. - /// Pass null to use default settings. - /// - /// One of the following errors occurred: - /// - /// - /// is null. - /// - /// - /// is null. - /// - /// - /// is null. - /// - /// - /// - /// doesn't support writing. - /// An I/O error occurred while writing to the stream. - /// is disposed. - public void ConvertNotesToCsv(IEnumerable notes, Stream stream, TempoMap tempoMap, NoteCsvConversionSettings settings = null) - { - ThrowIfArgument.IsNull(nameof(notes), notes); - ThrowIfArgument.IsNull(nameof(stream), stream); - ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); - - if (!stream.CanWrite) - throw new ArgumentException("Stream doesn't support writing.", nameof(stream)); - - NotesToCsvConverter.ConvertToCsv(notes, stream, tempoMap, settings ?? new NoteCsvConversionSettings()); - } - - /// - /// Converts CSV representation of notes to collection of reading CSV data from a file. - /// - /// Path of the file with CSV representation of notes. - /// Tempo map used to convert notes from CSV. - /// Settings according to which CSV data must be converted. Pass null to - /// use default settings. - /// Collection of representing notes written in CSV format. - /// is a zero-length string, - /// contains only white space, or contains one or more invalid characters as defined by - /// . - /// - /// One of the following errors occurred: - /// - /// - /// is null. - /// - /// - /// is null. - /// - /// - /// - /// 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. - /// The specified path is invalid, (for example, - /// it is on an unmapped drive). - /// An I/O error occurred while reading the file. - /// is in an invalid format. - /// - /// One of the following errors occurred: - /// - /// - /// This operation is not supported on the current platform. - /// - /// - /// specified a directory. - /// - /// - /// The caller does not have the required permission. - /// - /// - /// - public IEnumerable ConvertCsvToNotes(string filePath, TempoMap tempoMap, NoteCsvConversionSettings settings = null) - { - ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); - - using (var fileStream = FileUtilities.OpenFileForRead(filePath)) - { - return ConvertCsvToNotes(fileStream, tempoMap, settings).ToList(); - } - } - - /// - /// Converts CSV representation of notes to collection of reading CSV data from a stream. - /// - /// Stream to read notes from. - /// Tempo map used to convert notes from CSV. - /// Settings according to which CSV data must be converted. Pass null to - /// use default settings. - /// Collection of representing notes written in CSV format. - /// - /// One of the following errors occurred: - /// - /// - /// is null. - /// - /// - /// is null. - /// - /// - /// - /// doesn't support reading. - /// An I/O error occurred while reading from the stream. - /// is disposed. - public IEnumerable ConvertCsvToNotes(Stream stream, TempoMap tempoMap, NoteCsvConversionSettings settings = null) - { - ThrowIfArgument.IsNull(nameof(stream), stream); - ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); - - if (!stream.CanRead) - throw new ArgumentException("Stream doesn't support reading.", nameof(stream)); - - return CsvToNotesConverter.ConvertToNotes(stream, tempoMap, settings ?? new NoteCsvConversionSettings()); - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/CsvUtilities.cs b/DryWetMidi/Tools/CsvConverter/CsvUtilities.cs deleted file mode 100644 index ab2f73249..000000000 --- a/DryWetMidi/Tools/CsvConverter/CsvUtilities.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Melanchall.DryWetMidi.Tools -{ - internal static class CsvUtilities - { - #region Constants - - private const char Quote = '"'; - private const string QuoteString = "\""; - private const string DoubleQuote = "\"\""; - - #endregion - - #region Methods - - public static string EscapeString(string input) - { - return $"{Quote}{input.Replace(QuoteString, DoubleQuote)}{Quote}"; - } - - public static string UnescapeString(string input) - { - if (input.Length > 1 && input[0] == '\"' && input[input.Length - 1] == '\"') - input = input.Substring(1, input.Length - 2); - - return input.Replace(DoubleQuote, QuoteString); - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/CsvToMidiFileConverter.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/CsvToMidiFileConverter.cs deleted file mode 100644 index 9c5eb8dd1..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/CsvToMidiFileConverter.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Core; -using Melanchall.DryWetMidi.Interaction; - -namespace Melanchall.DryWetMidi.Tools -{ - internal static class CsvToMidiFileConverter - { - #region Constants - - private static readonly Dictionary RecordLabelsToRecordTypes = - new Dictionary(StringComparer.OrdinalIgnoreCase) - { - [RecordLabels.File.Header] = RecordType.Header, - [RecordLabels.Note] = RecordType.Note - }; - - #endregion - - #region Methods - - public static MidiFile ConvertToMidiFile(Stream stream, MidiFileCsvConversionSettings settings) - { - var midiFile = new MidiFile(); - var events = new Dictionary>(); - - using (var csvReader = new CsvReader(stream, settings.CsvSettings)) - { - var lineNumber = 0; - Record record; - - while ((record = ReadRecord(csvReader, 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 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; - } - - private static void AddTimedEvents(Dictionary> eventsMap, - int trackChunkNumber, - params TimedMidiEvent[] events) - { - List timedMidiEvents; - if (!eventsMap.TryGetValue(trackChunkNumber, out timedMidiEvents)) - eventsMap.Add(trackChunkNumber, timedMidiEvents = new List()); - - timedMidiEvents.AddRange(events); - } - - private static TempoMap GetTempoMap(IEnumerable timedMidiEvents, TimeDivision timeDivision) - { - using (var tempoMapManager = new TempoMapManager(timeDivision)) - { - var setTempoEvents = timedMidiEvents.Where(e => e.Event is SetTempoEvent) - .OrderBy(e => e.Time, new TimeSpanComparer()); - foreach (var timedMidiEvent in setTempoEvents) - { - var setTempoEvent = (SetTempoEvent)timedMidiEvent.Event; - tempoMapManager.SetTempo(timedMidiEvent.Time, - new Tempo(setTempoEvent.MicrosecondsPerQuarterNote)); - } - - var timeSignatureEvents = timedMidiEvents.Where(e => e.Event is TimeSignatureEvent) - .OrderBy(e => e.Time, new TimeSpanComparer()); - foreach (var timedMidiEvent in timeSignatureEvents) - { - var timeSignatureEvent = (TimeSignatureEvent)timedMidiEvent.Event; - tempoMapManager.SetTimeSignature(timedMidiEvent.Time, - new TimeSignature(timeSignatureEvent.Numerator, timeSignatureEvent.Denominator)); - } - - return tempoMapManager.TempoMap; - } - } - - private static RecordType? GetRecordType(string recordType, MidiFileCsvConversionSettings settings) - { - var eventsNames = EventsNamesProvider.Get(); - - RecordType result; - if (RecordLabelsToRecordTypes.TryGetValue(recordType, out result)) - return result; - - if (eventsNames.Contains(recordType, StringComparer.OrdinalIgnoreCase)) - return RecordType.Event; - - return null; - } - - private static HeaderChunk ParseHeader(Record record, MidiFileCsvConversionSettings settings) - { - var parameters = record.Parameters; - - var format = default(MidiFileFormat?); - var timeDivision = default(short); - - 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."); - - return new HeaderChunk - { - FileFormat = format != null ? (ushort)format.Value : ushort.MaxValue, - TimeDivision = TimeDivisionFactory.GetTimeDivision(timeDivision) - }; - } - - 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); - - try - { - return eventParser(record.Parameters, settings); - } - catch (FormatException ex) - { - CsvError.ThrowBadFormat(record.LineNumber, "Invalid format of event record.", ex); - return null; - } - } - - 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; - 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; - } - - private static Record ReadRecord(CsvReader csvReader, 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; - 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); - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventParser.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventParser.cs deleted file mode 100644 index eb72dec5f..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventParser.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Melanchall.DryWetMidi.Core; - -namespace Melanchall.DryWetMidi.Tools -{ - internal delegate MidiEvent EventParser(string[] parameters, MidiFileCsvConversionSettings settings); -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventsNamesProvider.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventsNamesProvider.cs deleted file mode 100644 index d3ec604cd..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventsNamesProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Linq; -using System.Reflection; - -namespace Melanchall.DryWetMidi.Tools -{ - internal static class EventsNamesProvider - { - #region Constants - - - private static readonly string[] EventsNames = typeof(RecordLabels.Events) - .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) - .Where(fi => fi.IsLiteral && !fi.IsInitOnly) - .Select(fi => fi.GetValue(null).ToString()) - .ToArray(); - - #endregion - - #region Methods - - public static string[] Get() - { - return EventsNames; - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/ParameterParser.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/ParameterParser.cs deleted file mode 100644 index 9d70603f8..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/ParameterParser.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Melanchall.DryWetMidi.Tools -{ - internal delegate object ParameterParser(string parameter, MidiFileCsvConversionSettings settings); -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/Record.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/Record.cs deleted file mode 100644 index df24619c3..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/Record.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Melanchall.DryWetMidi.Interaction; - -namespace Melanchall.DryWetMidi.Tools -{ - internal sealed class Record - { - #region Constructor - - public Record(int lineNumber, int? trackNumber, ITimeSpan time, string recordType, string[] parameters) - { - LineNumber = lineNumber; - TrackNumber = trackNumber; - Time = time; - RecordType = recordType; - Parameters = parameters; - } - - #endregion - - #region Properties - - public int LineNumber { get; } - - public int? TrackNumber { get; } - - public ITimeSpan Time { get; } - - public string RecordType { get; } - - public string[] Parameters { get; } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/RecordType.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/RecordType.cs deleted file mode 100644 index 991e0a0e0..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/RecordType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Melanchall.DryWetMidi.Tools -{ - internal enum RecordType - { - Header, - TrackChunkStart, - TrackChunkEnd, - FileEnd, - Event, - Note - } -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/MidiFileCsvConversionSettings.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/MidiFileCsvConversionSettings.cs deleted file mode 100644 index 72efe8a3a..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/MidiFileCsvConversionSettings.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.ComponentModel; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Core; -using Melanchall.DryWetMidi.Interaction; - -namespace Melanchall.DryWetMidi.Tools -{ - /// - /// Settings according to which must be read from or written to - /// CSV representation. - /// - public sealed class MidiFileCsvConversionSettings - { - #region Fields - - private TimeSpanType _timeType = TimeSpanType.Midi; - private TimeSpanType _noteLengthType = TimeSpanType.Midi; - private NoteFormat _noteFormat = NoteFormat.Events; - private NoteNumberFormat _noteNumberFormat = NoteNumberFormat.NoteNumber; - - #endregion - - #region Properties - - /// - /// Gets or sets format of timestamps inside CSV representation. The default value is - /// - /// specified an invalid value. - public TimeSpanType TimeType - { - get { return _timeType; } - set - { - ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); - - _timeType = value; - } - } - - /// - /// Gets or sets the type of a note length (metric, bar/beat and so on) which should be used to - /// write to or read from CSV. The default value is . - /// - /// specified an invalid value. - public TimeSpanType NoteLengthType - { - get { return _noteLengthType; } - set - { - ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); - - _noteLengthType = value; - } - } - - /// - /// Gets or sets the format which should be used to write notes to or read them from CSV. - /// The default value is . - /// - /// specified an invalid value. - public NoteFormat NoteFormat - { - get { return _noteFormat; } - set - { - ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); - - _noteFormat = value; - } - } - - /// - /// Gets or sets the format which should be used to write a note's number to or read it from CSV. - /// The default value is . - /// - /// specified an invalid value. - public NoteNumberFormat NoteNumberFormat - { - get { return _noteNumberFormat; } - set - { - ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); - - _noteNumberFormat = value; - } - } - - /// - /// Gets common CSV settings. - /// - public CsvSettings CsvSettings { get; } = new CsvSettings(); - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/NoteFormat.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/NoteFormat.cs deleted file mode 100644 index 8794bbba6..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/NoteFormat.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Melanchall.DryWetMidi.Tools -{ - /// - /// The format which should be used to write notes to or read them from CSV. - /// - public enum NoteFormat - { - /// - /// Notes are presented in CSV as note objects. - /// - Note, - - /// - /// Notes are presented in CSV as Note On/Note Off events. - /// - Events - } -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/RecordLabels.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/RecordLabels.cs deleted file mode 100644 index a78c8a204..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/RecordLabels.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Melanchall.DryWetMidi.Tools -{ - internal static class RecordLabels - { - public static class File - { - public const string Header = "Header"; - } - - public static class Events - { - public const string SequenceTrackName = "Sequence/Track Name"; - public const string CopyrightNotice = "Copyright Notice"; - public const string InstrumentName = "Instrument Name"; - public const string Marker = "Marker"; - public const string CuePoint = "Cue Point"; - public const string Lyric = "Lyric"; - public const string Text = "Text"; - public const string SequenceNumber = "Sequence Number"; - public const string PortPrefix = "Port Prefix"; - public const string ChannelPrefix = "Channel Prefix"; - public const string TimeSignature = "Time Signature"; - public const string KeySignature = "Key Signature"; - public const string SetTempo = "Set Tempo"; - public const string SmpteOffset = "SMPTE Offset"; - public const string SequencerSpecific = "Sequencer Specific"; - public const string UnknownMeta = "Unknown Meta"; - public const string NoteOn = "Note On"; - public const string NoteOff = "Note Off"; - public const string PitchBend = "Pitch Bend"; - public const string ControlChange = "Control Change"; - public const string ProgramChange = "Program Change"; - public const string ChannelAftertouch = "Channel Aftertouch"; - public const string NoteAftertouch = "Note Aftertouch"; - public const string SysExCompleted = "System Exclusive"; - public const string SysExIncompleted = "System Exclusive Packet"; - } - - public const string Note = "Note"; - } -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventNameGetter.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventNameGetter.cs deleted file mode 100644 index 5d45a1ed1..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventNameGetter.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Melanchall.DryWetMidi.Core; - -namespace Melanchall.DryWetMidi.Tools -{ - internal delegate string EventNameGetter(MidiEvent midiEvent); -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventNameGetterProvider.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventNameGetterProvider.cs deleted file mode 100644 index 20ff1aacf..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventNameGetterProvider.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using Melanchall.DryWetMidi.Core; - -namespace Melanchall.DryWetMidi.Tools -{ - internal static class EventNameGetterProvider - { - #region Constants - - private static readonly Dictionary EventsTypes = - new Dictionary - { - [typeof(SequenceTrackNameEvent)] = GetType(RecordLabels.Events.SequenceTrackName), - [typeof(CopyrightNoticeEvent)] = GetType(RecordLabels.Events.CopyrightNotice), - [typeof(InstrumentNameEvent)] = GetType(RecordLabels.Events.InstrumentName), - [typeof(MarkerEvent)] = GetType(RecordLabels.Events.Marker), - [typeof(CuePointEvent)] = GetType(RecordLabels.Events.CuePoint), - [typeof(LyricEvent)] = GetType(RecordLabels.Events.Lyric), - [typeof(TextEvent)] = GetType(RecordLabels.Events.Text), - [typeof(SequenceNumberEvent)] = GetType(RecordLabels.Events.SequenceNumber), - [typeof(PortPrefixEvent)] = GetType(RecordLabels.Events.PortPrefix), - [typeof(ChannelPrefixEvent)] = GetType(RecordLabels.Events.ChannelPrefix), - [typeof(TimeSignatureEvent)] = GetType(RecordLabels.Events.TimeSignature), - [typeof(KeySignatureEvent)] = GetType(RecordLabels.Events.KeySignature), - [typeof(SetTempoEvent)] = GetType(RecordLabels.Events.SetTempo), - [typeof(SmpteOffsetEvent)] = GetType(RecordLabels.Events.SmpteOffset), - [typeof(SequencerSpecificEvent)] = GetType(RecordLabels.Events.SequencerSpecific), - [typeof(UnknownMetaEvent)] = GetType(RecordLabels.Events.UnknownMeta), - [typeof(NoteOnEvent)] = GetType(RecordLabels.Events.NoteOn), - [typeof(NoteOffEvent)] = GetType(RecordLabels.Events.NoteOff), - [typeof(PitchBendEvent)] = GetType(RecordLabels.Events.PitchBend), - [typeof(ControlChangeEvent)] = GetType(RecordLabels.Events.ControlChange), - [typeof(ProgramChangeEvent)] = GetType(RecordLabels.Events.ProgramChange), - [typeof(ChannelAftertouchEvent)] = GetType(RecordLabels.Events.ChannelAftertouch), - [typeof(NoteAftertouchEvent)] = GetType(RecordLabels.Events.NoteAftertouch), - [typeof(NormalSysExEvent)] = GetSysExType(RecordLabels.Events.SysExCompleted, - RecordLabels.Events.SysExIncompleted), - [typeof(EscapeSysExEvent)] = GetSysExType(RecordLabels.Events.SysExCompleted, - RecordLabels.Events.SysExIncompleted) - }; - - #endregion - - #region Methods - - public static EventNameGetter Get(Type eventType) - { - return EventsTypes[eventType]; - } - - private static EventNameGetter GetType(string type) - { - return e => type; - } - - private static EventNameGetter GetSysExType(string completedType, string incompletedType) - { - return e => - { - var sysExEvent = (SysExEvent)e; - return sysExEvent.Completed ? completedType : incompletedType; - }; - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventParametersGetter.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventParametersGetter.cs deleted file mode 100644 index bcdd8bed9..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventParametersGetter.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Melanchall.DryWetMidi.Core; - -namespace Melanchall.DryWetMidi.Tools -{ - internal delegate object[] EventParametersGetter(MidiEvent midiEvent, MidiFileCsvConversionSettings settings); -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventParametersGetterProvider.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventParametersGetterProvider.cs deleted file mode 100644 index 12e78ddb7..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventParametersGetterProvider.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Core; - -namespace Melanchall.DryWetMidi.Tools -{ - internal static class EventParametersGetterProvider - { - #region Constants - - private static readonly Dictionary EventsParametersGetters = - new Dictionary - { - [typeof(SequenceTrackNameEvent)] = GetParameters((e, s) => e.Text), - [typeof(CopyrightNoticeEvent)] = GetParameters((e, s) => e.Text), - [typeof(InstrumentNameEvent)] = GetParameters((e, s) => e.Text), - [typeof(MarkerEvent)] = GetParameters((e, s) => e.Text), - [typeof(CuePointEvent)] = GetParameters((e, s) => e.Text), - [typeof(LyricEvent)] = GetParameters((e, s) => e.Text), - [typeof(TextEvent)] = GetParameters((e, s) => e.Text), - [typeof(SequenceNumberEvent)] = GetParameters((e, s) => e.Number), - [typeof(PortPrefixEvent)] = GetParameters((e, s) => e.Port), - [typeof(ChannelPrefixEvent)] = GetParameters((e, s) => e.Channel), - [typeof(TimeSignatureEvent)] = GetParameters((e, s) => e.Numerator, - (e, s) => e.Denominator, - (e, s) => e.ClocksPerClick, - (e, s) => e.ThirtySecondNotesPerBeat), - [typeof(KeySignatureEvent)] = GetParameters((e, s) => e.Key, - (e, s) => e.Scale), - [typeof(SetTempoEvent)] = GetParameters((e, s) => e.MicrosecondsPerQuarterNote), - [typeof(SmpteOffsetEvent)] = GetParameters((e, s) => SmpteData.GetFormatAndHours(e.Format, e.Hours), - (e, s) => e.Minutes, - (e, s) => e.Seconds, - (e, s) => e.Frames, - (e, s) => e.SubFrames), - [typeof(SequencerSpecificEvent)] = GetParameters((e, s) => e.Data.Length, - (e, s) => e.Data), - [typeof(UnknownMetaEvent)] = GetParameters((e, s) => e.StatusByte, - (e, s) => e.Data.Length, - (e, s) => e.Data), - [typeof(NoteOnEvent)] = GetParameters((e, s) => e.Channel, - (e, s) => FormatNoteNumber(e.NoteNumber, s), - (e, s) => e.Velocity), - [typeof(NoteOffEvent)] = GetParameters((e, s) => e.Channel, - (e, s) => FormatNoteNumber(e.NoteNumber, s), - (e, s) => e.Velocity), - [typeof(PitchBendEvent)] = GetParameters((e, s) => e.Channel, - (e, s) => e.PitchValue), - [typeof(ControlChangeEvent)] = GetParameters((e, s) => e.Channel, - (e, s) => e.ControlNumber, - (e, s) => e.ControlValue), - [typeof(ProgramChangeEvent)] = GetParameters((e, s) => e.Channel, - (e, s) => e.ProgramNumber), - [typeof(ChannelAftertouchEvent)] = GetParameters((e, s) => e.Channel, - (e, s) => e.AftertouchValue), - [typeof(NoteAftertouchEvent)] = GetParameters((e, s) => e.Channel, - (e, s) => FormatNoteNumber(e.NoteNumber, s), - (e, s) => e.AftertouchValue), - [typeof(NormalSysExEvent)] = GetParameters((e, s) => e.Data.Length, - (e, s) => e.Data), - [typeof(EscapeSysExEvent)] = GetParameters((e, s) => e.Data.Length, - (e, s) => e.Data) - }; - - #endregion - - #region Methods - - public static EventParametersGetter Get(Type eventType) - { - return EventsParametersGetters[eventType]; - } - - private static EventParametersGetter GetParameters(params Func[] parametersGetters) - where T : MidiEvent - { - return (e, s) => parametersGetters.Select(g => g((T)e, s)).ToArray(); - } - - private static object FormatNoteNumber(SevenBitNumber noteNumber, MidiFileCsvConversionSettings settings) - { - return NoteCsvConversionUtilities.FormatNoteNumber(noteNumber, settings.NoteNumberFormat); - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/MidiFileToCsvConverter.cs b/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/MidiFileToCsvConverter.cs deleted file mode 100644 index d6208a13d..000000000 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/MidiFileToCsvConverter.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Melanchall.DryWetMidi.Core; -using Melanchall.DryWetMidi.Interaction; - -namespace Melanchall.DryWetMidi.Tools -{ - internal static class MidiFileToCsvConverter - { - #region Methods - - 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()) - { - var time = 0L; - var timedEvents = trackChunk.Events.GetTimedEventsLazy(null, false); - var timedObjects = settings.NoteFormat == NoteFormat.Events - ? (IEnumerable)timedEvents - : timedEvents.GetObjects(ObjectType.TimedEvent | ObjectType.Note); - - 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); - } - } - - trackNumber++; - } - } - } - - 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, - RecordLabels.Note, - settings, - tempoMap, - note.Channel, - formattedNote, - formattedLength, - note.Velocity, - note.OffVelocity); - } - - 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); - var recordType = eventNameGetter(midiEvent); - - var eventParametersGetter = EventParametersGetterProvider.Get(eventType); - var recordParameters = eventParametersGetter(midiEvent, settings); - - WriteRecord(csvWriter, - trackNumber, - time, - recordType, - settings, - tempoMap, - recordParameters); - } - - private static void WriteHeader(CsvWriter csvWriter, - MidiFile midiFile, - MidiFileCsvConversionSettings settings, - TempoMap tempoMap) - { - MidiFileFormat? format = null; - try - { - format = midiFile.OriginalFormat; - } - catch { } - - var trackChunksCount = midiFile.GetTrackChunks().Count(); - - WriteRecord( - csvWriter, - null, - null, - RecordLabels.File.Header, - settings, - tempoMap, - format, - midiFile.TimeDivision.ToInt16()); - } - - 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 object[] ProcessParameter(object parameter) - { - if (parameter == null) - return new object[] { string.Empty }; - - var bytes = parameter as byte[]; - if (bytes != null) - return bytes.OfType().ToArray(); - - var s = parameter as string; - if (s != null) - parameter = CsvUtilities.EscapeString(s); - - return new[] { parameter }; - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/Notes/CsvToNotesConverter.cs b/DryWetMidi/Tools/CsvConverter/Notes/CsvToNotesConverter.cs deleted file mode 100644 index ff0b844ab..000000000 --- a/DryWetMidi/Tools/CsvConverter/Notes/CsvToNotesConverter.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Interaction; - -namespace Melanchall.DryWetMidi.Tools -{ - internal static class CsvToNotesConverter - { - #region Methods - - public static IEnumerable ConvertToNotes(Stream stream, TempoMap tempoMap, NoteCsvConversionSettings settings) - { - using (var csvReader = new CsvReader(stream, settings.CsvSettings)) - { - CsvRecord record; - - while ((record = csvReader.ReadRecord()) != null) - { - var values = record.Values; - if (values.Length < 6) - CsvError.ThrowBadFormat(record.LineNumber, "Missing required parameters."); - - ITimeSpan time; - if (!TimeSpanUtilities.TryParse(values[0], settings.TimeType, out time)) - CsvError.ThrowBadFormat(record.LineNumber, "Invalid time."); - - FourBitNumber channel; - if (!FourBitNumber.TryParse(values[1], out channel)) - CsvError.ThrowBadFormat(record.LineNumber, "Invalid channel."); - - SevenBitNumber noteNumber; - if (!TryParseNoteNumber(values[2], settings.NoteNumberFormat, out noteNumber)) - CsvError.ThrowBadFormat(record.LineNumber, "Invalid note number or letter."); - - ITimeSpan length; - if (!TimeSpanUtilities.TryParse(values[3], settings.NoteLengthType, out length)) - CsvError.ThrowBadFormat(record.LineNumber, "Invalid length."); - - SevenBitNumber velocity; - if (!SevenBitNumber.TryParse(values[4], out velocity)) - CsvError.ThrowBadFormat(record.LineNumber, "Invalid velocity."); - - SevenBitNumber offVelocity; - if (!SevenBitNumber.TryParse(values[5], out offVelocity)) - CsvError.ThrowBadFormat(record.LineNumber, "Invalid off velocity."); - - var convertedTime = TimeConverter.ConvertFrom(time, tempoMap); - var convertedLength = LengthConverter.ConvertFrom(length, convertedTime, tempoMap); - - yield return new Note(noteNumber, convertedLength, convertedTime) - { - Channel = channel, - Velocity = velocity, - OffVelocity = offVelocity - }; - } - } - } - - public static bool TryParseNoteNumber(string input, NoteNumberFormat noteNumberFormat, out SevenBitNumber result) - { - result = default(SevenBitNumber); - - switch (noteNumberFormat) - { - case NoteNumberFormat.NoteNumber: - return SevenBitNumber.TryParse(input, out result); - case NoteNumberFormat.Letter: - { - MusicTheory.Note note; - if (!MusicTheory.Note.TryParse(input, out note)) - return false; - - result = note.NoteNumber; - return true; - } - } - - return false; - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/Notes/NoteCsvConversionSettings.cs b/DryWetMidi/Tools/CsvConverter/Notes/NoteCsvConversionSettings.cs deleted file mode 100644 index 487ab97fc..000000000 --- a/DryWetMidi/Tools/CsvConverter/Notes/NoteCsvConversionSettings.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.ComponentModel; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Interaction; - -namespace Melanchall.DryWetMidi.Tools -{ - /// - /// Settings according to which instances of the must be read from or written to - /// CSV representation. - /// - public sealed class NoteCsvConversionSettings - { - #region Fields - - private TimeSpanType _timeType = TimeSpanType.Midi; - private TimeSpanType _noteLengthType = TimeSpanType.Midi; - private NoteNumberFormat _noteNumberFormat = NoteNumberFormat.NoteNumber; - - #endregion - - #region Properties - - /// - /// Gets or sets format of timestamps inside CSV representation. The default value is - /// - /// specified an invalid value. - public TimeSpanType TimeType - { - get { return _timeType; } - set - { - ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); - - _timeType = value; - } - } - - /// - /// Gets or sets the type of a note length (metric, bar/beat and so on) which should be used to - /// write to or read from CSV. The default value is . - /// - /// specified an invalid value. - public TimeSpanType NoteLengthType - { - get { return _noteLengthType; } - set - { - ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); - - _noteLengthType = value; - } - } - - /// - /// Gets or sets the format which should be used to write a note's number to or read it from CSV. - /// The default value is . - /// - /// specified an invalid value. - public NoteNumberFormat NoteNumberFormat - { - get { return _noteNumberFormat; } - set - { - ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); - - _noteNumberFormat = value; - } - } - - /// - /// Gets common CSV settings. - /// - public CsvSettings CsvSettings { get; } = new CsvSettings(); - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/Notes/NoteCsvConversionUtilities.cs b/DryWetMidi/Tools/CsvConverter/Notes/NoteCsvConversionUtilities.cs deleted file mode 100644 index a7e6540ed..000000000 --- a/DryWetMidi/Tools/CsvConverter/Notes/NoteCsvConversionUtilities.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Melanchall.DryWetMidi.Common; - -namespace Melanchall.DryWetMidi.Tools -{ - internal static class NoteCsvConversionUtilities - { - #region Methods - - public static object FormatNoteNumber(SevenBitNumber noteNumber, NoteNumberFormat noteNumberFormat) - { - switch (noteNumberFormat) - { - case NoteNumberFormat.NoteNumber: - return noteNumber; - case NoteNumberFormat.Letter: - return MusicTheory.Note.Get(noteNumber); - } - - return null; - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/Notes/NoteNumberFormat.cs b/DryWetMidi/Tools/CsvConverter/Notes/NoteNumberFormat.cs deleted file mode 100644 index 4dc7a8131..000000000 --- a/DryWetMidi/Tools/CsvConverter/Notes/NoteNumberFormat.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Melanchall.DryWetMidi.Tools -{ - /// - /// Defines how a note's number is presented in CSV representation: either a number or - /// a letter (for example, A#5). - /// - public enum NoteNumberFormat - { - /// - /// A note's number is presented as just a number. - /// - NoteNumber, - - /// - /// A note's number is presented as a letter. - /// - Letter - } -} diff --git a/DryWetMidi/Tools/CsvConverter/Notes/NotesToCsvConverter.cs b/DryWetMidi/Tools/CsvConverter/Notes/NotesToCsvConverter.cs deleted file mode 100644 index 46293983e..000000000 --- a/DryWetMidi/Tools/CsvConverter/Notes/NotesToCsvConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Melanchall.DryWetMidi.Interaction; - -namespace Melanchall.DryWetMidi.Tools -{ - internal static class NotesToCsvConverter - { - #region Methods - - public static void ConvertToCsv(IEnumerable notes, Stream stream, TempoMap tempoMap, NoteCsvConversionSettings settings) - { - using (var csvWriter = new CsvWriter(stream, settings.CsvSettings)) - { - foreach (var note in notes.Where(n => n != null)) - { - csvWriter.WriteRecord(new[] - { - note.TimeAs(settings.TimeType, tempoMap), - note.Channel, - NoteCsvConversionUtilities.FormatNoteNumber(note.NoteNumber, settings.NoteNumberFormat), - note.LengthAs(settings.NoteLengthType, tempoMap), - note.Velocity, - note.OffVelocity - }); - } - } - } - - #endregion - } -} diff --git a/DryWetMidi/Tools/CsvConverter/Common/CsvError.cs b/DryWetMidi/Tools/CsvSerializer/CsvError.cs similarity index 100% rename from DryWetMidi/Tools/CsvConverter/Common/CsvError.cs rename to DryWetMidi/Tools/CsvSerializer/CsvError.cs diff --git a/DryWetMidi/Tools/CsvSerializer/CsvFormattingUtilities.cs b/DryWetMidi/Tools/CsvSerializer/CsvFormattingUtilities.cs new file mode 100644 index 000000000..d17656a00 --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/CsvFormattingUtilities.cs @@ -0,0 +1,60 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Interaction; + +namespace Melanchall.DryWetMidi.Tools +{ + internal static class CsvFormattingUtilities + { + #region Constants + + private const char Quote = '"'; + private const string QuoteString = "\""; + private const string DoubleQuote = "\"\""; + + #endregion + + #region Methods + + public static object FormatTime( + ITimedObject obj, + TimeSpanType timeType, + TempoMap tempoMap) + { + return obj.TimeAs(timeType, tempoMap); + } + + public static object FormatLength( + ILengthedObject obj, + TimeSpanType lengthType, + TempoMap tempoMap) + { + return obj.LengthAs(lengthType, tempoMap); + } + + public static object FormatNoteNumber(SevenBitNumber noteNumber, CsvNoteFormat noteNumberFormat) + { + switch (noteNumberFormat) + { + case CsvNoteFormat.Letter: + return MusicTheory.Note.Get(noteNumber); + } + + return noteNumber; + } + + public static string EscapeString(string input) + { + return $"{Quote}{input.Replace(QuoteString, DoubleQuote)}{Quote}"; + } + + public static string UnescapeString(string input) + { + if (input.Length > 1 && input[0] == '\"' && input[input.Length - 1] == '\"') + input = input.Substring(1, input.Length - 2); + + return input.Replace(DoubleQuote, QuoteString); + } + + #endregion + } +} diff --git a/DryWetMidi/Tools/CsvConverter/Common/CsvReader.cs b/DryWetMidi/Tools/CsvSerializer/FromCsv/CsvReader.cs similarity index 93% rename from DryWetMidi/Tools/CsvConverter/Common/CsvReader.cs rename to DryWetMidi/Tools/CsvSerializer/FromCsv/CsvReader.cs index 37de8c798..1a4a465e0 100644 --- a/DryWetMidi/Tools/CsvConverter/Common/CsvReader.cs +++ b/DryWetMidi/Tools/CsvSerializer/FromCsv/CsvReader.cs @@ -30,11 +30,11 @@ internal sealed class CsvReader : IDisposable #region Constructor - public CsvReader(Stream stream, CsvSettings settings) + public CsvReader(Stream stream, CsvSerializationSettings settings) { - _streamReader = new StreamReader(stream, Encoding.UTF8, true, settings.IoBufferSize, true); - _buffer = new char[settings.IoBufferSize]; - _delimiter = settings.CsvDelimiter; + _streamReader = new StreamReader(stream, Encoding.UTF8, true, settings.ReadWriteBufferSize, true); + _buffer = new char[settings.ReadWriteBufferSize]; + _delimiter = settings.Delimiter; } #endregion @@ -64,7 +64,12 @@ public CsvRecord ReadRecord() line += nextLine; } - return new CsvRecord(oldLineNumber, _currentLineNumber - oldLineNumber, values); + return new CsvRecord(oldLineNumber, _currentLineNumber - oldLineNumber, values.Select(v => CsvFormattingUtilities.UnescapeString(v)).ToArray()); + } + + public void Dispose() + { + Dispose(true); } private string GetFirstLine() @@ -182,11 +187,6 @@ private static bool IsValueClosed(string value) #region IDisposable - public void Dispose() - { - Dispose(true); - } - private void Dispose(bool disposing) { if (_disposed) diff --git a/DryWetMidi/Tools/CsvConverter/Common/CsvRecord.cs b/DryWetMidi/Tools/CsvSerializer/FromCsv/CsvRecord.cs similarity index 100% rename from DryWetMidi/Tools/CsvConverter/Common/CsvRecord.cs rename to DryWetMidi/Tools/CsvSerializer/FromCsv/CsvRecord.cs diff --git a/DryWetMidi/Tools/CsvSerializer/FromCsv/CsvSerializer.Deserialize.cs b/DryWetMidi/Tools/CsvSerializer/FromCsv/CsvSerializer.Deserialize.cs new file mode 100644 index 000000000..40c1c2623 --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/FromCsv/CsvSerializer.Deserialize.cs @@ -0,0 +1,542 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Interaction; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Melanchall.DryWetMidi.Tools +{ + public static partial class CsvSerializer + { + #region Enums + + private enum RecordType + { + Header, + Event, + Note, + } + + #endregion + + #region Constants + + private static readonly Dictionary RecordLabelsToRecordTypes = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [Record.HeaderType] = RecordType.Header, + [Record.NoteType] = RecordType.Note, + }; + + private static readonly string[] EventsNames = Enum + .GetValues(typeof(MidiEventType)) + .Cast() + .Select(t => t.ToString()) + .ToArray(); + + #endregion + + #region Methods + + public static MidiFile DeserializeFileFromCsv( + Stream stream, + CsvSerializationSettings settings = null) + { + ThrowIfArgument.IsNull(nameof(stream), stream); + + if (!stream.CanRead) + throw new ArgumentException("Stream doesn't support reading.", nameof(stream)); + + settings = settings ?? new CsvSerializationSettings(); + + var midiFile = new MidiFile(); + + using (var reader = new CsvReader(stream, settings)) + { + var chunks = ReadChunks( + reader, + settings, + (objects, readChunks) => GetTempoMap(objects, readChunks.OfType().First().TimeDivision), + true); + + var headerChunk = chunks.OfType().First(); + + midiFile.TimeDivision = headerChunk.TimeDivision; + midiFile.Chunks.AddRange(chunks.Where(c => !(c is HeaderChunk))); + } + + return midiFile; + } + + public static MidiFile DeserializeFileFromCsv( + string filePath, + CsvSerializationSettings settings = null) + { + ThrowIfArgument.IsNullOrEmptyString(nameof(filePath), filePath, "File path"); + + using (var fileStream = FileUtilities.OpenFileForRead(filePath)) + { + return DeserializeFileFromCsv(fileStream, settings); + } + } + + public static IEnumerable DeserializeChunksFromCsv( + Stream stream, + TempoMap tempoMap, + CsvSerializationSettings settings = null) + { + ThrowIfArgument.IsNull(nameof(stream), stream); + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + + if (!stream.CanRead) + throw new ArgumentException("Stream doesn't support reading.", nameof(stream)); + + settings = settings ?? new CsvSerializationSettings(); + + using (var reader = new CsvReader(stream, settings)) + { + return ReadChunks( + reader, + settings, + (objects, readChunks) => tempoMap, + true); + } + } + + public static IEnumerable DeserializeChunksFromCsv( + string filePath, + TempoMap tempoMap, + CsvSerializationSettings settings = null) + { + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + ThrowIfArgument.IsNullOrEmptyString(nameof(filePath), filePath, "File path"); + + using (var fileStream = FileUtilities.OpenFileForRead(filePath)) + { + return DeserializeChunksFromCsv(fileStream, tempoMap, settings); + } + } + + public static MidiChunk DeserializeChunkFromCsv( + Stream stream, + TempoMap tempoMap, + CsvSerializationSettings settings = null) + { + ThrowIfArgument.IsNull(nameof(stream), stream); + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + + if (!stream.CanRead) + throw new ArgumentException("Stream doesn't support reading.", nameof(stream)); + + settings = settings ?? new CsvSerializationSettings(); + + using (var reader = new CsvReader(stream, settings)) + { + var chunks = ReadChunks( + reader, + settings, + (objects, readChunks) => tempoMap, + true); + + if (chunks.Count > 1) + CsvError.ThrowBadFormat("More than one chunk."); + + return chunks.First(); + } + } + + public static MidiChunk DeserializeChunkFromCsv( + string filePath, + TempoMap tempoMap, + CsvSerializationSettings settings = null) + { + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + ThrowIfArgument.IsNullOrEmptyString(nameof(filePath), filePath, "File path"); + + using (var fileStream = FileUtilities.OpenFileForRead(filePath)) + { + return DeserializeChunkFromCsv(fileStream, tempoMap, settings); + } + } + + public static IEnumerable DeserializeObjectsFromCsv( + Stream stream, + TempoMap tempoMap, + CsvSerializationSettings settings = null) + { + ThrowIfArgument.IsNull(nameof(stream), stream); + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + + if (!stream.CanRead) + throw new ArgumentException("Stream doesn't support reading.", nameof(stream)); + + settings = settings ?? new CsvSerializationSettings(); + + using (var reader = new CsvReader(stream, settings)) + { + var objects = ReadObjects( + reader, + settings, + tempoMap, + false); + + if (objects.Count > 1) + CsvError.ThrowBadFormat("More than one chunk."); + + return objects.First(); + } + } + + public static IEnumerable DeserializeObjectsFromCsv( + string filePath, + TempoMap tempoMap, + CsvSerializationSettings settings = null) + { + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + ThrowIfArgument.IsNullOrEmptyString(nameof(filePath), filePath, "File path"); + + using (var fileStream = FileUtilities.OpenFileForRead(filePath)) + { + return DeserializeObjectsFromCsv(fileStream, tempoMap, settings); + } + } + + private static ICollection ReadChunks( + CsvReader reader, + CsvSerializationSettings settings, + Func, ICollection, TempoMap> getTempoMap, + bool readChunkId) + { + var result = new List(); + + var objects = new List(); + var chords = new Dictionary, CsvChord>(); + + Record record; + + while ((record = ReadRecord(reader, readChunkId)) != null) + { + var lineNumber = record.CsvRecord.LineNumber; + + var recordType = GetRecordType(record.RecordType); + if (recordType == null) + CsvError.ThrowBadFormat(lineNumber, "Unknown record."); + + if (readChunkId && record.ChunkId != TrackChunk.Id && record.ChunkId != HeaderChunk.Id) + continue; + + switch (recordType) + { + case RecordType.Header: + { + var headerChunk = ParseHeader(record); + result.Add(headerChunk); + } + break; + case RecordType.Event: + { + var csvEvent = ParseEvent(record, settings, readChunkId); + objects.Add(csvEvent); + } + break; + case RecordType.Note: + { + var csvNote = ParseNote(record, settings, readChunkId); + var id = Tuple.Create(csvNote.ChunkIndex, csvNote.ObjectIndex); + + CsvChord csvChord; + if (!chords.TryGetValue(id, out csvChord)) + { + chords.Add( + id, + csvChord = new CsvChord(csvNote.ChunkIndex, csvNote.ChunkId, csvNote.ObjectIndex)); + + objects.Add(csvChord); + } + + csvChord.Notes.Add(csvNote); + } + break; + } + } + + if (!objects.Any()) + return result; + + var tempoMap = getTempoMap(objects, result); + var timedObjects = GetTimedObjects(objects, tempoMap); + + result.AddRange(timedObjects + .Select(obj => obj.ToTrackChunk()) + .ToArray()); + + return result; + } + + private static ICollection> ReadObjects( + CsvReader reader, + CsvSerializationSettings settings, + TempoMap tempoMap, + bool readChunkId) + { + var objects = new List(); + var chords = new Dictionary, CsvChord>(); + + Record record; + + while ((record = ReadRecord(reader, readChunkId)) != null) + { + var lineNumber = record.CsvRecord.LineNumber; + + var recordType = GetRecordType(record.RecordType); + if (recordType == null) + CsvError.ThrowBadFormat(lineNumber, "Unknown record."); + + switch (recordType) + { + case RecordType.Event: + { + var csvEvent = ParseEvent(record, settings, readChunkId); + objects.Add(csvEvent); + } + break; + case RecordType.Note: + { + var csvNote = ParseNote(record, settings, readChunkId); + + CsvChord csvChord; + if (!chords.TryGetValue(Tuple.Create(csvNote.ChunkIndex, csvNote.ObjectIndex), out csvChord)) + { + chords.Add( + Tuple.Create(csvNote.ChunkIndex, csvNote.ObjectIndex), + csvChord = new CsvChord(csvNote.ChunkIndex, csvNote.ChunkId, csvNote.ObjectIndex)); + + objects.Add(csvChord); + } + + csvChord.Notes.Add(csvNote); + } + break; + } + } + + if (!objects.Any()) + return new ITimedObject[0][]; + + return GetTimedObjects(objects, tempoMap); + } + + private static ICollection> GetTimedObjects( + ICollection objects, + TempoMap tempoMap) + { + return objects + .GroupBy(obj => obj.ChunkIndex) + .Select(g => g + .Select(obj => + { + var csvEvent = obj as CsvEvent; + if (csvEvent != null) + return new TimedEvent(csvEvent.Event).SetTime(csvEvent.Time, tempoMap); + + var csvChord = obj as CsvChord; + if (csvChord != null) + { + var notes = csvChord + .Notes + .Select(n => new Note(n.NoteNumber) { Channel = n.Channel, Velocity = n.Velocity, OffVelocity = n.OffVlocity }.SetTime(n.Time, tempoMap).SetLength(n.Length, tempoMap)) + .ToArray(); + + if (notes.Length == 1) + return notes.First(); + + return new Chord(notes); + } + + return (ITimedObject)null; + }) + .Where(obj => obj != null) + .ToArray()) + .ToArray(); + } + + private static Record ReadRecord( + CsvReader csvReader, + bool readChunkId) + { + var record = csvReader.ReadRecord(); + if (record == null) + return null; + + var requiredPartsCount = readChunkId ? 4 : 2; + + var values = record.Values; + if (values.Length < requiredPartsCount) + CsvError.ThrowBadFormat(record.LineNumber, "Missing required parameters."); + + int? chunkIndex = null; + string chunkId = null; + + if (readChunkId) + { + int parsedChunkIndex; + chunkIndex = int.TryParse(values[0], out parsedChunkIndex) + ? (int?)parsedChunkIndex + : null; + + chunkId = values[1]; + if (string.IsNullOrEmpty(chunkId)) + CsvError.ThrowBadFormat(record.LineNumber, "Chunk ID isn't specified."); + } + + int parsedObjectIndex; + var objectIndex = int.TryParse(values[readChunkId ? 2 : 0], out parsedObjectIndex) + ? (int?)parsedObjectIndex + : null; + + var recordType = values[readChunkId ? 3 : 1]; + if (string.IsNullOrEmpty(recordType)) + CsvError.ThrowBadFormat(record.LineNumber, "Record type isn't specified."); + + var parameters = values.Skip(requiredPartsCount).ToArray(); + + return new Record(record, chunkIndex, chunkId, objectIndex, recordType, parameters); + } + + private static RecordType? GetRecordType(string recordType) + { + RecordType result; + if (RecordLabelsToRecordTypes.TryGetValue(recordType, out result)) + return result; + + if (EventsNames.Contains(recordType, StringComparer.OrdinalIgnoreCase)) + return RecordType.Event; + + return null; + } + + private static HeaderChunk ParseHeader(Record record) + { + var parameters = record.Parameters; + + if (parameters.Length < 1) + CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Parameters count is invalid."); + + var timeDivision = default(short); + if (!short.TryParse(parameters[0], out timeDivision)) + CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Invalid time division."); + + return new HeaderChunk + { + TimeDivision = TimeDivisionFactory.GetTimeDivision(timeDivision) + }; + } + + private static CsvEvent ParseEvent( + Record record, + CsvSerializationSettings settings, + bool parseChunkId) + { + //if (record.TrackNumber == null) + // CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Invalid track number."); + // + //if (record.Time == null) + // CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Invalid time."); + + ITimeSpan time; + TimeSpanUtilities.TryParse(record.Parameters.First(), settings.TimeType, out time); + + if (time == null) + CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Invalid time."); + + try + { + var midiEvent = EventParser.ParseEvent( + (MidiEventType)Enum.Parse(typeof(MidiEventType), record.RecordType), + record.Parameters.Skip(1).ToArray(), + settings); + return new CsvEvent(midiEvent, record.ChunkIndex, record.ChunkId, record.ObjectIndex, time); + } + catch (FormatException ex) + { + CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Invalid format of event record.", ex); + return null; + } + } + + private static CsvNote ParseNote( + Record record, + CsvSerializationSettings settings, + bool parseChunkId) + { + //if (record.TrackNumber == null) + // CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Invalid track number."); + // + //if (record.Time == null) + // CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Invalid time."); + + var parameters = record.Parameters; + if (parameters.Length < 6) + CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Invalid number of parameters provided."); + + ITimeSpan time; + TimeSpanUtilities.TryParse(record.Parameters.First(), settings.TimeType, out time); + + if (time == null) + CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Invalid time."); + + ITimeSpan length; + TimeSpanUtilities.TryParse(parameters[1], settings.LengthType, out length); + + if (length == null) + CsvError.ThrowBadFormat(record.CsvRecord.LineNumber, "Invalid length."); + + var channel = (FourBitNumber)TypeParser.FourBitNumber(parameters[2], settings); + + var noteNumber = (SevenBitNumber)TypeParser.NoteNumber(parameters[3], settings); + + var velocity = (SevenBitNumber)TypeParser.SevenBitNumber(parameters[4], settings); + var offVelocity = (SevenBitNumber)TypeParser.SevenBitNumber(parameters[5], settings); + + return new CsvNote(noteNumber, velocity, offVelocity, channel, length, record.ChunkIndex, record.ChunkId, record.ObjectIndex, time); + } + + private static TempoMap GetTempoMap(IEnumerable objects, TimeDivision timeDivision) + { + using (var tempoMapManager = new TempoMapManager(timeDivision)) + { + var setTempoEvents = objects + .OfType() + .Where(e => e.Event is SetTempoEvent) + .OrderBy(e => e.Time, new TimeSpanComparer()); + + foreach (var csvEvent in setTempoEvents) + { + var setTempoEvent = (SetTempoEvent)csvEvent.Event; + tempoMapManager.SetTempo( + csvEvent.Time, + new Tempo(setTempoEvent.MicrosecondsPerQuarterNote)); + } + + var timeSignatureEvents = objects + .OfType() + .Where(e => e.Event is TimeSignatureEvent) + .OrderBy(e => e.Time, new TimeSpanComparer()); + + foreach (var csvEvent in timeSignatureEvents) + { + var timeSignatureEvent = (TimeSignatureEvent)csvEvent.Event; + tempoMapManager.SetTimeSignature( + csvEvent.Time, + new TimeSignature(timeSignatureEvent.Numerator, timeSignatureEvent.Denominator)); + } + + return tempoMapManager.TempoMap; + } + } + + #endregion + } +} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventParserProvider.cs b/DryWetMidi/Tools/CsvSerializer/FromCsv/EventParser.cs similarity index 50% rename from DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventParserProvider.cs rename to DryWetMidi/Tools/CsvSerializer/FromCsv/EventParser.cs index 648d74567..c931701a1 100644 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventParserProvider.cs +++ b/DryWetMidi/Tools/CsvSerializer/FromCsv/EventParser.cs @@ -1,90 +1,126 @@ -using System; +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; +using System; using System.Collections.Generic; using System.Linq; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Core; namespace Melanchall.DryWetMidi.Tools { - internal static class EventParserProvider + internal static class EventParser { + #region Delegates + + public delegate MidiEvent Parser(string[] parameters, CsvSerializationSettings settings); + + #endregion + #region Constants - private static readonly Dictionary EventsParsers = - new Dictionary(StringComparer.OrdinalIgnoreCase) + private static readonly Dictionary EventsParsers = + new Dictionary() { - [RecordLabels.Events.SequenceTrackName] = GetTextEventParser(), - [RecordLabels.Events.CopyrightNotice] = GetTextEventParser(), - [RecordLabels.Events.InstrumentName] = GetTextEventParser(), - [RecordLabels.Events.Marker] = GetTextEventParser(), - [RecordLabels.Events.CuePoint] = GetTextEventParser(), - [RecordLabels.Events.Lyric] = GetTextEventParser(), - [RecordLabels.Events.Text] = GetTextEventParser(), - [RecordLabels.Events.SequenceNumber] = GetEventParser( + [MidiEventType.SequenceTrackName] = GetTextEventParser(), + [MidiEventType.CopyrightNotice] = GetTextEventParser(), + [MidiEventType.InstrumentName] = GetTextEventParser(), + [MidiEventType.Marker] = GetTextEventParser(), + [MidiEventType.CuePoint] = GetTextEventParser(), + [MidiEventType.Lyric] = GetTextEventParser(), + [MidiEventType.Text] = GetTextEventParser(), + [MidiEventType.DeviceName] = GetTextEventParser(), + [MidiEventType.ProgramName] = GetTextEventParser(), + [MidiEventType.SequenceNumber] = GetEventParser( x => new SequenceNumberEvent((ushort)x[0]), TypeParser.UShort), - [RecordLabels.Events.PortPrefix] = GetEventParser( + [MidiEventType.PortPrefix] = GetEventParser( x => new PortPrefixEvent((byte)x[0]), TypeParser.Byte), - [RecordLabels.Events.ChannelPrefix] = GetEventParser( + [MidiEventType.ChannelPrefix] = GetEventParser( x => new ChannelPrefixEvent((byte)x[0]), TypeParser.Byte), - [RecordLabels.Events.TimeSignature] = GetEventParser( + [MidiEventType.TimeSignature] = GetEventParser( x => new TimeSignatureEvent((byte)x[0], (byte)x[1], (byte)x[2], (byte)x[3]), TypeParser.Byte, TypeParser.Byte, TypeParser.Byte, TypeParser.Byte), - [RecordLabels.Events.KeySignature] = GetEventParser( + [MidiEventType.KeySignature] = GetEventParser( x => new KeySignatureEvent((sbyte)x[0], (byte)x[1]), TypeParser.SByte, TypeParser.Byte), - [RecordLabels.Events.SetTempo] = GetEventParser( + [MidiEventType.SetTempo] = GetEventParser( x => new SetTempoEvent((long)x[0]), TypeParser.Long), - [RecordLabels.Events.SmpteOffset] = GetEventParser( - x => new SmpteOffsetEvent(SmpteData.GetFormat((byte)x[0]), - SmpteData.GetHours((byte)x[0]), - (byte)x[1], - (byte)x[2], - (byte)x[3], - (byte)x[4]), + [MidiEventType.SmpteOffset] = GetEventParser( + x => new SmpteOffsetEvent( + (SmpteFormat)Enum.Parse(typeof(SmpteFormat), x[0].ToString()), + (byte)x[1], + (byte)x[2], + (byte)x[3], + (byte)x[4], + (byte)x[5]), + TypeParser.String, TypeParser.Byte, TypeParser.Byte, TypeParser.Byte, TypeParser.Byte, TypeParser.Byte), - [RecordLabels.Events.SequencerSpecific] = GetBytesBasedEventParser( - x => new SequencerSpecificEvent((byte[])x[1])), - [RecordLabels.Events.UnknownMeta] = GetBytesBasedEventParser( - x => new UnknownMetaEvent((byte)x[0], (byte[])x[2]), + [MidiEventType.SequencerSpecific] = GetBytesBasedEventParser( + x => new SequencerSpecificEvent((byte[])x[0])), + [MidiEventType.UnknownMeta] = GetBytesBasedEventParser( + x => new UnknownMetaEvent((byte)x[0], (byte[])x[1]), TypeParser.Byte), - [RecordLabels.Events.NoteOn] = GetNoteEventParser(2), - [RecordLabels.Events.NoteOff] = GetNoteEventParser(2), - [RecordLabels.Events.PitchBend] = GetEventParser( + [MidiEventType.NoteOn] = GetNoteEventParser(2), + [MidiEventType.NoteOff] = GetNoteEventParser(2), + [MidiEventType.PitchBend] = GetEventParser( x => new PitchBendEvent((ushort)x[1]) { Channel = (FourBitNumber)x[0] }, TypeParser.FourBitNumber, TypeParser.UShort), - [RecordLabels.Events.ControlChange] = GetChannelEventParser(2), - [RecordLabels.Events.ProgramChange] = GetChannelEventParser(1), - [RecordLabels.Events.ChannelAftertouch] = GetChannelEventParser(1), - [RecordLabels.Events.NoteAftertouch] = GetNoteEventParser(2), - [RecordLabels.Events.SysExCompleted] = GetBytesBasedEventParser( - x => new NormalSysExEvent((byte[])x[1])), - [RecordLabels.Events.SysExIncompleted] = GetBytesBasedEventParser( - x => new NormalSysExEvent((byte[])x[1])), + [MidiEventType.ControlChange] = GetChannelEventParser(2), + [MidiEventType.ProgramChange] = GetChannelEventParser(1), + [MidiEventType.ChannelAftertouch] = GetChannelEventParser(1), + [MidiEventType.NoteAftertouch] = GetNoteEventParser(2), + [MidiEventType.NormalSysEx] = GetBytesBasedEventParser( + x => new NormalSysExEvent((byte[])x[0])), + [MidiEventType.EscapeSysEx] = GetBytesBasedEventParser( + x => new EscapeSysExEvent((byte[])x[0])), + [MidiEventType.Start] = GetEventParser( + x => new StartEvent()), + [MidiEventType.Stop] = GetEventParser( + x => new StopEvent()), + [MidiEventType.Reset] = GetEventParser( + x => new ResetEvent()), + [MidiEventType.Continue] = GetEventParser( + x => new ContinueEvent()), + [MidiEventType.TuneRequest] = GetEventParser( + x => new TuneRequestEvent()), + [MidiEventType.TimingClock] = GetEventParser( + x => new TimingClockEvent()), + [MidiEventType.ActiveSensing] = GetEventParser( + x => new ActiveSensingEvent()), + [MidiEventType.SongSelect] = GetEventParser( + x => new SongSelectEvent((SevenBitNumber)x[0]), + TypeParser.SevenBitNumber), + [MidiEventType.SongPositionPointer] = GetEventParser( + x => new SongPositionPointerEvent((ushort)x[0]), + TypeParser.UShort), + [MidiEventType.MidiTimeCode] = GetEventParser( + x => new MidiTimeCodeEvent( + (MidiTimeCodeComponent)Enum.Parse(typeof(MidiTimeCodeComponent), x[0].ToString()), + (FourBitNumber)x[1]), + TypeParser.String, + TypeParser.FourBitNumber), }; #endregion #region Methods - public static EventParser Get(string eventName) + public static MidiEvent ParseEvent(MidiEventType eventType, string[] parameters, CsvSerializationSettings settings) { - return EventsParsers[eventName]; + return EventsParsers[eventType](parameters, settings); } - private static EventParser GetBytesBasedEventParser(Func eventCreator, params ParameterParser[] parametersParsers) + private static Parser GetBytesBasedEventParser(Func eventCreator, params ParameterParser[] parametersParsers) { return (p, s) => { @@ -112,32 +148,9 @@ private static EventParser GetBytesBasedEventParser(Func ev if (p.Length < i) CsvError.ThrowBadFormat("Invalid number of parameters provided."); - int bytesNumber = 0; - - try - { - bytesNumber = int.Parse(p[i]); - parameters.Add(bytesNumber); - } - catch - { - CsvError.ThrowBadFormat($"Parameter ({i}) is invalid."); - } - - i++; - if (p.Length < i + bytesNumber) - CsvError.ThrowBadFormat("Invalid number of parameters provided."); - try { - var bytes = p.Skip(i) - .Select(x => - { - var b = (byte)TypeParser.Byte(x, s); - i++; - return b; - }) - .ToArray(); + var bytes = TypeParser.BytesArray(p.Last(), s); parameters.Add(bytes); } catch @@ -149,7 +162,7 @@ private static EventParser GetBytesBasedEventParser(Func ev }; } - private static EventParser GetTextEventParser() + private static Parser GetTextEventParser() where TEvent : BaseTextEvent { return GetEventParser( @@ -162,7 +175,7 @@ private static EventParser GetTextEventParser() TypeParser.String); } - private static EventParser GetNoteEventParser(int parametersNumber) + private static Parser GetNoteEventParser(int parametersNumber) where TEvent : ChannelEvent { return GetChannelEventParser( @@ -171,7 +184,7 @@ private static EventParser GetNoteEventParser(int parametersNumber) .ToArray()); } - private static EventParser GetChannelEventParser(int parametersNumber) + private static Parser GetChannelEventParser(int parametersNumber) where TEvent : ChannelEvent { return GetChannelEventParser(Enumerable.Range(0, parametersNumber) @@ -179,7 +192,7 @@ private static EventParser GetChannelEventParser(int parametersNumber) .ToArray()); } - private static EventParser GetChannelEventParser(ParameterParser[] parametersParsers) + private static Parser GetChannelEventParser(ParameterParser[] parametersParsers) where TEvent : ChannelEvent { return GetEventParser( @@ -199,7 +212,7 @@ private static EventParser GetChannelEventParser(ParameterParser[] param .ToArray()); } - private static EventParser GetEventParser(Func eventCreator, params ParameterParser[] parametersParsers) + private static Parser GetEventParser(Func eventCreator, params ParameterParser[] parametersParsers) { return (p, s) => { diff --git a/DryWetMidi/Tools/CsvSerializer/FromCsv/ParameterParser.cs b/DryWetMidi/Tools/CsvSerializer/FromCsv/ParameterParser.cs new file mode 100644 index 000000000..a7b945fd4 --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/FromCsv/ParameterParser.cs @@ -0,0 +1,4 @@ +namespace Melanchall.DryWetMidi.Tools +{ + internal delegate object ParameterParser(string parameter, CsvSerializationSettings settings); +} diff --git a/DryWetMidi/Tools/CsvSerializer/FromCsv/Record.cs b/DryWetMidi/Tools/CsvSerializer/FromCsv/Record.cs new file mode 100644 index 000000000..ec5b9ddb7 --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/FromCsv/Record.cs @@ -0,0 +1,43 @@ +namespace Melanchall.DryWetMidi.Tools +{ + internal sealed class Record + { + #region Constants + + public const string HeaderType = "Header"; + public const string EventType = "Event"; + public const string NoteType = "Note"; + + #endregion + + #region Constructor + + public Record(CsvRecord csvRecord, int? chunkIndex, string chunkId, int? objectIndex, string recordType, string[] parameters) + { + CsvRecord = csvRecord; + ChunkIndex = chunkIndex; + ChunkId = chunkId; + ObjectIndex = objectIndex; + RecordType = recordType; + Parameters = parameters; + } + + #endregion + + #region Properties + + public CsvRecord CsvRecord { get; } + + public int? ChunkIndex { get; } + + public string ChunkId { get; } + + public int? ObjectIndex { get; } + + public string RecordType { get; } + + public string[] Parameters { get; } + + #endregion + } +} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/TypeParser.cs b/DryWetMidi/Tools/CsvSerializer/FromCsv/TypeParser.cs similarity index 70% rename from DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/TypeParser.cs rename to DryWetMidi/Tools/CsvSerializer/FromCsv/TypeParser.cs index fd4fbb526..54e1b7325 100644 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/TypeParser.cs +++ b/DryWetMidi/Tools/CsvSerializer/FromCsv/TypeParser.cs @@ -1,4 +1,6 @@ using Melanchall.DryWetMidi.Common; +using System; +using System.Linq; namespace Melanchall.DryWetMidi.Tools { @@ -8,7 +10,7 @@ internal static class TypeParser public static readonly ParameterParser SByte = (p, s) => sbyte.Parse(p); public static readonly ParameterParser Long = (p, s) => long.Parse(p); public static readonly ParameterParser UShort = (p, s) => ushort.Parse(p); - public static readonly ParameterParser String = (p, s) => CsvUtilities.UnescapeString(p); + public static readonly ParameterParser String = (p, s) => p; public static readonly ParameterParser Int = (p, s) => int.Parse(p); public static readonly ParameterParser FourBitNumber = (p, s) => (FourBitNumber)byte.Parse(p); public static readonly ParameterParser SevenBitNumber = (p, s) => (SevenBitNumber)byte.Parse(p); @@ -16,13 +18,23 @@ internal static class TypeParser { switch (s.NoteNumberFormat) { - case NoteNumberFormat.NoteNumber: + case CsvNoteFormat.NoteNumber: return SevenBitNumber(p, s); - case NoteNumberFormat.Letter: + case CsvNoteFormat.Letter: return MusicTheory.Note.Parse(p).NoteNumber; } return null; }; + public static readonly ParameterParser BytesArray = (p, s) => p + .Split(' ') + .Select(b => + { + if (s.BytesArrayFormat == CsvBytesArrayFormat.Hexadecimal) + return Convert.ToByte(b, 16); + + return byte.Parse(b); + }) + .ToArray(); } } diff --git a/DryWetMidi/Tools/CsvSerializer/Objects/CsvChord.cs b/DryWetMidi/Tools/CsvSerializer/Objects/CsvChord.cs new file mode 100644 index 000000000..3e8474318 --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/Objects/CsvChord.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace Melanchall.DryWetMidi.Tools +{ + internal sealed class CsvChord : CsvObject + { + #region Constructor + + public CsvChord(int? chunkIndex, string chunkId, int? objectIndex) + : base(chunkIndex, chunkId, objectIndex) + { + } + + #endregion + + #region Properties + + public List Notes { get; } = new List(); + + #endregion + } +} diff --git a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/TimedMidiEvent.cs b/DryWetMidi/Tools/CsvSerializer/Objects/CsvEvent.cs similarity index 58% rename from DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/TimedMidiEvent.cs rename to DryWetMidi/Tools/CsvSerializer/Objects/CsvEvent.cs index 582bd57bb..3e584f686 100644 --- a/DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/TimedMidiEvent.cs +++ b/DryWetMidi/Tools/CsvSerializer/Objects/CsvEvent.cs @@ -3,24 +3,30 @@ namespace Melanchall.DryWetMidi.Tools { - internal sealed class TimedMidiEvent + internal sealed class CsvEvent : CsvObject { #region Constructor - public TimedMidiEvent(ITimeSpan time, MidiEvent midiEvent) + public CsvEvent( + MidiEvent midiEvent, + int? chunkIndex, + string chunkId, + int? objectIndex, + ITimeSpan time) + : base(chunkIndex, chunkId, objectIndex) { - Time = time; Event = midiEvent; + Time = time; } #endregion #region Properties - public ITimeSpan Time { get; } - public MidiEvent Event { get; } + public ITimeSpan Time { get; } + #endregion } } diff --git a/DryWetMidi/Tools/CsvSerializer/Objects/CsvNote.cs b/DryWetMidi/Tools/CsvSerializer/Objects/CsvNote.cs new file mode 100644 index 000000000..5b0f4af96 --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/Objects/CsvNote.cs @@ -0,0 +1,44 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Interaction; + +namespace Melanchall.DryWetMidi.Tools +{ + internal sealed class CsvNote : CsvObject + { + #region Constructor + + public CsvNote( + SevenBitNumber noteNumber, + SevenBitNumber velocity, + SevenBitNumber offVlocity, + FourBitNumber channel, + ITimeSpan length, + int? chunkIndex, + string chunkId, + int? objectIndex, + ITimeSpan time) + : base(chunkIndex, chunkId, objectIndex) + { + NoteNumber = noteNumber; + Velocity = velocity; + OffVlocity = offVlocity; + Channel = channel; + Time = time; + Length = length; + } + + public SevenBitNumber NoteNumber { get; } + + public SevenBitNumber Velocity { get; } + + public SevenBitNumber OffVlocity { get; } + + public FourBitNumber Channel { get; } + + public ITimeSpan Time { get; } + + public ITimeSpan Length { get; } + + #endregion + } +} diff --git a/DryWetMidi/Tools/CsvSerializer/Objects/CsvObject.cs b/DryWetMidi/Tools/CsvSerializer/Objects/CsvObject.cs new file mode 100644 index 000000000..7d0b6fccc --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/Objects/CsvObject.cs @@ -0,0 +1,26 @@ +namespace Melanchall.DryWetMidi.Tools +{ + internal abstract class CsvObject + { + #region Constrcutor + + public CsvObject(int? chunkIndex, string chunkId, int? objectIndex) + { + ChunkIndex = chunkIndex; + ChunkId = chunkId; + ObjectIndex = objectIndex; + } + + #endregion + + #region Properties + + public int? ChunkIndex { get; } + + public string ChunkId { get; } + + public int? ObjectIndex { get; } + + #endregion + } +} diff --git a/DryWetMidi/Tools/CsvSerializer/Settings/CsvBytesArrayFormat.cs b/DryWetMidi/Tools/CsvSerializer/Settings/CsvBytesArrayFormat.cs new file mode 100644 index 000000000..03d0bafc9 --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/Settings/CsvBytesArrayFormat.cs @@ -0,0 +1,8 @@ +namespace Melanchall.DryWetMidi.Tools +{ + public enum CsvBytesArrayFormat + { + Decimal = 0, + Hexadecimal, + } +} diff --git a/DryWetMidi/Tools/CsvSerializer/Settings/CsvNoteFormat.cs b/DryWetMidi/Tools/CsvSerializer/Settings/CsvNoteFormat.cs new file mode 100644 index 000000000..738e5b90b --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/Settings/CsvNoteFormat.cs @@ -0,0 +1,8 @@ +namespace Melanchall.DryWetMidi.Tools +{ + public enum CsvNoteFormat + { + NoteNumber = 0, + Letter + } +} diff --git a/DryWetMidi/Tools/CsvSerializer/Settings/CsvSerializationSettings.cs b/DryWetMidi/Tools/CsvSerializer/Settings/CsvSerializationSettings.cs new file mode 100644 index 000000000..ab693fc62 --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/Settings/CsvSerializationSettings.cs @@ -0,0 +1,80 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Interaction; + +namespace Melanchall.DryWetMidi.Tools +{ + public sealed class CsvSerializationSettings + { + #region Fields + + private TimeSpanType _timeType = TimeSpanType.Midi; + private TimeSpanType _lengthType = TimeSpanType.Midi; + private CsvNoteFormat _noteFormat = CsvNoteFormat.NoteNumber; + private CsvBytesArrayFormat _bytesArrayFormat = CsvBytesArrayFormat.Decimal; + + private int _readWriteBufferSize = 1024; + + #endregion + + #region Properties + + public TimeSpanType TimeType + { + get { return _timeType; } + set + { + ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); + + _timeType = value; + } + } + + public TimeSpanType LengthType + { + get { return _lengthType; } + set + { + ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); + + _lengthType = value; + } + } + + public CsvNoteFormat NoteNumberFormat + { + get { return _noteFormat; } + set + { + ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); + + _noteFormat = value; + } + } + + public CsvBytesArrayFormat BytesArrayFormat + { + get { return _bytesArrayFormat; } + set + { + ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); + + _bytesArrayFormat = value; + } + } + + public char Delimiter { get; set; } = ','; + + public int ReadWriteBufferSize + { + get { return _readWriteBufferSize; } + set + { + ThrowIfArgument.IsNonpositive(nameof(value), value, "Buffer size is zero or negative."); + + _readWriteBufferSize = value; + } + } + + #endregion + } +} diff --git a/DryWetMidi/Tools/CsvSerializer/ToCsv/CsvSerializer.Serialize.cs b/DryWetMidi/Tools/CsvSerializer/ToCsv/CsvSerializer.Serialize.cs new file mode 100644 index 000000000..489200634 --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/ToCsv/CsvSerializer.Serialize.cs @@ -0,0 +1,443 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Interaction; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Melanchall.DryWetMidi.Tools +{ + public static partial class CsvSerializer + { + #region Methods + + public static void SerializeToCsv( + this MidiFile midiFile, + Stream stream, + CsvSerializationSettings settings = null, + ObjectType objectType = ObjectType.TimedEvent, + ObjectDetectionSettings objectDetectionSettings = null) + { + ThrowIfArgument.IsNull(nameof(midiFile), midiFile); + ThrowIfArgument.IsNull(nameof(stream), stream); + + if (!stream.CanWrite) + throw new ArgumentException("Stream doesn't support writing.", nameof(stream)); + + settings = settings ?? new CsvSerializationSettings(); + objectDetectionSettings = objectDetectionSettings ?? new ObjectDetectionSettings(); + + var chunkIndex = 0; + var tempoMap = midiFile.GetTempoMap(); + + using (var writer = new CsvWriter(stream, settings)) + { + WriteHeaderChunk(midiFile, writer, settings, chunkIndex++); + + foreach (var chunk in midiFile.Chunks) + { + WriteChunk(chunk, writer, settings, tempoMap, objectType, objectDetectionSettings, chunkIndex++); + } + } + } + + public static void SerializeToCsv( + this MidiFile midiFile, + string filePath, + bool overwriteFile, + CsvSerializationSettings settings = null, + ObjectType objectType = ObjectType.TimedEvent, + ObjectDetectionSettings objectDetectionSettings = null) + { + ThrowIfArgument.IsNull(nameof(midiFile), midiFile); + ThrowIfArgument.IsNullOrEmptyString(nameof(filePath), filePath, "File path"); + + using (var fileStream = FileUtilities.OpenFileForWrite(filePath, overwriteFile)) + { + midiFile.SerializeToCsv(fileStream, settings, objectType, objectDetectionSettings); + } + } + + public static void SerializeToCsv( + this IEnumerable midiChunks, + Stream stream, + TempoMap tempoMap, + CsvSerializationSettings settings = null, + ObjectType objectType = ObjectType.TimedEvent, + ObjectDetectionSettings objectDetectionSettings = null) + { + ThrowIfArgument.IsNull(nameof(midiChunks), midiChunks); + ThrowIfArgument.IsNull(nameof(stream), stream); + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + + if (!stream.CanWrite) + throw new ArgumentException("Stream doesn't support writing.", nameof(stream)); + + settings = settings ?? new CsvSerializationSettings(); + objectDetectionSettings = objectDetectionSettings ?? new ObjectDetectionSettings(); + + var chunkIndex = 0; + + using (var writer = new CsvWriter(stream, settings)) + { + foreach (var midiChunk in midiChunks) + { + WriteChunk(midiChunk, writer, settings, tempoMap, objectType, objectDetectionSettings, chunkIndex++); + } + } + } + + public static void SerializeToCsv( + this IEnumerable midiChunks, + string filePath, + bool overwriteFile, + TempoMap tempoMap, + CsvSerializationSettings settings = null, + ObjectType objectType = ObjectType.TimedEvent, + ObjectDetectionSettings objectDetectionSettings = null) + { + ThrowIfArgument.IsNull(nameof(midiChunks), midiChunks); + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + ThrowIfArgument.IsNullOrEmptyString(nameof(filePath), filePath, "File path"); + + using (var fileStream = FileUtilities.OpenFileForWrite(filePath, overwriteFile)) + { + midiChunks.SerializeToCsv(fileStream, tempoMap, settings, objectType, objectDetectionSettings); + } + } + + public static void SerializeToCsv( + this MidiChunk midiChunk, + Stream stream, + TempoMap tempoMap, + CsvSerializationSettings settings = null, + ObjectType objectType = ObjectType.TimedEvent, + ObjectDetectionSettings objectDetectionSettings = null) + { + ThrowIfArgument.IsNull(nameof(midiChunk), midiChunk); + ThrowIfArgument.IsNull(nameof(stream), stream); + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + + if (!stream.CanWrite) + throw new ArgumentException("Stream doesn't support writing.", nameof(stream)); + + settings = settings ?? new CsvSerializationSettings(); + objectDetectionSettings = objectDetectionSettings ?? new ObjectDetectionSettings(); + + using (var writer = new CsvWriter(stream, settings)) + { + WriteChunk(midiChunk, writer, settings, tempoMap, objectType, objectDetectionSettings, 0); + } + } + + public static void SerializeToCsv( + this MidiChunk midiChunk, + string filePath, + bool overwriteFile, + TempoMap tempoMap, + CsvSerializationSettings settings = null, + ObjectType objectType = ObjectType.TimedEvent, + ObjectDetectionSettings objectDetectionSettings = null) + { + ThrowIfArgument.IsNull(nameof(midiChunk), midiChunk); + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + ThrowIfArgument.IsNullOrEmptyString(nameof(filePath), filePath, "File path"); + + using (var fileStream = FileUtilities.OpenFileForWrite(filePath, overwriteFile)) + { + midiChunk.SerializeToCsv(fileStream, tempoMap, settings, objectType, objectDetectionSettings); + } + } + + public static void SerializeToCsv( + this IEnumerable timedObjects, + Stream stream, + TempoMap tempoMap, + CsvSerializationSettings settings = null) + { + ThrowIfArgument.IsNull(nameof(timedObjects), timedObjects); + ThrowIfArgument.IsNull(nameof(stream), stream); + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + + if (!stream.CanWrite) + throw new ArgumentException("Stream doesn't support writing.", nameof(stream)); + + settings = settings ?? new CsvSerializationSettings(); + + using (var writer = new CsvWriter(stream, settings)) + { + WriteObjects(timedObjects, writer, settings, tempoMap, null, null); + } + } + + public static void SerializeToCsv( + this IEnumerable timedObjects, + string filePath, + bool overwriteFile, + TempoMap tempoMap, + CsvSerializationSettings settings = null) + { + ThrowIfArgument.IsNull(nameof(timedObjects), timedObjects); + ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); + ThrowIfArgument.IsNullOrEmptyString(nameof(filePath), filePath, "File path"); + + using (var fileStream = FileUtilities.OpenFileForWrite(filePath, overwriteFile)) + { + timedObjects.SerializeToCsv(fileStream, tempoMap, settings); + } + } + + private static void WriteChunk( + MidiChunk midiChunk, + CsvWriter writer, + CsvSerializationSettings settings, + TempoMap tempoMap, + ObjectType objectType, + ObjectDetectionSettings objectDetectionSettings, + int chunkIndex) + { + var trackChunk = midiChunk as TrackChunk; + if (trackChunk != null) + { + WriteTrackChunk(trackChunk, writer, settings, tempoMap, objectType, objectDetectionSettings, chunkIndex); + return; + } + + var unknownChunk = midiChunk as UnknownChunk; + if (unknownChunk != null) + { + WriteUnknownChunk(unknownChunk, writer, settings, chunkIndex); + return; + } + + WriteCustomChunk(midiChunk, writer, settings, chunkIndex); + } + + private static void WriteHeaderChunk( + MidiFile midiFile, + CsvWriter writer, + CsvSerializationSettings settings, + int chunkIndex) + { + writer.WriteRecord( + chunkIndex, + HeaderChunk.Id, + 0, + Record.HeaderType, + midiFile.TimeDivision.ToInt16()); + } + + private static void WriteTrackChunk( + TrackChunk trackChunk, + CsvWriter writer, + CsvSerializationSettings settings, + TempoMap tempoMap, + ObjectType objectType, + ObjectDetectionSettings objectDetectionSettings, + int chunkIndex) + { + WriteObjects( + trackChunk.GetObjects(objectType, objectDetectionSettings), + writer, + settings, + tempoMap, + chunkIndex, + TrackChunk.Id); + } + + private static void WriteUnknownChunk( + UnknownChunk unknownChunk, + CsvWriter writer, + CsvSerializationSettings settings, + int chunkIndex) + { + // TODO: WriteUnknownChunk + } + + private static void WriteCustomChunk( + MidiChunk midiChunk, + CsvWriter writer, + CsvSerializationSettings settings, + int chunkIndex) + { + // TODO: WriteCustomChunk + } + + private static void WriteObjects( + IEnumerable timedObjects, + CsvWriter writer, + CsvSerializationSettings settings, + TempoMap tempoMap, + int? chunkIndex, + string chunkId) + { + var objectIndex = 0; + + foreach (var obj in timedObjects) + { + WriteObject(obj, writer, settings, tempoMap, chunkIndex, chunkId, objectIndex++); + } + } + + private static void WriteObject( + ITimedObject timedObject, + CsvWriter writer, + CsvSerializationSettings settings, + TempoMap tempoMap, + int? chunkIndex, + string chunkId, + int objectIndex) + { + var timedEvent = timedObject as TimedEvent; + if (timedEvent != null) + { + WriteTimedEvent(timedEvent, writer, settings, tempoMap, chunkIndex, chunkId, objectIndex); + return; + } + + var note = timedObject as Note; + if (note != null) + { + WriteNote(note, writer, settings, tempoMap, chunkIndex, chunkId, objectIndex); + return; + } + + var chord = timedObject as Chord; + if (chord != null) + { + WriteChord(chord, writer, settings, tempoMap, chunkIndex, chunkId, objectIndex); + return; + } + + var registeredParameter = timedObject as RegisteredParameter; + if (registeredParameter != null) + { + WriteRegisteredParameter(registeredParameter, writer, settings, tempoMap, chunkIndex, chunkId, objectIndex); + return; + } + + var rest = timedObject as Rest; + if (rest != null) + return; + + WriteCustomObject(timedObject, writer, settings, chunkIndex, chunkId, objectIndex); + } + + private static void WriteRegisteredParameter( + RegisteredParameter registeredParameter, + CsvWriter writer, + CsvSerializationSettings settings, + TempoMap tempoMap, + int? chunkIndex, + string chunkId, + int objectIndex) + { + foreach (var timedEvent in registeredParameter.GetTimedEvents()) + { + WriteTimedEvent(timedEvent, writer, settings, tempoMap, chunkIndex, chunkId, objectIndex); + } + } + + private static void WriteTimedEvent( + TimedEvent timedEvent, + CsvWriter writer, + CsvSerializationSettings settings, + TempoMap tempoMap, + int? chunkIndex, + string chunkId, + int objectIndex) + { + var midiEvent = timedEvent.Event; + if (midiEvent is EndOfTrackEvent) + return; + + var eventParameters = EventParametersProvider.GetEventParameters(midiEvent, settings); + + WriteObjectRecord( + writer, + settings, + tempoMap, + chunkIndex, + chunkId, + objectIndex, + timedEvent, + eventParameters); + } + + private static void WriteNote( + Note note, + CsvWriter writer, + CsvSerializationSettings settings, + TempoMap tempoMap, + int? chunkIndex, + string chunkId, + int objectIndex) + { + WriteObjectRecord( + writer, + settings, + tempoMap, + chunkIndex, + chunkId, + objectIndex, + note, + CsvFormattingUtilities.FormatLength(note, settings.LengthType, tempoMap), + note.Channel, + CsvFormattingUtilities.FormatNoteNumber(note.NoteNumber, settings.NoteNumberFormat), + note.Velocity, + note.OffVelocity); + } + + private static void WriteChord( + Chord chord, + CsvWriter writer, + CsvSerializationSettings settings, + TempoMap tempoMap, + int? chunkIndex, + string chunkId, + int objectIndex) + { + foreach (var note in chord.Notes) + { + WriteNote(note, writer, settings, tempoMap, chunkIndex, chunkId, objectIndex); + } + } + + private static void WriteCustomObject( + ITimedObject timedObject, + CsvWriter writer, + CsvSerializationSettings settings, + int? chunkIndex, + string chunkId, + int objectIndex) + { + // TODO: WriteCustomObject + } + + private static void WriteObjectRecord( + CsvWriter writer, + CsvSerializationSettings settings, + TempoMap tempoMap, + int? chunkIndex, + string chunkId, + int objectIndex, + ITimedObject obj, + params object[] values) + { + writer.WriteRecord( + new object[] + { + chunkIndex, + chunkId, + objectIndex, + obj is TimedEvent ? ((TimedEvent)obj).Event.EventType.ToString() : Record.NoteType, + CsvFormattingUtilities.FormatTime(obj, settings.TimeType, tempoMap) + } + .Where(v => v != null) + .Concat(values)); + } + + #endregion + } +} diff --git a/DryWetMidi/Tools/CsvSerializer/ToCsv/CsvWriter.cs b/DryWetMidi/Tools/CsvSerializer/ToCsv/CsvWriter.cs new file mode 100644 index 000000000..1b2bb102f --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/ToCsv/CsvWriter.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Melanchall.DryWetMidi.Tools +{ + internal sealed class CsvWriter : IDisposable + { + #region Fields + + private readonly StreamWriter _streamWriter; + private readonly string _delimiterString; + private readonly Func _byteFormatter; + + private bool _disposed = false; + + #endregion + + #region Constructor + + public CsvWriter(Stream stream, CsvSerializationSettings settings) + { + _streamWriter = new StreamWriter(stream, new UTF8Encoding(false, true), settings.ReadWriteBufferSize, true); + _delimiterString = settings.Delimiter.ToString(); + _byteFormatter = GetByteFormatter(settings.BytesArrayFormat); + } + + #endregion + + #region Methods + + public void WriteRecord(IEnumerable values) + { + _streamWriter.WriteLine(string.Join(_delimiterString, values.Select(ProcessValue))); + } + + public void WriteRecord(params object[] values) + { + WriteRecord((IEnumerable)values); + } + + public void Dispose() + { + Dispose(true); + } + + private object ProcessValue(object value) + { + if (value == null) + return string.Empty; + + // TODO: bytes delimiter + var bytes = value as byte[]; + if (bytes != null) + value = string.Join(" ", bytes.Select(b => _byteFormatter(b))); + + var s = value as string; + if (s != null) + return CsvFormattingUtilities.EscapeString(s); + + return value; + } + + private static Func GetByteFormatter(CsvBytesArrayFormat format) + { + if (format == CsvBytesArrayFormat.Decimal) + return GetAsDecimal; + + if (format == CsvBytesArrayFormat.Hexadecimal) + return GetAsHexadecimal; + + throw new NotImplementedException(); + } + + private static string GetAsDecimal(byte b) + { + return b.ToString(); + } + + private static string GetAsHexadecimal(byte b) + { + return Convert.ToString((int)b, 16).PadLeft(2, '0').ToUpperInvariant(); + } + + #endregion + + #region IDisposable + + void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + _streamWriter.Dispose(); + + _disposed = true; + } + + #endregion + } +} diff --git a/DryWetMidi/Tools/CsvSerializer/ToCsv/EventParametersProvider.cs b/DryWetMidi/Tools/CsvSerializer/ToCsv/EventParametersProvider.cs new file mode 100644 index 000000000..094f48f4a --- /dev/null +++ b/DryWetMidi/Tools/CsvSerializer/ToCsv/EventParametersProvider.cs @@ -0,0 +1,129 @@ +using Melanchall.DryWetMidi.Core; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Melanchall.DryWetMidi.Tools +{ + internal static class EventParametersProvider + { + #region Delegates + + public delegate object[] EventParametersGetter(MidiEvent midiEvent, CsvSerializationSettings settings); + + #endregion + + #region Constants + + private static readonly Dictionary EventsParametersGetters = + new Dictionary + { + [MidiEventType.SequenceTrackName] = GetParameters( + (e, s) => e.Text), + [MidiEventType.CopyrightNotice] = GetParameters( + (e, s) => e.Text), + [MidiEventType.InstrumentName] = GetParameters( + (e, s) => e.Text), + [MidiEventType.Marker] = GetParameters( + (e, s) => e.Text), + [MidiEventType.CuePoint] = GetParameters( + (e, s) => e.Text), + [MidiEventType.Lyric] = GetParameters( + (e, s) => e.Text), + [MidiEventType.Text] = GetParameters( + (e, s) => e.Text), + [MidiEventType.ProgramName] = GetParameters( + (e, s) => e.Text), + [MidiEventType.DeviceName] = GetParameters( + (e, s) => e.Text), + [MidiEventType.SequenceNumber] = GetParameters( + (e, s) => e.Number), + [MidiEventType.PortPrefix] = GetParameters( + (e, s) => e.Port), + [MidiEventType.ChannelPrefix] = GetParameters( + (e, s) => e.Channel), + [MidiEventType.TimeSignature] = GetParameters( + (e, s) => e.Numerator, + (e, s) => e.Denominator, + (e, s) => e.ClocksPerClick, + (e, s) => e.ThirtySecondNotesPerBeat), + [MidiEventType.KeySignature] = GetParameters( + (e, s) => e.Key, + (e, s) => e.Scale), + [MidiEventType.SetTempo] = GetParameters( + (e, s) => e.MicrosecondsPerQuarterNote), + [MidiEventType.SmpteOffset] = GetParameters( + (e, s) => e.Format.ToString(), + (e, s) => e.Hours, + (e, s) => e.Minutes, + (e, s) => e.Seconds, + (e, s) => e.Frames, + (e, s) => e.SubFrames), + [MidiEventType.SequencerSpecific] = GetParameters( + (e, s) => e.Data), + [MidiEventType.UnknownMeta] = GetParameters( + (e, s) => e.StatusByte, + (e, s) => e.Data), + [MidiEventType.NoteOn] = GetParameters( + (e, s) => e.Channel, + (e, s) => CsvFormattingUtilities.FormatNoteNumber(e.NoteNumber, s.NoteNumberFormat), + (e, s) => e.Velocity), + [MidiEventType.NoteOff] = GetParameters( + (e, s) => e.Channel, + (e, s) => CsvFormattingUtilities.FormatNoteNumber(e.NoteNumber, s.NoteNumberFormat), + (e, s) => e.Velocity), + [MidiEventType.PitchBend] = GetParameters( + (e, s) => e.Channel, + (e, s) => e.PitchValue), + [MidiEventType.ControlChange] = GetParameters( + (e, s) => e.Channel, + (e, s) => e.ControlNumber, + (e, s) => e.ControlValue), + [MidiEventType.ProgramChange] = GetParameters( + (e, s) => e.Channel, + (e, s) => e.ProgramNumber), + [MidiEventType.ChannelAftertouch] = GetParameters( + (e, s) => e.Channel, + (e, s) => e.AftertouchValue), + [MidiEventType.NoteAftertouch] = GetParameters( + (e, s) => e.Channel, + (e, s) => CsvFormattingUtilities.FormatNoteNumber(e.NoteNumber, s.NoteNumberFormat), + (e, s) => e.AftertouchValue), + [MidiEventType.NormalSysEx] = GetParameters( + (e, s) => e.Data), + [MidiEventType.EscapeSysEx] = GetParameters( + (e, s) => e.Data), + [MidiEventType.ActiveSensing] = GetParameters(), + [MidiEventType.Start] = GetParameters(), + [MidiEventType.Stop] = GetParameters(), + [MidiEventType.Reset] = GetParameters(), + [MidiEventType.Continue] = GetParameters(), + [MidiEventType.TimingClock] = GetParameters(), + [MidiEventType.TuneRequest] = GetParameters(), + [MidiEventType.MidiTimeCode] = GetParameters( + (e, s) => e.Component.ToString(), + (e, s) => e.ComponentValue), + [MidiEventType.SongSelect] = GetParameters( + (e, s) => e.Number), + [MidiEventType.SongPositionPointer] = GetParameters( + (e, s) => e.PointerValue), + }; + + #endregion + + #region Methods + + public static object[] GetEventParameters(MidiEvent midiEvent, CsvSerializationSettings settings) + { + return EventsParametersGetters[midiEvent.EventType](midiEvent, settings); + } + + private static EventParametersGetter GetParameters(params Func[] parametersGetters) + where TEvent : MidiEvent + { + return (e, s) => parametersGetters.Select(g => g((TEvent)e, s)).ToArray(); + } + + #endregion + } +} diff --git a/Resources/CI/run-static-analysis.yaml b/Resources/CI/run-static-analysis.yaml index 3804623c0..d5d9cee49 100644 --- a/Resources/CI/run-static-analysis.yaml +++ b/Resources/CI/run-static-analysis.yaml @@ -42,14 +42,14 @@ stages: - task: CmdLine@2 displayName: Run analysis inputs: - script: 'jb inspectcode Melanchall.DryWetMidi.sln --properties:Configuration=DebugTest --exclude="DryWetMidi.Benchmarks\**.*" -o=ReSharperReport.xml -f="xml"' + script: 'jb inspectcode Melanchall.DryWetMidi.sln --properties:Configuration=DebugTest --exclude="DryWetMidi.Benchmarks\**.*" -o=ReSharperReport.sarif' - task: DotNetCoreCLI@2 displayName: Convert report to HTML inputs: command: 'run' projects: 'Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml.csproj' - arguments: '-c Release -- ReSharperReport.xml ReSharperReport.html $(Build.SourceBranchName)' + arguments: '-c Release -- ReSharperReport.sarif ReSharperReport.html $(Build.SourceBranchName)' - task: PublishPipelineArtifact@1 displayName: Publish 'ReSharperReport' artifact diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ArtifactLocationDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ArtifactLocationDto.cs new file mode 100644 index 000000000..02cd36db0 --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ArtifactLocationDto.cs @@ -0,0 +1,7 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class ArtifactLocationDto + { + public string Uri { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/DriverDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/DriverDto.cs new file mode 100644 index 000000000..0bd5ae8c9 --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/DriverDto.cs @@ -0,0 +1,7 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class DriverDto + { + public RuleDto[] Rules { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/LocationDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/LocationDto.cs new file mode 100644 index 000000000..1e94abd2c --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/LocationDto.cs @@ -0,0 +1,7 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class LocationDto + { + public PhysicalLocationDto PhysicalLocation { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/PhysicalLocationDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/PhysicalLocationDto.cs new file mode 100644 index 000000000..b24349480 --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/PhysicalLocationDto.cs @@ -0,0 +1,9 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class PhysicalLocationDto + { + public ArtifactLocationDto ArtifactLocation { get; set; } + + public RegionDto Region { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RegionDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RegionDto.cs new file mode 100644 index 000000000..4a7ed7b9d --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RegionDto.cs @@ -0,0 +1,17 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class RegionDto + { + public int StartLine { get; set; } + + public int StartColumn { get; set; } + + public int EndLine { get; set; } + + public int EndColumn { get; set; } + + public int CharOffset { get; set; } + + public int CharLength { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RelationshipDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RelationshipDto.cs new file mode 100644 index 000000000..55c5c7156 --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RelationshipDto.cs @@ -0,0 +1,7 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class RelationshipDto + { + public TargetDto Target { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ReportDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ReportDto.cs new file mode 100644 index 000000000..fd1a6cb03 --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ReportDto.cs @@ -0,0 +1,7 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class ReportDto + { + public RunDto[] Runs { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ResultDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ResultDto.cs new file mode 100644 index 000000000..7655be0c0 --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ResultDto.cs @@ -0,0 +1,11 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class ResultDto + { + public string RuleId { get; set; } + + public TextDto Message { get; set; } + + public LocationDto[] Locations { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RuleDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RuleDto.cs new file mode 100644 index 000000000..7b4bfa6a8 --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RuleDto.cs @@ -0,0 +1,13 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class RuleDto + { + public string Id { get; set; } + + public TextDto FullDescription { get; set; } + + public TextDto ShortDescription { get; set; } + + public RelationshipDto[] Relationships { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RunDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RunDto.cs new file mode 100644 index 000000000..8b39eab0b --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/RunDto.cs @@ -0,0 +1,9 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class RunDto + { + public ResultDto[] Results { get; set; } + + public ToolDto Tool { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/TargetDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/TargetDto.cs new file mode 100644 index 000000000..d7ec9813c --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/TargetDto.cs @@ -0,0 +1,7 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class TargetDto + { + public string Id { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/TextDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/TextDto.cs new file mode 100644 index 000000000..9e577260e --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/TextDto.cs @@ -0,0 +1,7 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class TextDto + { + public string Text { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ToolDto.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ToolDto.cs new file mode 100644 index 000000000..0aa3f878d --- /dev/null +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Dto/ToolDto.cs @@ -0,0 +1,7 @@ +namespace ConvertReSharperReportToHtml +{ + internal sealed class ToolDto + { + public DriverDto Driver { get; set; } + } +} diff --git a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Program.cs b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Program.cs index 502755cf9..5194b09a0 100644 --- a/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Program.cs +++ b/Resources/Utilities/ConvertReSharperReportToHtml/ConvertReSharperReportToHtml/Program.cs @@ -1,7 +1,7 @@ -using System.Text; +using ConvertReSharperReportToHtml; +using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; -using System.Xml.Linq; -using System.Xml.XPath; var inputFilePath = Path.GetFullPath(args[0]); var outputFilePath = Path.GetFullPath(args[1]); @@ -9,7 +9,8 @@ Console.WriteLine($"Converting [{inputFilePath}] to HTML [{outputFilePath}] for [{branch}] branch..."); -var xDocument = XDocument.Load(inputFilePath); +var reportJson = File.ReadAllText(inputFilePath); +var xDocument = JsonSerializer.Deserialize(reportJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); var issueTypes = GetIssueTypes(xDocument); var projects = GetProjects(xDocument); @@ -20,27 +21,47 @@ foreach (var project in projects) { + Console.WriteLine($"Processing [{project}] project..."); + if (project.Contains("Test")) + { + Console.WriteLine(" skip test project"); + continue; + } + var allIssues = GetProjectIssues(xDocument, project, issueTypes); + Console.WriteLine($" [{allIssues.Length}] issues found"); + var issuesGroups = allIssues .GroupBy(i => i.IssueType) .ToArray(); + Console.WriteLine($" [{issuesGroups.Length}] issues groups built"); + stringBuilder.AppendLine($@"

{project} ({allIssues.Length} issues)

"); + var i = 1; + foreach (var issues in issuesGroups) { + Console.WriteLine($" [{i} / {issuesGroups.Length}] {issues.Key}"); + stringBuilder.AppendLine($@"

[{issues.Key.Severity}] {issues.Key.Description}

"); + var j = 1; + var groupSize = issues.Count(); + foreach (var issue in issues) { + Console.WriteLine($" [{j} / {groupSize}] {issue.Message}"); + var gitHubPath = issue.FilePath.Replace('\\', '/'); stringBuilder.AppendLine($@" @@ -48,11 +69,15 @@ "); + + j++; } stringBuilder.AppendLine($@"
{issue.Message}
"); + + i++; } } @@ -63,27 +88,33 @@ // -Dictionary GetIssueTypes(XDocument xDocument) => xDocument - .Root - .XPathSelectElements("//IssueType") +Dictionary GetIssueTypes(ReportDto xDocument) => xDocument + .Runs + .First() + .Tool + .Driver + .Rules .ToDictionary( - e => e.Attribute("Id").Value, - e => new IssueType(e.Attribute("Description").Value, e.Attribute("Severity").Value)); - -string[] GetProjects(XDocument xDocument) => xDocument - .Root - .XPathSelectElements("//Issues//Project") - .Select(e => e.Attribute("Name").Value) + e => e.Id, + e => new IssueType(e.FullDescription?.Text ?? e.ShortDescription.Text, e.Relationships.First().Target.Id)); + +string[] GetProjects(ReportDto xDocument) => xDocument + .Runs + .First() + .Results + .Select(e => Regex.Match(e.Locations.First().PhysicalLocation.ArtifactLocation.Uri, @"(.+?)/").Groups[1].Value) .ToArray(); -Issue[] GetProjectIssues(XDocument xDocument, string project, Dictionary issueTypes) => xDocument - .Root - .XPathSelectElements($"//Issues//Project[@Name='{project}']//Issue") +Issue[] GetProjectIssues(ReportDto xDocument, string project, Dictionary issueTypes) => xDocument + .Runs + .First() + .Results + .Where(e => e.Locations.First().PhysicalLocation.ArtifactLocation.Uri.StartsWith($"{project}/")) .Select(e => new Issue( - e.Attribute("File").Value, - issueTypes[e.Attribute("TypeId").Value], - Convert.ToInt32(e.Attribute("Line")?.Value ?? "-1"), - e.Attribute("Message").Value)) + e.Locations.First().PhysicalLocation.ArtifactLocation.Uri, + issueTypes[e.RuleId], + e.Locations.First().PhysicalLocation.Region.StartLine, + e.Message.Text)) .ToArray(); record IssueType(string Description, string Severity);