From ce23efb6d51737c2fc8e55667b4dceb13312e93b Mon Sep 17 00:00:00 2001 From: Maxim Dobroselsky Date: Thu, 11 Jan 2024 22:11:51 +0300 Subject: [PATCH] CSV serialization redesign --- .../high-level-managing/Getting-objects.md | 181 ++-- Docs/articles/tools/MIDI-file-splitting.md | 1 - Docs/obsolete/OBS6/description.md | 2 +- DryWetMidi.Tests.Common/FileOperations.cs | 2 +- .../GetObjectsUtilitiesTests.NotesAndRests.cs | 486 ---------- .../GetObjectsUtilitiesTests.Rests.cs | 433 --------- .../ObjectId/ObjectIdUtilitiesTests.cs | 2 +- .../Rests/RestsUtilitiesTests.GetRests.cs | 452 +++++++++ .../Rests/RestsUtilitiesTests.WithRests.cs | 537 +++++++++++ .../Multimedia/Recording/RecordingTests.cs | 10 +- .../Tools/CsvConverter/CsvConverterTests.cs | 906 ------------------ .../CsvSerializerTests.Deserialize.cs | 407 ++++++++ .../CsvSerializer/CsvSerializerTests.Misc.cs | 124 +++ .../CsvSerializerTests.Serialize.cs | 419 ++++++++ .../Tools/Merger/MergerTests.MergeObjects.cs | 44 +- .../RepeaterTests.MultipleCollections.cs | 25 + .../RepeaterTests.SingleCollection.cs | 20 +- .../Sanitizer/SanitizerTests.NoteMinLength.cs | 6 +- ...tizerTests.RemoveEventsOnUnusedChannels.cs | 6 +- ...itizerTests.RemoveOrphanedNoteOffEvents.cs | 22 +- ...nitizerTests.RemoveOrphanedNoteOnEvents.cs | 24 +- .../Tools/Sanitizer/SanitizerTests.Trim.cs | 110 +++ .../SplitterTests.SplitObjectsByStep.cs | 6 +- DryWetMidi.Tests/Tools/TimeAndMidiEvent.cs | 26 - DryWetMidi.Tests/Utilities/ChordMethods.cs | 17 +- .../Utilities/LengthedObjectMethods.cs | 29 +- DryWetMidi.Tests/Utilities/MidiAsserts.cs | 9 +- DryWetMidi.Tests/Utilities/NoteMethods.cs | 15 +- .../Utilities/TimedObjectMethods.cs | 24 - DryWetMidi/Common/MathUtilities.cs | 7 - DryWetMidi/Core/MidiFile.cs | 4 +- DryWetMidi/Interaction/Chords/Chord.cs | 10 + .../Chords/ChordsManagingUtilities.cs | 8 +- .../GetObjects/GetObjectsUtilities.cs | 97 +- .../GetObjects/ObjectDetectionSettings.cs | 5 - .../Interaction/GetObjects/ObjectType.cs | 5 - .../GetObjects/RestSeparationPolicy.cs | 30 - .../Settings/RestDetectionSettings.cs | 42 - DryWetMidi/Interaction/Notes/Note.cs | 10 + .../Notes/NotesManagingUtilities.cs | 6 +- .../Interaction/ObjectId/ObjectIdUtilities.cs | 2 +- DryWetMidi/Interaction/ObjectId/RestId.cs | 18 +- .../Interaction/Parameters/Parameter.cs | 5 + .../Interaction/{GetObjects => Rests}/Rest.cs | 42 +- .../Rests/RestDetectionSettings.cs | 13 + .../Interaction/Rests/RestsUtilities.cs | 117 +++ .../Converters/MusicalTimeSpanConverter.cs | 4 +- .../Interaction/TimedEvents/TimedEvent.cs | 5 + .../TimedEventsManagingUtilities.cs | 4 +- .../ISortedTimedObjectsImmutableCollection.cs | 6 + .../SortedTimedObjectsImmutableCollection.cs | 73 ++ .../Interaction/ValueLine/ValueChange.cs | 6 + .../Recording/MidiEventRecordedEventArgs.cs | 25 + DryWetMidi/Multimedia/Recording/Recording.cs | 18 +- .../Tools/CsvConverter/Common/CsvSettings.cs | 41 - .../Tools/CsvConverter/Common/CsvWriter.cs | 58 -- DryWetMidi/Tools/CsvConverter/CsvConverter.cs | 338 ------- DryWetMidi/Tools/CsvConverter/CsvUtilities.cs | 30 - .../FromCsv/CsvToMidiFileConverter.cs | 260 ----- .../MidiFile/FromCsv/EventParser.cs | 6 - .../MidiFile/FromCsv/EventsNamesProvider.cs | 28 - .../MidiFile/FromCsv/ParameterParser.cs | 4 - .../CsvConverter/MidiFile/FromCsv/Record.cs | 34 - .../MidiFile/FromCsv/RecordType.cs | 12 - .../MidiFile/MidiFileCsvConversionSettings.cs | 95 -- .../Tools/CsvConverter/MidiFile/NoteFormat.cs | 18 - .../CsvConverter/MidiFile/RecordLabels.cs | 41 - .../MidiFile/ToCsv/EventNameGetter.cs | 6 - .../MidiFile/ToCsv/EventNameGetterProvider.cs | 68 -- .../MidiFile/ToCsv/EventParametersGetter.cs | 6 - .../ToCsv/EventParametersGetterProvider.cs | 89 -- .../MidiFile/ToCsv/MidiFileToCsvConverter.cs | 161 ---- .../CsvConverter/Notes/CsvToNotesConverter.cs | 85 -- .../Notes/NoteCsvConversionSettings.cs | 77 -- .../Notes/NoteCsvConversionUtilities.cs | 24 - .../CsvConverter/Notes/NoteNumberFormat.cs | 19 - .../CsvConverter/Notes/NotesToCsvConverter.cs | 33 - .../Common => CsvSerializer}/CsvError.cs | 0 .../CsvSerializer/CsvFormattingUtilities.cs | 60 ++ .../FromCsv}/CsvReader.cs | 20 +- .../FromCsv}/CsvRecord.cs | 0 .../FromCsv/CsvSerializer.Deserialize.cs | 542 +++++++++++ .../FromCsv/EventParser.cs} | 159 +-- .../CsvSerializer/FromCsv/ParameterParser.cs | 4 + .../Tools/CsvSerializer/FromCsv/Record.cs | 43 + .../FromCsv/TypeParser.cs | 18 +- .../Tools/CsvSerializer/Objects/CsvChord.cs | 22 + .../Objects/CsvEvent.cs} | 16 +- .../Tools/CsvSerializer/Objects/CsvNote.cs | 44 + .../Tools/CsvSerializer/Objects/CsvObject.cs | 26 + .../Settings/CsvBytesArrayFormat.cs | 8 + .../CsvSerializer/Settings/CsvNoteFormat.cs | 8 + .../Settings/CsvSerializationSettings.cs | 80 ++ .../ToCsv/CsvSerializer.Serialize.cs | 443 +++++++++ .../Tools/CsvSerializer/ToCsv/CsvWriter.cs | 104 ++ .../ToCsv/EventParametersProvider.cs | 129 +++ DryWetMidi/Tools/Merger/ObjectsMerger.cs | 32 +- DryWetMidi/Tools/Repeater/Repeater.cs | 28 +- .../Tools/Repeater/RepeaterUtilities.cs | 16 +- DryWetMidi/Tools/Sanitizer/Sanitizer.cs | 25 + .../Tools/Sanitizer/SanitizingSettings.cs | 2 + Resources/CI/run-static-analysis.yaml | 2 +- 102 files changed, 4279 insertions(+), 3992 deletions(-) delete mode 100644 DryWetMidi.Tests/Interaction/GetObjects/GetObjectsUtilitiesTests.NotesAndRests.cs delete mode 100644 DryWetMidi.Tests/Interaction/GetObjects/GetObjectsUtilitiesTests.Rests.cs create mode 100644 DryWetMidi.Tests/Interaction/Rests/RestsUtilitiesTests.GetRests.cs create mode 100644 DryWetMidi.Tests/Interaction/Rests/RestsUtilitiesTests.WithRests.cs delete mode 100644 DryWetMidi.Tests/Tools/CsvConverter/CsvConverterTests.cs create mode 100644 DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Deserialize.cs create mode 100644 DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Misc.cs create mode 100644 DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Serialize.cs create mode 100644 DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.Trim.cs delete mode 100644 DryWetMidi.Tests/Tools/TimeAndMidiEvent.cs delete mode 100644 DryWetMidi.Tests/Utilities/TimedObjectMethods.cs delete mode 100644 DryWetMidi/Interaction/GetObjects/RestSeparationPolicy.cs delete mode 100644 DryWetMidi/Interaction/GetObjects/Settings/RestDetectionSettings.cs rename DryWetMidi/Interaction/{GetObjects => Rests}/Rest.cs (82%) create mode 100644 DryWetMidi/Interaction/Rests/RestDetectionSettings.cs create mode 100644 DryWetMidi/Interaction/Rests/RestsUtilities.cs create mode 100644 DryWetMidi/Interaction/TimedObject/ISortedTimedObjectsImmutableCollection.cs create mode 100644 DryWetMidi/Interaction/TimedObject/SortedTimedObjectsImmutableCollection.cs create mode 100644 DryWetMidi/Multimedia/Recording/MidiEventRecordedEventArgs.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/Common/CsvSettings.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/Common/CsvWriter.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/CsvConverter.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/CsvUtilities.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/CsvToMidiFileConverter.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventParser.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/EventsNamesProvider.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/ParameterParser.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/Record.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/FromCsv/RecordType.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/MidiFileCsvConversionSettings.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/NoteFormat.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/RecordLabels.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventNameGetter.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventNameGetterProvider.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventParametersGetter.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/EventParametersGetterProvider.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/MidiFile/ToCsv/MidiFileToCsvConverter.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/Notes/CsvToNotesConverter.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/Notes/NoteCsvConversionSettings.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/Notes/NoteCsvConversionUtilities.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/Notes/NoteNumberFormat.cs delete mode 100644 DryWetMidi/Tools/CsvConverter/Notes/NotesToCsvConverter.cs rename DryWetMidi/Tools/{CsvConverter/Common => CsvSerializer}/CsvError.cs (100%) create mode 100644 DryWetMidi/Tools/CsvSerializer/CsvFormattingUtilities.cs rename DryWetMidi/Tools/{CsvConverter/Common => CsvSerializer/FromCsv}/CsvReader.cs (93%) rename DryWetMidi/Tools/{CsvConverter/Common => CsvSerializer/FromCsv}/CsvRecord.cs (100%) create mode 100644 DryWetMidi/Tools/CsvSerializer/FromCsv/CsvSerializer.Deserialize.cs rename DryWetMidi/Tools/{CsvConverter/MidiFile/FromCsv/EventParserProvider.cs => CsvSerializer/FromCsv/EventParser.cs} (50%) create mode 100644 DryWetMidi/Tools/CsvSerializer/FromCsv/ParameterParser.cs create mode 100644 DryWetMidi/Tools/CsvSerializer/FromCsv/Record.cs rename DryWetMidi/Tools/{CsvConverter/MidiFile => CsvSerializer}/FromCsv/TypeParser.cs (70%) create mode 100644 DryWetMidi/Tools/CsvSerializer/Objects/CsvChord.cs rename DryWetMidi/Tools/{CsvConverter/MidiFile/FromCsv/TimedMidiEvent.cs => CsvSerializer/Objects/CsvEvent.cs} (58%) create mode 100644 DryWetMidi/Tools/CsvSerializer/Objects/CsvNote.cs create mode 100644 DryWetMidi/Tools/CsvSerializer/Objects/CsvObject.cs create mode 100644 DryWetMidi/Tools/CsvSerializer/Settings/CsvBytesArrayFormat.cs create mode 100644 DryWetMidi/Tools/CsvSerializer/Settings/CsvNoteFormat.cs create mode 100644 DryWetMidi/Tools/CsvSerializer/Settings/CsvSerializationSettings.cs create mode 100644 DryWetMidi/Tools/CsvSerializer/ToCsv/CsvSerializer.Serialize.cs create mode 100644 DryWetMidi/Tools/CsvSerializer/ToCsv/CsvWriter.cs create mode 100644 DryWetMidi/Tools/CsvSerializer/ToCsv/EventParametersProvider.cs diff --git a/Docs/articles/high-level-managing/Getting-objects.md b/Docs/articles/high-level-managing/Getting-objects.md index f28a8120f..8f1f17bda 100644 --- a/Docs/articles/high-level-managing/Getting-objects.md +++ b/Docs/articles/high-level-managing/Getting-objects.md @@ -482,143 +482,104 @@ Currently `GetObjects` can build objects of the following types: ### Rests -Let's see on rests building in details, since `GetObjects` is the only way to get them. First of all, all `GetObjects` overloads accept settings as an instance of the [ObjectDetectionSettings](xref:Melanchall.DryWetMidi.Interaction.ObjectDetectionSettings) class. Most of its properties are already discussed in previous sections on [notes building settings](#settings) and [chords building ones](#settings-1). But there is the [RestDetectionSettings](xref:Melanchall.DryWetMidi.Interaction.ObjectDetectionSettings.RestDetectionSettings) property which controls how rests should be detected. +To build rests you need to use extension methods from the [RestsUtilities](xref:Melanchall.DryWetMidi.Interaction.RestsUtilities) class. + +If you take a look into the class, you'll discover two methods – [WithRests](xref:Melanchall.DryWetMidi.Interaction.RestsUtilities.WithRests*) and [GetRests](xref:Melanchall.DryWetMidi.Interaction.RestsUtilities.GetRests*). The first one adds rests to a collection of objects you've passed to the method. The second method returns rests only. + +It will be much easier to understand how rests building works with examples. So let's look on [WithRests](xref:Melanchall.DryWetMidi.Interaction.RestsUtilities.WithRests*) (there is no great value to discuss [GetRests](xref:Melanchall.DryWetMidi.Interaction.RestsUtilities.GetRests*) since it works in the same way but just returns rests only). Supposing we have following notes (with two different note numbers on two different channels): ![GetObjects-Rests-Initial](images/Getting-objects-GetObjects-Rests-Initial.png) -[RestDetectionSettings](xref:Melanchall.DryWetMidi.Interaction.RestDetectionSettings) provides [RestSeparationPolicy](xref:Melanchall.DryWetMidi.Interaction.RestDetectionSettings.RestSeparationPolicy) property which determines a rule for creating rests. Now we'll see how each possible value of the policy affects the result of rests building. +Using following code: -Using [NoSeparation](xref:Melanchall.DryWetMidi.Interaction.RestSeparationPolicy.NoSeparation) (which is the default value) we'll get only one rest: +```csharp +var notesAndRests = notes + .WithRests(new RestDetectionSettings + { + KeySelector = obj => 0 + }); +``` + +we'll get only one rest: ![GetObjects-Rests-NoSeparation](images/Getting-objects-GetObjects-Rests-NoSeparation.png) -_"No separation"_ means _"there is no difference between channels and note numbers"_ so rests will be constructed only at spaces where there are no notes at all (with any channels and any note numbers). +An important concept we need to discuss is a key selection. Key is used to calculate rests. Rests are always calculated only between objects with the same key. If an object with different key is encountered, rests will be calculated for that key. -With [SeparateByChannel](xref:Melanchall.DryWetMidi.Interaction.RestSeparationPolicy.SeparateByChannel) we'll get three rests: +In the code above we're saying: _The key of each object is 0_. So for the rests building algorithm all objects are same, there is no difference between channels and note numbers, for example. So rests will be constructed only at spaces where there are no notes at all (with any channels and any note numbers). -![GetObjects-Rests-SeparateByChannel](images/Getting-objects-GetObjects-Rests-SeparateByChannel.png) +Using following code: -So rests are separated by channels only. Note number of a note doesn't matter, all numbers are treated as the same one. So rests will be constructed for each channel at spaces where there are no notes (with any note numbers). +```csharp +var notesAndRests = notes + .WithRests(new RestDetectionSettings + { + KeySelector = obj => (obj as Note)?.Channel + }); +``` -With [SeparateByNoteNumber](xref:Melanchall.DryWetMidi.Interaction.RestSeparationPolicy.SeparateByNoteNumber) we'll get following rests: + we'll get three rests now: -![GetObjects-Rests-SeparateByNoteNumber](images/Getting-objects-GetObjects-Rests-SeparateByNoteNumber.png) +![GetObjects-Rests-SeparateByChannel](images/Getting-objects-GetObjects-Rests-SeparateByChannel.png) -As you can see rests now are separated by note number (channel doesn't matter). So rests will be constructed for each note number at spaces where there are no notes (with any channel). +So rests are separated by channels. Channel is the key of an object. Note number of a note doesn't matter, all numbers are treated as the same one. So rests will be constructed separately for each channel at spaces where there are no notes (with any note numbers). -With [SeparateByChannelAndNoteNumber](xref:Melanchall.DryWetMidi.Interaction.RestSeparationPolicy.SeparateByChannelAndNoteNumber) we'll get rests at every "free" space: +The key for which a rest has been built will be store in the [Key](xref:Melanchall.DryWetMidi.Interaction.Rest.Key) property of [Rest](xref:Melanchall.DryWetMidi.Interaction.Rest) class. `notesAndRests` is a collection containing both `notes` and calculated rests, and elements of this collection are sorted by their times. -![GetObjects-Rests-SeparateByChannelAndNoteNumber](images/Getting-objects-GetObjects-Rests-SeparateByChannelAndNoteNumber.png) +Note that you can build rests for objects of different types. Why not to get chords from a MIDI file and add rests between them? -Let's see all these processes in action with a small program: +```csharp +var chordsAndRests = midiFile + .GetObjects(ObjectType.Chord) + .WithRests(new RestDetectionSettings + { + KeySelector = obj => (obj as Chord)?.Channel + }); +``` + +And a couple of words about return value of key selector. If `null` is returned, an object won't participate in rests building process. It allows you to have rests for desired objects only. For example: ```csharp -using System; -using System.Collections.Generic; -using System.Linq; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Interaction; -using NoteName = Melanchall.DryWetMidi.MusicTheory.NoteName; +var notesAndChordsAndRests = midiFile + .GetObjects(ObjectType.Note | ObjectType.Chord) + .WithRests(new RestDetectionSettings + { + KeySelector = obj => (obj as Note)?.Channel + }); +``` -namespace DwmExamples -{ - class Program +Here we specify that rests will be built for notes only (key selector will return `null` for an object other than note). So the result collection will have chords, notes and rests between notes with channel as the key. + +And a couple of additional examples with notes presented on the picture above. + +Code: + +```csharp +var notesAndRests = notes + .WithRests(new RestDetectionSettings { - static void Main(string[] args) - { - var ch0 = (FourBitNumber)0; - var a2Ch0Notes = new[] - { - new Note(NoteName.A, 2, 3, 0) { Channel = ch0 }, - new Note(NoteName.A, 2, 2, 6) { Channel = ch0 }, - new Note(NoteName.A, 2, 2, 8) { Channel = ch0 }, - new Note(NoteName.A, 2, 3, 11) { Channel = ch0 } - }; - var b1Ch0Notes = new[] - { - new Note(NoteName.B, 1, 3, 1) { Channel = ch0 }, - new Note(NoteName.B, 1, 4, 4) { Channel = ch0 }, - new Note(NoteName.B, 1, 1, 13) { Channel = ch0 } - }; + KeySelector = obj => (obj as Note)?.NoteNumber + }); +``` - var ch1 = (FourBitNumber)1; - var a2Ch1Notes = new[] - { - new Note(NoteName.A, 2, 2, 0) { Channel = ch1 }, - new Note(NoteName.A, 2, 2, 7) { Channel = ch1 } - }; - var b1Ch1Notes = new[] - { - new Note(NoteName.B, 1, 3, 1) { Channel = ch1 }, - new Note(NoteName.B, 1, 3, 5) { Channel = ch1 }, - new Note(NoteName.B, 1, 2, 12) { Channel = ch1 } - }; - - var notes = a2Ch0Notes - .Concat(b1Ch0Notes) - .Concat(a2Ch1Notes) - .Concat(b1Ch1Notes) - .ToArray(); - - WriteRests(notes, RestSeparationPolicy.NoSeparation); - WriteRests(notes, RestSeparationPolicy.SeparateByChannel); - WriteRests(notes, RestSeparationPolicy.SeparateByNoteNumber); - WriteRests(notes, RestSeparationPolicy.SeparateByChannelAndNoteNumber); +Rests: - Console.ReadKey(); - } +![GetObjects-Rests-SeparateByNoteNumber](images/Getting-objects-GetObjects-Rests-SeparateByNoteNumber.png) - private static void WriteRests( - ICollection notes, - RestSeparationPolicy restSeparationPolicy) - { - var rests = notes - .GetObjects( - ObjectType.Rest, - new ObjectDetectionSettings - { - RestDetectionSettings = new RestDetectionSettings - { - RestSeparationPolicy = restSeparationPolicy - } - }) - .Cast() - .ToArray(); +As you can see rests now are separated by note number (channel doesn't matter). So rests will be constructed for each note number at spaces where there are no notes (with any channel). - Console.WriteLine($"Rests by {restSeparationPolicy} policy:"); +Code: - foreach (var rest in rests) - { - Console.WriteLine($"[{rest.Length}] at [{rest.Time}] (note number = {rest.NoteNumber}, channel = {rest.Channel})"); - } - } - } -} +```csharp +var notesAndRests = notes + .WithRests(new RestDetectionSettings + { + KeySelector = obj => ((obj as Note)?.NoteNumber, (obj as Note)?.NoteNumber) + }); ``` -Output is: +Now we'll get rests at every "free" space (since the key is a pair of channel and note's number): -```text -Rests by NoSeparation policy: -[1] at [10] (note number = , channel = ) -Rests by SeparateByChannel policy: -[1] at [4] (note number = , channel = 1) -[3] at [9] (note number = , channel = 1) -[1] at [10] (note number = , channel = 0) -Rests by SeparateByNoteNumber policy: -[1] at [0] (note number = 35, channel = ) -[3] at [3] (note number = 45, channel = ) -[4] at [8] (note number = 35, channel = ) -[1] at [10] (note number = 45, channel = ) -Rests by SeparateByChannelAndNoteNumber policy: -[1] at [0] (note number = 35, channel = 0) -[1] at [0] (note number = 35, channel = 1) -[5] at [2] (note number = 45, channel = 1) -[3] at [3] (note number = 45, channel = 0) -[1] at [4] (note number = 35, channel = 1) -[4] at [8] (note number = 35, channel = 1) -[5] at [8] (note number = 35, channel = 0) -[1] at [10] (note number = 45, channel = 0) -``` +![GetObjects-Rests-SeparateByChannelAndNoteNumber](images/Getting-objects-GetObjects-Rests-SeparateByChannelAndNoteNumber.png) \ No newline at end of file diff --git a/Docs/articles/tools/MIDI-file-splitting.md b/Docs/articles/tools/MIDI-file-splitting.md index cbec85c2b..5056407da 100644 --- a/Docs/articles/tools/MIDI-file-splitting.md +++ b/Docs/articles/tools/MIDI-file-splitting.md @@ -38,7 +38,6 @@ To split a file by objects the tool needs to determine the key of each object. O |[TimedEvent](xref:Melanchall.DryWetMidi.Interaction.ObjectType.TimedEvent)|The type of the underlying event ([EventType](xref:Melanchall.DryWetMidi.Core.MidiEvent.EventType) of [TimedEvent.Event](xref:Melanchall.DryWetMidi.Interaction.TimedEvent.Event)).| |[Note](xref:Melanchall.DryWetMidi.Interaction.ObjectType.Note)|Pair of the [channel](xref:Melanchall.DryWetMidi.Interaction.Note.Channel) and [note number](xref:Melanchall.DryWetMidi.Interaction.Note.NoteNumber) of a note.| |[Chord](xref:Melanchall.DryWetMidi.Interaction.ObjectType.Chord)|Collection of keys of the underlying [notes](xref:Melanchall.DryWetMidi.Interaction.Chord.Notes).| -|[Rest](xref:Melanchall.DryWetMidi.Interaction.ObjectType.Rest)|Pair of the [channel](xref:Melanchall.DryWetMidi.Interaction.Rest.Channel) and [note number](xref:Melanchall.DryWetMidi.Interaction.Rest.NoteNumber) of a rest.| You can alter key calculation logic providing custom key selector. For example, to separate notes by only note number ignoring a note's channel: diff --git a/Docs/obsolete/OBS6/description.md b/Docs/obsolete/OBS6/description.md index 685851dde..bc76addc4 100644 --- a/Docs/obsolete/OBS6/description.md +++ b/Docs/obsolete/OBS6/description.md @@ -4,7 +4,7 @@ Methods from `GetNotesAndRestsUtilities` are now obsolete and you should use `Ge var notesAndRests = midiFile.GetObjects(ObjectType.Note | ObjectType.Rest); ``` -[RestSeparationPolicy](xref:Melanchall.DryWetMidi.Interaction.RestSeparationPolicy) can be specified via [ObjectDetectionSettings](xref:Melanchall.DryWetMidi.Interaction.ObjectDetectionSettings): +`RestSeparationPolicy` can be specified via [ObjectDetectionSettings](xref:Melanchall.DryWetMidi.Interaction.ObjectDetectionSettings): ```csharp var notesAndRests = midiFile.GetObjects( 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/Interaction/GetObjects/GetObjectsUtilitiesTests.NotesAndRests.cs b/DryWetMidi.Tests/Interaction/GetObjects/GetObjectsUtilitiesTests.NotesAndRests.cs deleted file mode 100644 index a32ed86c5..000000000 --- a/DryWetMidi.Tests/Interaction/GetObjects/GetObjectsUtilitiesTests.NotesAndRests.cs +++ /dev/null @@ -1,486 +0,0 @@ -using System.Collections.Generic; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Core; -using Melanchall.DryWetMidi.Interaction; -using NUnit.Framework; - -namespace Melanchall.DryWetMidi.Tests.Interaction -{ - [TestFixture] - public sealed partial class GetObjectsUtilitiesTests - { - #region Test methods - - [TestCase(10, 10, 50, 50)] - [TestCase(10, 2, 50, 50)] - [TestCase(10, 10, 50, 100)] - [TestCase(10, 2, 50, 100)] - public void GetObjects_NotesAndRests_NoSeparation_FromNotes( - byte channel1, - byte channel2, - byte noteNumber1, - byte noteNumber2) - { - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.NoSeparation, - inputObjects: new ITimedObject[] - { - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 1000, 100000) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 10, 100100) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 10, 110000) { Channel = (FourBitNumber)channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, null, null), - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Rest(130, 170, null, null), - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Rest(350, 650, null, null), - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = (FourBitNumber)channel1 }, - new Rest(2300, 7700, null, null), - new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = (FourBitNumber)channel2 }, - new Rest(11000, 89000, null, null), - new Note((SevenBitNumber)noteNumber1, 1000, 100000) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 10, 100100) { Channel = (FourBitNumber)channel2 }, - new Rest(101000, 9000, null, null), - new Note((SevenBitNumber)noteNumber1, 10, 110000) { Channel = (FourBitNumber)channel1 }, - }); - } - - [TestCase(10, 10)] - [TestCase(10, 50)] - public void GetObjects_NotesAndRests_SeparateByChannel_SingleChannel_FromNotes( - byte noteNumber1, - byte noteNumber2) - { - var channel = (FourBitNumber)10; - - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannel, - inputObjects: new ITimedObject[] - { - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel }, - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel }, - new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = channel }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel, null), - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel }, - new Rest(130, 170, channel, null), - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, - new Rest(350, 650, channel, null), - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel }, - new Rest(2300, 7700, channel, null), - new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = channel }, - }); - } - - [TestCase(10, 10)] - [TestCase(10, 50)] - public void GetObjects_NotesAndRests_SeparateByChannel_DifferentChannels_FromNotes( - byte noteNumber1, - byte noteNumber2) - { - var channel1 = (FourBitNumber)10; - var channel2 = (FourBitNumber)2; - - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannel, - inputObjects: new ITimedObject[] - { - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel2 }, - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel2 }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel2 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel1, null), - new Rest(0, 30, channel2, null), - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel2 }, - new Rest(110, 190, channel1, null), - new Rest(130, 870, channel2, null), - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel1 }, - new Rest(350, 850, channel1, null), - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel2 }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel2 }, - }); - } - - [TestCase(10, 10)] - [TestCase(10, 5)] - public void GetObjects_NotesAndRests_SeparateByNoteNumber_SingleNoteNumber_FromNotes( - byte channel1, - byte channel2) - { - var noteNumber = (SevenBitNumber)10; - - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.SeparateByNoteNumber, - inputObjects: new ITimedObject[] - { - new Note(noteNumber, 100, 10) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Note(noteNumber, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 500, 1000) { Channel = (FourBitNumber)channel1 }, - new Note(noteNumber, 150, 1200) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 1000, 1300) { Channel = (FourBitNumber)channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, null, noteNumber), - new Note(noteNumber, 100, 10) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Rest(130, 170, null, noteNumber), - new Note(noteNumber, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Rest(350, 650, null, noteNumber), - new Note(noteNumber, 500, 1000) { Channel = (FourBitNumber)channel1 }, - new Note(noteNumber, 150, 1200) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 1000, 1300) { Channel = (FourBitNumber)channel1 }, - }); - } - - [TestCase(10, 10)] - [TestCase(10, 5)] - public void GetObjects_NotesAndRests_SeparateByNoteNumber_DifferentNoteNumbers_FromNotes( - byte channel1, - byte channel2) - { - var noteNumber1 = (SevenBitNumber)10; - var noteNumber2 = (SevenBitNumber)100; - - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.SeparateByNoteNumber, - inputObjects: new ITimedObject[] - { - new Note(noteNumber1, 100, 0) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber2, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Note(noteNumber1, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber2, 500, 1000) { Channel = (FourBitNumber)channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Note(noteNumber1, 100, 0) { Channel = (FourBitNumber)channel2 }, - new Rest(0, 30, null, noteNumber2), - new Note(noteNumber2, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Rest(100, 200, null, noteNumber1), - new Rest(130, 870, null, noteNumber2), - new Note(noteNumber1, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber2, 500, 1000) { Channel = (FourBitNumber)channel1 }, - }); - } - - [Test] - public void GetObjects_NotesAndRests_SeparateByChannelAndNoteNumber_FromNotes() - { - var noteNumber1 = (SevenBitNumber)10; - var noteNumber2 = (SevenBitNumber)100; - var channel1 = (FourBitNumber)10; - var channel2 = (FourBitNumber)2; - - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannelAndNoteNumber, - inputObjects: new ITimedObject[] - { - new Note(noteNumber1, 100, 10) { Channel = channel1 }, - new Note(noteNumber2, 100, 30) { Channel = channel1 }, - new Note(noteNumber1, 50, 300) { Channel = channel2 }, - new Note(noteNumber2, 500, 1000) { Channel = channel2 }, - new Note(noteNumber1, 150, 1200) { Channel = channel1 }, - new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel1, noteNumber1), - new Rest(0, 30, channel1, noteNumber2), - new Rest(0, 300, channel2, noteNumber1), - new Rest(0, 1000, channel2, noteNumber2), - new Note(noteNumber1, 100, 10) { Channel = channel1 }, - new Note(noteNumber2, 100, 30) { Channel = channel1 }, - new Rest(110, 1090, channel1, noteNumber1), - new Rest(130, 1170, channel1, noteNumber2), - new Note(noteNumber1, 50, 300) { Channel = channel2 }, - new Note(noteNumber2, 500, 1000) { Channel = channel2 }, - new Note(noteNumber1, 150, 1200) { Channel = channel1 }, - new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, - }); - } - - [TestCase(10, 10, 50, 50)] - [TestCase(10, 2, 50, 50)] - [TestCase(10, 10, 50, 100)] - [TestCase(10, 2, 50, 100)] - public void GetObjects_NotesAndRests_NoSeparation_FromNotesAndTimedEvents( - byte channel1, - byte channel2, - byte noteNumber1, - byte noteNumber2) - { - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.NoSeparation, - inputObjects: new ITimedObject[] - { - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = (FourBitNumber)channel2 }, - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 30), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 130), - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = (FourBitNumber)channel1 }, - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 1200), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 1350), - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 1000, 100000) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 10, 100100) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 10, 110000) { Channel = (FourBitNumber)channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, null, null), - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Rest(130, 170, null, null), - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Rest(350, 650, null, null), - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = (FourBitNumber)channel1 }, - new Rest(2300, 7700, null, null), - new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = (FourBitNumber)channel2 }, - new Rest(11000, 89000, null, null), - new Note((SevenBitNumber)noteNumber1, 1000, 100000) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 10, 100100) { Channel = (FourBitNumber)channel2 }, - new Rest(101000, 9000, null, null), - new Note((SevenBitNumber)noteNumber1, 10, 110000) { Channel = (FourBitNumber)channel1 }, - }); - } - - [TestCase(10, 10)] - [TestCase(10, 50)] - public void GetObjects_NotesAndRests_SeparateByChannel_SingleChannel_FromNotesAndTimedEvents( - byte noteNumber1, - byte noteNumber2) - { - var channel = (FourBitNumber)10; - - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannel, - inputObjects: new ITimedObject[] - { - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel }, - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel }, 1000), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel }, 1500), - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel }, 1300), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel }, 2300), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = channel }, 10000), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = channel }, 11000), - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel, null), - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel }, - new Rest(130, 170, channel, null), - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, - new Rest(350, 650, channel, null), - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel }, - new Rest(2300, 7700, channel, null), - new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = channel }, - }, - noteDetectionSettings: new NoteDetectionSettings { NoteStartDetectionPolicy = NoteStartDetectionPolicy.FirstNoteOn }); - } - - [TestCase(10, 10)] - [TestCase(10, 50)] - public void GetObjects_NotesAndRests_SeparateByChannel_DifferentChannels_FromNotesAndTimedEvents( - byte noteNumber1, - byte noteNumber2) - { - var channel1 = (FourBitNumber)10; - var channel2 = (FourBitNumber)2; - - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannel, - inputObjects: new ITimedObject[] - { - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel1 }, 10), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel1 }, 110), - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel2 }, - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel2 }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel1 }, - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel2 }, 1300), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel2 }, 2300), - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel1, null), - new Rest(0, 30, channel2, null), - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel2 }, - new Rest(110, 190, channel1, null), - new Rest(130, 870, channel2, null), - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel1 }, - new Rest(350, 850, channel1, null), - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel2 }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel2 }, - }); - } - - [TestCase(10, 10)] - [TestCase(10, 5)] - public void GetObjects_NotesAndRests_SeparateByNoteNumber_SingleNoteNumber_FromNotesAndTimedEvents( - byte channel1, - byte channel2) - { - var noteNumber = (SevenBitNumber)10; - - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.SeparateByNoteNumber, - inputObjects: new ITimedObject[] - { - new Note(noteNumber, 100, 10) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 100, 30) { Channel = (FourBitNumber)channel1 }, - new TimedEvent(new NoteOnEvent(noteNumber, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 300), - new TimedEvent(new NoteOffEvent(noteNumber, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 350), - new TimedEvent(new NoteOnEvent(noteNumber, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 1000), - new TimedEvent(new NoteOffEvent(noteNumber, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 1500), - new Note(noteNumber, 150, 1200) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 1000, 1300) { Channel = (FourBitNumber)channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, null, noteNumber), - new Note(noteNumber, 100, 10) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Rest(130, 170, null, noteNumber), - new Note(noteNumber, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Rest(350, 650, null, noteNumber), - new Note(noteNumber, 500, 1000) { Channel = (FourBitNumber)channel1 }, - new Note(noteNumber, 150, 1200) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 1000, 1300) { Channel = (FourBitNumber)channel1 }, - }); - } - - [TestCase(10, 10)] - [TestCase(10, 5)] - public void GetObjects_NotesAndRests_SeparateByNoteNumber_DifferentNoteNumbers_FromNotesAndTimedEvents( - byte channel1, - byte channel2) - { - var noteNumber1 = (SevenBitNumber)10; - var noteNumber2 = (SevenBitNumber)100; - - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.SeparateByNoteNumber, - inputObjects: new ITimedObject[] - { - new Note(noteNumber1, 100, 0) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber2, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Note(noteNumber1, 50, 300) { Channel = (FourBitNumber)channel2 }, - new TimedEvent(new NoteOnEvent(noteNumber2, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 1000), - new TimedEvent(new NoteOffEvent(noteNumber2, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 1500), - }, - outputObjects: new ITimedObject[] - { - new Note(noteNumber1, 100, 0) { Channel = (FourBitNumber)channel2 }, - new Rest(0, 30, null, noteNumber2), - new Note(noteNumber2, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Rest(100, 200, null, noteNumber1), - new Rest(130, 870, null, noteNumber2), - new Note(noteNumber1, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber2, 500, 1000) { Channel = (FourBitNumber)channel1 }, - }); - } - - [Test] - public void GetObjects_NotesAndRests_SeparateByChannelAndNoteNumber_FromNotesAndTimedEvents() - { - var noteNumber1 = (SevenBitNumber)10; - var noteNumber2 = (SevenBitNumber)100; - var channel1 = (FourBitNumber)10; - var channel2 = (FourBitNumber)2; - - GetObjects_NotesAndRests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannelAndNoteNumber, - inputObjects: new ITimedObject[] - { - new TimedEvent(new NoteOnEvent(noteNumber1, Note.DefaultVelocity) { Channel = channel1 }, 10), - new TimedEvent(new NoteOffEvent(noteNumber1, SevenBitNumber.MinValue) { Channel = channel1 }, 110), - new Note(noteNumber2, 100, 30) { Channel = channel1 }, - new Note(noteNumber1, 50, 300) { Channel = channel2 }, - new Note(noteNumber2, 500, 1000) { Channel = channel2 }, - new Note(noteNumber1, 150, 1200) { Channel = channel1 }, - new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel1, noteNumber1), - new Rest(0, 30, channel1, noteNumber2), - new Rest(0, 300, channel2, noteNumber1), - new Rest(0, 1000, channel2, noteNumber2), - new Note(noteNumber1, 100, 10) { Channel = channel1 }, - new Note(noteNumber2, 100, 30) { Channel = channel1 }, - new Rest(110, 1090, channel1, noteNumber1), - new Rest(130, 1170, channel1, noteNumber2), - new Note(noteNumber1, 50, 300) { Channel = channel2 }, - new Note(noteNumber2, 500, 1000) { Channel = channel2 }, - new Note(noteNumber1, 150, 1200) { Channel = channel1 }, - new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, - }); - } - - #endregion - - #region Private methods - - private void GetObjects_NotesAndRests( - RestSeparationPolicy restSeparationPolicy, - IEnumerable inputObjects, - IEnumerable outputObjects, - NoteDetectionSettings noteDetectionSettings = null) - { - GetObjects( - inputObjects, - outputObjects, - ObjectType.Note | ObjectType.Rest, - new ObjectDetectionSettings - { - RestDetectionSettings = new RestDetectionSettings - { - RestSeparationPolicy = restSeparationPolicy - }, - NoteDetectionSettings = noteDetectionSettings ?? new NoteDetectionSettings() - }); - } - - #endregion - } -} diff --git a/DryWetMidi.Tests/Interaction/GetObjects/GetObjectsUtilitiesTests.Rests.cs b/DryWetMidi.Tests/Interaction/GetObjects/GetObjectsUtilitiesTests.Rests.cs deleted file mode 100644 index 91f58ff30..000000000 --- a/DryWetMidi.Tests/Interaction/GetObjects/GetObjectsUtilitiesTests.Rests.cs +++ /dev/null @@ -1,433 +0,0 @@ -using System.Collections.Generic; -using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Core; -using Melanchall.DryWetMidi.Interaction; -using NUnit.Framework; - -namespace Melanchall.DryWetMidi.Tests.Interaction -{ - [TestFixture] - public sealed partial class GetObjectsUtilitiesTests - { - #region Test methods - - [TestCase(10, 10, 50, 50)] - [TestCase(10, 2, 50, 50)] - [TestCase(10, 10, 50, 100)] - [TestCase(10, 2, 50, 100)] - public void GetObjects_Rests_NoSeparation_FromNotes( - byte channel1, - byte channel2, - byte noteNumber1, - byte noteNumber2) - { - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.NoSeparation, - inputObjects: new ITimedObject[] - { - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 1000, 100000) { Channel = (FourBitNumber)channel1 }, - new Note((SevenBitNumber)noteNumber2, 10, 100100) { Channel = (FourBitNumber)channel2 }, - new Note((SevenBitNumber)noteNumber1, 10, 110000) { Channel = (FourBitNumber)channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, null, null), - new Rest(130, 170, null, null), - new Rest(350, 650, null, null), - new Rest(2300, 7700, null, null), - new Rest(11000, 89000, null, null), - new Rest(101000, 9000, null, null), - }); - } - - [TestCase(10, 10)] - [TestCase(10, 50)] - public void GetObjects_Rests_SeparateByChannel_SingleChannel_FromNotes( - byte noteNumber1, - byte noteNumber2) - { - var channel = (FourBitNumber)10; - - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannel, - inputObjects: new ITimedObject[] - { - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel }, - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel }, - new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = channel }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel, null), - new Rest(130, 170, channel, null), - new Rest(350, 650, channel, null), - new Rest(2300, 7700, channel, null), - }); - } - - [TestCase(10, 10)] - [TestCase(10, 50)] - public void GetObjects_Rests_SeparateByChannel_DifferentChannels_FromNotes( - byte noteNumber1, - byte noteNumber2) - { - var channel1 = (FourBitNumber)10; - var channel2 = (FourBitNumber)2; - - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannel, - inputObjects: new ITimedObject[] - { - new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel2 }, - new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel2 }, - new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel1 }, - new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel2 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel1, null), - new Rest(0, 30, channel2, null), - new Rest(110, 190, channel1, null), - new Rest(130, 870, channel2, null), - new Rest(350, 850, channel1, null), - }); - } - - [TestCase(10, 10)] - [TestCase(10, 5)] - public void GetObjects_Rests_SeparateByNoteNumber_SingleNoteNumber_FromNotes( - byte channel1, - byte channel2) - { - var noteNumber = (SevenBitNumber)10; - - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.SeparateByNoteNumber, - inputObjects: new ITimedObject[] - { - new Note(noteNumber, 100, 10) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Note(noteNumber, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 500, 1000) { Channel = (FourBitNumber)channel1 }, - new Note(noteNumber, 150, 1200) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber, 1000, 1300) { Channel = (FourBitNumber)channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, null, noteNumber), - new Rest(130, 170, null, noteNumber), - new Rest(350, 650, null, noteNumber), - }); - } - - [TestCase(10, 10)] - [TestCase(10, 5)] - public void GetObjects_Rests_SeparateByNoteNumber_DifferentNoteNumbers_FromNotes( - byte channel1, - byte channel2) - { - var noteNumber1 = (SevenBitNumber)10; - var noteNumber2 = (SevenBitNumber)100; - - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.SeparateByNoteNumber, - inputObjects: new ITimedObject[] - { - new Note(noteNumber1, 100, 0) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber2, 100, 30) { Channel = (FourBitNumber)channel1 }, - new Note(noteNumber1, 50, 300) { Channel = (FourBitNumber)channel2 }, - new Note(noteNumber2, 500, 1000) { Channel = (FourBitNumber)channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 30, null, noteNumber2), - new Rest(100, 200, null, noteNumber1), - new Rest(130, 870, null, noteNumber2), - }); - } - - [Test] - public void GetObjects_Rests_SeparateByChannelAndNoteNumber_FromNotes() - { - var noteNumber1 = (SevenBitNumber)10; - var noteNumber2 = (SevenBitNumber)100; - var channel1 = (FourBitNumber)10; - var channel2 = (FourBitNumber)2; - - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannelAndNoteNumber, - inputObjects: new ITimedObject[] - { - new Note(noteNumber1, 100, 10) { Channel = channel1 }, - new Note(noteNumber2, 100, 30) { Channel = channel1 }, - new Note(noteNumber1, 50, 300) { Channel = channel2 }, - new Note(noteNumber2, 500, 1000) { Channel = channel2 }, - new Note(noteNumber1, 150, 1200) { Channel = channel1 }, - new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel1, noteNumber1), - new Rest(0, 30, channel1, noteNumber2), - new Rest(0, 300, channel2, noteNumber1), - new Rest(0, 1000, channel2, noteNumber2), - new Rest(110, 1090, channel1, noteNumber1), - new Rest(130, 1170, channel1, noteNumber2), - }); - } - - [TestCase(10, 10, 50, 50)] - [TestCase(10, 2, 50, 50)] - [TestCase(10, 10, 50, 100)] - [TestCase(10, 2, 50, 100)] - public void GetObjects_Rests_NoSeparation_FromTimedEvents( - byte channel1, - byte channel2, - byte noteNumber1, - byte noteNumber2) - { - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.NoSeparation, - inputObjects: new ITimedObject[] - { - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 10), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 110), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 30), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 130), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 300), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 350), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 1000), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 1500), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 1200), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 1350), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 1300), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 2300), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 10000), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 11000), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 100000), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 101000), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 100100), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 100110), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 110000), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 110010), - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, null, null), - new Rest(130, 170, null, null), - new Rest(350, 650, null, null), - new Rest(2300, 7700, null, null), - new Rest(11000, 89000, null, null), - new Rest(101000, 9000, null, null), - }); - } - - [TestCase(10, 10)] - [TestCase(10, 50)] - public void GetObjects_Rests_SeparateByChannel_SingleChannel_FromTimedEvents( - byte noteNumber1, - byte noteNumber2) - { - var channel = (FourBitNumber)10; - - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannel, - inputObjects: new ITimedObject[] - { - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel }, 10), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel }, 110), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel }, 30), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel }, 130), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = channel }, 300), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = channel }, 350), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel }, 1000), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel }, 1500), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = channel }, 1200), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = channel }, 1350), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel }, 1300), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel }, 2300), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = channel }, 10000), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = channel }, 11000), - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel, null), - new Rest(130, 170, channel, null), - new Rest(350, 650, channel, null), - new Rest(2300, 7700, channel, null), - }); - } - - [TestCase(10, 10)] - [TestCase(10, 50)] - public void GetObjects_Rests_SeparateByChannel_DifferentChannels_FromTimedEvents( - byte noteNumber1, - byte noteNumber2) - { - var channel1 = (FourBitNumber)10; - var channel2 = (FourBitNumber)2; - - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannel, - inputObjects: new ITimedObject[] - { - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel1 }, 10), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel1 }, 110), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel2 }, 30), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel2 }, 130), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = channel1 }, 300), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = channel1 }, 350), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel2 }, 1000), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel2 }, 1500), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber2, Note.DefaultVelocity) { Channel = channel1 }, 1200), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber2, SevenBitNumber.MinValue) { Channel = channel1 }, 1350), - new TimedEvent(new NoteOnEvent((SevenBitNumber)noteNumber1, Note.DefaultVelocity) { Channel = channel2 }, 1300), - new TimedEvent(new NoteOffEvent((SevenBitNumber)noteNumber1, SevenBitNumber.MinValue) { Channel = channel2 }, 2300), - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel1, null), - new Rest(0, 30, channel2, null), - new Rest(110, 190, channel1, null), - new Rest(130, 870, channel2, null), - new Rest(350, 850, channel1, null), - }); - } - - [TestCase(10, 10)] - [TestCase(10, 5)] - public void GetObjects_Rests_SeparateByNoteNumber_SingleNoteNumber_FromTimedEvents( - byte channel1, - byte channel2) - { - var noteNumber = (SevenBitNumber)10; - - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.SeparateByNoteNumber, - inputObjects: new ITimedObject[] - { - new TimedEvent(new NoteOnEvent(noteNumber, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 10), - new TimedEvent(new NoteOffEvent(noteNumber, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 110), - new TimedEvent(new NoteOnEvent(noteNumber, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 30), - new TimedEvent(new NoteOffEvent(noteNumber, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 130), - new TimedEvent(new NoteOnEvent(noteNumber, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 300), - new TimedEvent(new NoteOffEvent(noteNumber, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 350), - new TimedEvent(new NoteOnEvent(noteNumber, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 1000), - new TimedEvent(new NoteOffEvent(noteNumber, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 1500), - new TimedEvent(new NoteOnEvent(noteNumber, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 1200), - new TimedEvent(new NoteOffEvent(noteNumber, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 1350), - new TimedEvent(new NoteOnEvent(noteNumber, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 1300), - new TimedEvent(new NoteOffEvent(noteNumber, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 2300), - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, null, noteNumber), - new Rest(130, 170, null, noteNumber), - new Rest(350, 650, null, noteNumber), - }); - } - - [TestCase(10, 10)] - [TestCase(10, 5)] - public void GetObjects_Rests_SeparateByNoteNumber_DifferentNoteNumbers_FromTimedEvents( - byte channel1, - byte channel2) - { - var noteNumber1 = (SevenBitNumber)10; - var noteNumber2 = (SevenBitNumber)100; - - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.SeparateByNoteNumber, - inputObjects: new ITimedObject[] - { - new TimedEvent(new NoteOnEvent(noteNumber1, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 0), - new TimedEvent(new NoteOffEvent(noteNumber1, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 100), - new TimedEvent(new NoteOnEvent(noteNumber2, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 30), - new TimedEvent(new NoteOffEvent(noteNumber2, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 130), - new TimedEvent(new NoteOnEvent(noteNumber1, Note.DefaultVelocity) { Channel = (FourBitNumber)channel2 }, 300), - new TimedEvent(new NoteOffEvent(noteNumber1, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel2 }, 350), - new TimedEvent(new NoteOnEvent(noteNumber2, Note.DefaultVelocity) { Channel = (FourBitNumber)channel1 }, 1000), - new TimedEvent(new NoteOffEvent(noteNumber2, SevenBitNumber.MinValue) { Channel = (FourBitNumber)channel1 }, 1500), - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 30, null, noteNumber2), - new Rest(100, 200, null, noteNumber1), - new Rest(130, 870, null, noteNumber2), - }); - } - - [Test] - public void GetObjects_Rests_SeparateByChannelAndNoteNumber_FromTimedEvents() - { - var noteNumber1 = (SevenBitNumber)10; - var noteNumber2 = (SevenBitNumber)100; - var channel1 = (FourBitNumber)10; - var channel2 = (FourBitNumber)2; - - GetObjects_Rests( - restSeparationPolicy: RestSeparationPolicy.SeparateByChannelAndNoteNumber, - inputObjects: new ITimedObject[] - { - new TimedEvent(new NoteOnEvent(noteNumber1, Note.DefaultVelocity) { Channel = channel1 }, 10), - new TimedEvent(new NoteOffEvent(noteNumber1, SevenBitNumber.MinValue) { Channel = channel1 }, 110), - new TimedEvent(new NoteOnEvent(noteNumber2, Note.DefaultVelocity) { Channel = channel1 }, 30), - new TimedEvent(new NoteOffEvent(noteNumber2, SevenBitNumber.MinValue) { Channel = channel1 }, 130), - new TimedEvent(new NoteOnEvent(noteNumber1, Note.DefaultVelocity) { Channel = channel2 }, 300), - new TimedEvent(new NoteOffEvent(noteNumber1, SevenBitNumber.MinValue) { Channel = channel2 }, 350), - new TimedEvent(new NoteOnEvent(noteNumber2, Note.DefaultVelocity) { Channel = channel2 }, 1000), - new TimedEvent(new NoteOffEvent(noteNumber2, SevenBitNumber.MinValue) { Channel = channel2 }, 1500), - new TimedEvent(new NoteOnEvent(noteNumber1, Note.DefaultVelocity) { Channel = channel1 }, 1200), - new TimedEvent(new NoteOffEvent(noteNumber1, SevenBitNumber.MinValue) { Channel = channel1 }, 1350), - new TimedEvent(new NoteOnEvent(noteNumber2, Note.DefaultVelocity) { Channel = channel1 }, 1300), - new TimedEvent(new NoteOffEvent(noteNumber2, SevenBitNumber.MinValue) { Channel = channel1 }, 2300), - }, - outputObjects: new ITimedObject[] - { - new Rest(0, 10, channel1, noteNumber1), - new Rest(0, 30, channel1, noteNumber2), - new Rest(0, 300, channel2, noteNumber1), - new Rest(0, 1000, channel2, noteNumber2), - new Rest(110, 1090, channel1, noteNumber1), - new Rest(130, 1170, channel1, noteNumber2), - }); - } - - #endregion - - #region Private methods - - private void GetObjects_Rests( - RestSeparationPolicy restSeparationPolicy, - IEnumerable inputObjects, - IEnumerable outputObjects) - { - GetObjects( - inputObjects, - outputObjects, - ObjectType.Rest, - new ObjectDetectionSettings - { - RestDetectionSettings = new RestDetectionSettings - { - RestSeparationPolicy = restSeparationPolicy - } - }); - } - - #endregion - } -} diff --git a/DryWetMidi.Tests/Interaction/ObjectId/ObjectIdUtilitiesTests.cs b/DryWetMidi.Tests/Interaction/ObjectId/ObjectIdUtilitiesTests.cs index 400455e6f..befc19bd0 100644 --- a/DryWetMidi.Tests/Interaction/ObjectId/ObjectIdUtilitiesTests.cs +++ b/DryWetMidi.Tests/Interaction/ObjectId/ObjectIdUtilitiesTests.cs @@ -29,7 +29,7 @@ public void GetObjectId_AllObjectsTypes() ITimedObject obj = null; if (type == typeof(Rest)) - obj = new Rest(0, 100, null, null); + obj = new Rest(0, 100, null); else if (type == typeof(Note)) obj = new Note((SevenBitNumber)70); else if (type == typeof(TimedEvent)) diff --git a/DryWetMidi.Tests/Interaction/Rests/RestsUtilitiesTests.GetRests.cs b/DryWetMidi.Tests/Interaction/Rests/RestsUtilitiesTests.GetRests.cs new file mode 100644 index 000000000..414fc36f2 --- /dev/null +++ b/DryWetMidi.Tests/Interaction/Rests/RestsUtilitiesTests.GetRests.cs @@ -0,0 +1,452 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Interaction; +using Melanchall.DryWetMidi.Tests.Utilities; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Melanchall.DryWetMidi.Tests.Interaction +{ + [TestFixture] + public sealed partial class RestsUtilitiesTests + { + #region Constants + + private const int SameKey = 123; + + #endregion + + #region Test methods + + [TestCase(10, 10, 50, 50)] + [TestCase(10, 2, 50, 50)] + [TestCase(10, 10, 50, 100)] + [TestCase(10, 2, 50, 100)] + public void GetRests_Notes_SameKey( + byte channel1, + byte channel2, + byte noteNumber1, + byte noteNumber2) + { + GetRests( + keySelector: obj => SameKey, + inputObjects: new ITimedObject[] + { + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = (FourBitNumber)channel1 }, + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = (FourBitNumber)channel1 }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = (FourBitNumber)channel1 }, + new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 1000, 100000) { Channel = (FourBitNumber)channel1 }, + new Note((SevenBitNumber)noteNumber2, 10, 100100) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 10, 110000) { Channel = (FourBitNumber)channel1 }, + }, + expectedRests: new [] + { + new Rest(0, 10, SameKey), + new Rest(130, 170, SameKey), + new Rest(350, 650, SameKey), + new Rest(2300, 7700, SameKey), + new Rest(11000, 89000, SameKey), + new Rest(101000, 9000, SameKey), + }); + } + + [TestCase(10, 10)] + [TestCase(10, 50)] + public void GetRests_Notes_RestsByChannel_SameChannel( + byte noteNumber1, + byte noteNumber2) + { + var channel = (FourBitNumber)10; + + GetRests( + keySelector: obj => (obj as Note)?.Channel, + inputObjects: new ITimedObject[] + { + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = channel }, + }, + expectedRests: new[] + { + new Rest(0, 10, channel), + new Rest(130, 170, channel), + new Rest(350, 650, channel), + new Rest(2300, 7700, channel), + }); + } + + [TestCase(10, 10)] + [TestCase(10, 50)] + public void GetRests_Notes_RestsByChannel_DifferentChannels( + byte noteNumber1, + byte noteNumber2) + { + var channel1 = (FourBitNumber)10; + var channel2 = (FourBitNumber)2; + + GetRests( + keySelector: obj => (obj as Note)?.Channel, + inputObjects: new ITimedObject[] + { + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel1 }, + new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel2 }, + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel1 }, + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel2 }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel1 }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel2 }, + }, + expectedRests: new[] + { + new Rest(0, 10, channel1), + new Rest(0, 30, channel2), + new Rest(110, 190, channel1), + new Rest(130, 870, channel2), + new Rest(350, 850, channel1), + }); + } + + [TestCase(10, 10)] + [TestCase(10, 5)] + public void GetRests_Notes_RestsByNoteNumber_SameNoteNumber( + byte channel1, + byte channel2) + { + var noteNumber = (SevenBitNumber)10; + + GetRests( + keySelector: obj => (obj as Note)?.NoteNumber, + inputObjects: new ITimedObject[] + { + new Note(noteNumber, 100, 10) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber, 100, 30) { Channel = (FourBitNumber)channel1 }, + new Note(noteNumber, 50, 300) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber, 500, 1000) { Channel = (FourBitNumber)channel1 }, + new Note(noteNumber, 150, 1200) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber, 1000, 1300) { Channel = (FourBitNumber)channel1 }, + }, + expectedRests: new[] + { + new Rest(0, 10, noteNumber), + new Rest(130, 170, noteNumber), + new Rest(350, 650, noteNumber), + }); + } + + [TestCase(10, 10)] + [TestCase(10, 5)] + public void GetRests_Notes_RestsByNoteNumber_DifferentNoteNumbers( + byte channel1, + byte channel2) + { + var noteNumber1 = (SevenBitNumber)10; + var noteNumber2 = (SevenBitNumber)100; + + GetRests( + keySelector: obj => (obj as Note)?.NoteNumber, + inputObjects: new ITimedObject[] + { + new Note(noteNumber1, 100, 0) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber2, 100, 30) { Channel = (FourBitNumber)channel1 }, + new Note(noteNumber1, 50, 300) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = (FourBitNumber)channel1 }, + }, + expectedRests: new[] + { + new Rest(0, 30, noteNumber2), + new Rest(100, 200, noteNumber1), + new Rest(130, 870, noteNumber2), + }); + } + + [Test] + public void GetRests_Notes_RestsByChannelAndNoteNumber() + { + var noteNumber1 = (SevenBitNumber)10; + var noteNumber2 = (SevenBitNumber)100; + var channel1 = (FourBitNumber)10; + var channel2 = (FourBitNumber)2; + + GetRests( + keySelector: obj => ((obj as Note)?.Channel, (obj as Note)?.NoteNumber), + inputObjects: new ITimedObject[] + { + new Note(noteNumber1, 100, 10) { Channel = channel1 }, + new Note(noteNumber2, 100, 30) { Channel = channel1 }, + new Note(noteNumber1, 50, 300) { Channel = channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = channel2 }, + new Note(noteNumber1, 150, 1200) { Channel = channel1 }, + new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, + }, + expectedRests: new[] + { + new Rest(0, 10, (channel1, noteNumber1)), + new Rest(0, 30, (channel1, noteNumber2)), + new Rest(0, 300, (channel2, noteNumber1)), + new Rest(0, 1000, (channel2, noteNumber2)), + new Rest(110, 1090, (channel1, noteNumber1)), + new Rest(130, 1170, (channel1, noteNumber2)), + }); + } + + [Test] + public void GetRests_Notes_RestsByChannelAndNoteNumber_WithTimedEvents() + { + var noteNumber1 = (SevenBitNumber)10; + var noteNumber2 = (SevenBitNumber)100; + var channel1 = (FourBitNumber)10; + var channel2 = (FourBitNumber)2; + + GetRests( + keySelector: obj => obj is Note + ? (((Note)obj).Channel, ((Note)obj).NoteNumber) + : ((FourBitNumber, SevenBitNumber)?)null, + inputObjects: new ITimedObject[] + { + new Note(noteNumber1, 100, 10) { Channel = channel1 }, + new TimedEvent(new TextEvent("A"), 15), + new Note(noteNumber2, 100, 30) { Channel = channel1 }, + new TimedEvent(new TextEvent("B"), 120), + new Note(noteNumber1, 50, 300) { Channel = channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = channel2 }, + new TimedEvent(new TextEvent("C"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new Note(noteNumber1, 150, 1200) { Channel = channel1 }, + new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, + new TimedEvent(new TextEvent("E"), 2000), + }, + expectedRests: new[] + { + new Rest(0, 10, (channel1, noteNumber1)), + new Rest(0, 30, (channel1, noteNumber2)), + new Rest(0, 300, (channel2, noteNumber1)), + new Rest(0, 1000, (channel2, noteNumber2)), + new Rest(110, 1090, (channel1, noteNumber1)), + new Rest(130, 1170, (channel1, noteNumber2)), + }); + } + + [Test] + public void GetRests_TimedEvents_1() + { + GetRests( + keySelector: obj => ((TextEvent)((TimedEvent)obj).Event).Text.Contains("A"), + inputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("CA"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new TimedEvent(new TextEvent("E"), 2000), + }, + expectedRests: new[] + { + new Rest(0, 15, true), + new Rest(0, 120, false), + new Rest(15, 1085, true), + new Rest(120, 1030, false), + new Rest(1150, 850, false), + }); + } + + [Test] + public void GetRests_TimedEvents_2() + { + GetRests( + keySelector: obj => obj is TimedEvent ? SameKey : (int?)null, + inputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("C"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new TimedEvent(new TextEvent("E"), 2000), + }, + expectedRests: new[] + { + new Rest(0, 15, SameKey), + new Rest(15, 105, SameKey), + new Rest(120, 980, SameKey), + new Rest(1100, 50, SameKey), + new Rest(1150, 850, SameKey), + }); + } + + [Test] + public void GetRests_TimedEvents_3() + { + GetRests( + keySelector: obj => obj is TimedEvent ? SameKey : (int?)null, + inputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("B"), 120), + new Note((SevenBitNumber)100, 20, 130), + new TimedEvent(new TextEvent("C"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new TimedEvent(new TextEvent("E"), 2000), + new Note((SevenBitNumber)100, 20, 2000), + }, + expectedRests: new[] + { + new Rest(0, 15, SameKey), + new Rest(15, 105, SameKey), + new Rest(120, 980, SameKey), + new Rest(1100, 50, SameKey), + new Rest(1150, 850, SameKey), + }); + } + + [TestCase(10, 10)] + [TestCase(10, 50)] + public void GetRests_NotesAndChords_RestsByChannel_SameChannel(byte noteNumber1, byte noteNumber2) + { + var channel = (FourBitNumber)10; + + GetRests( + keySelector: obj => (obj as Note)?.Channel ?? (obj as Chord)?.Channel, + inputObjects: new ITimedObject[] + { + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, + new Chord( + new Note((SevenBitNumber)noteNumber1, 100, 30), + new Note((SevenBitNumber)(noteNumber1 + 1), 100, 40)) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel }, + new Chord( + new Note((SevenBitNumber)noteNumber2, 1000, 10000), + new Note((SevenBitNumber)(noteNumber2 + 1), 1000, 10010), + new Note((SevenBitNumber)(noteNumber2 + 2), 1000, 10010)) { Channel = channel }, + }, + expectedRests: new[] + { + new Rest(0, 10, channel), + new Rest(140, 160, channel), + new Rest(350, 650, channel), + new Rest(2300, 7700, channel), + }); + } + + [Test] + public void GetRests_TimedEvents_NullKeySelector() + { + GetRests( + keySelector: null, + inputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("CA"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new TimedEvent(new TextEvent("E"), 2000), + }, + expectedRests: Array.Empty()); + } + + [Test] + public void GetRests_Notes_NullKeySelector() + { + var noteNumber1 = (SevenBitNumber)10; + var noteNumber2 = (SevenBitNumber)100; + var channel1 = (FourBitNumber)10; + var channel2 = (FourBitNumber)2; + + GetRests( + keySelector: null, + inputObjects: new ITimedObject[] + { + new Note(noteNumber1, 100, 10) { Channel = channel1 }, + new Note(noteNumber2, 100, 30) { Channel = channel1 }, + new Note(noteNumber1, 50, 300) { Channel = channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = channel2 }, + new Note(noteNumber1, 150, 1200) { Channel = channel1 }, + new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, + }, + expectedRests: Array.Empty()); + } + + [Test] + public void GetRests_UnsortedRandomCollection() + { + GetRests( + keySelector: obj => obj is TimedEvent ? SameKey : (int?)null, + inputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("C"), 1100), + new Note((SevenBitNumber)100, 20, 130), + new TimedEvent(new TextEvent("E"), 2000), + new TimedEvent(new TextEvent("D"), 1150), + new Note((SevenBitNumber)100, 20, 2000), + }, + expectedRests: new[] + { + new Rest(0, 15, SameKey), + new Rest(15, 105, SameKey), + new Rest(120, 980, SameKey), + new Rest(1100, 50, SameKey), + new Rest(1150, 850, SameKey), + }); + } + + [Test] + public void GetRests_AfterGetObjects() + { + var rawObjects = new ITimedObject[] + { + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("C"), 1100), + new Note((SevenBitNumber)100, 20, 130), + new TimedEvent(new TextEvent("E"), 2000), + new TimedEvent(new TextEvent("D"), 1150), + new Note((SevenBitNumber)100, 20, 2000), + }; + + GetRests( + keySelector: obj => obj is TimedEvent ? SameKey : (int?)null, + inputObjects: rawObjects.GetObjects(ObjectType.Note | ObjectType.TimedEvent), + expectedRests: new[] + { + new Rest(0, 15, SameKey), + new Rest(15, 105, SameKey), + new Rest(120, 980, SameKey), + new Rest(1100, 50, SameKey), + new Rest(1150, 850, SameKey), + }); + } + + #endregion + + #region Private methods + + private void GetRests( + Func keySelector, + IEnumerable inputObjects, + IEnumerable expectedRests) + { + var actualRests = inputObjects + .GetRests(new RestDetectionSettings + { + KeySelector = keySelector + }) + .ToArray(); + + MidiAsserts.AreEqual(expectedRests, actualRests, true, 0, "Rests are invalid."); + } + + #endregion + } +} diff --git a/DryWetMidi.Tests/Interaction/Rests/RestsUtilitiesTests.WithRests.cs b/DryWetMidi.Tests/Interaction/Rests/RestsUtilitiesTests.WithRests.cs new file mode 100644 index 000000000..cf67900a2 --- /dev/null +++ b/DryWetMidi.Tests/Interaction/Rests/RestsUtilitiesTests.WithRests.cs @@ -0,0 +1,537 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Interaction; +using Melanchall.DryWetMidi.Tests.Utilities; +using NUnit.Framework; +using System; +using System.Collections.Generic; + +namespace Melanchall.DryWetMidi.Tests.Interaction +{ + [TestFixture] + public sealed partial class RestsUtilitiesTests + { + #region Test methods + + [TestCase(10, 10, 50, 50)] + [TestCase(10, 2, 50, 50)] + [TestCase(10, 10, 50, 100)] + [TestCase(10, 2, 50, 100)] + public void WithRests_Notes_SameKey(byte channel1, byte channel2, byte noteNumber1, byte noteNumber2) => WithRests( + keySelector: obj => SameKey, + inputObjects: new ITimedObject[] + { + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = (FourBitNumber)channel1 }, + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = (FourBitNumber)channel1 }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = (FourBitNumber)channel1 }, + new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 1000, 100000) { Channel = (FourBitNumber)channel1 }, + new Note((SevenBitNumber)noteNumber2, 10, 100100) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 10, 110000) { Channel = (FourBitNumber)channel1 }, + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 10, SameKey), + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = (FourBitNumber)channel1 }, + new Rest(130, 170, SameKey), + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = (FourBitNumber)channel2 }, + new Rest(350, 650, SameKey), + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = (FourBitNumber)channel1 }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = (FourBitNumber)channel2 }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = (FourBitNumber)channel1 }, + new Rest(2300, 7700, SameKey), + new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = (FourBitNumber)channel2 }, + new Rest(11000, 89000, SameKey), + new Note((SevenBitNumber)noteNumber1, 1000, 100000) { Channel = (FourBitNumber)channel1 }, + new Note((SevenBitNumber)noteNumber2, 10, 100100) { Channel = (FourBitNumber)channel2 }, + new Rest(101000, 9000, SameKey), + new Note((SevenBitNumber)noteNumber1, 10, 110000) { Channel = (FourBitNumber)channel1 }, + }); + + [TestCase(10, 10)] + [TestCase(10, 50)] + public void WithRests_Notes_RestsByChannel_SameChannel(byte noteNumber1, byte noteNumber2) + { + var channel = (FourBitNumber)10; + + WithRests( + keySelector: obj => (obj as Note)?.Channel, + inputObjects: new ITimedObject[] + { + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = channel }, + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 10, channel), + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel }, + new Rest(130, 170, channel), + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, + new Rest(350, 650, channel), + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel }, + new Rest(2300, 7700, channel), + new Note((SevenBitNumber)noteNumber2, 1000, 10000) { Channel = channel }, + }); + } + + [TestCase(10, 10)] + [TestCase(10, 50)] + public void WithRests_Notes_RestsByChannel_DifferentChannels(byte noteNumber1, byte noteNumber2) + { + var channel1 = (FourBitNumber)10; + var channel2 = (FourBitNumber)2; + + WithRests( + keySelector: obj => (obj as Note)?.Channel, + inputObjects: new ITimedObject[] + { + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel1 }, + new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel2 }, + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel1 }, + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel2 }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel1 }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel2 }, + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 10, channel1), + new Rest(0, 30, channel2), + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel1 }, + new Note((SevenBitNumber)noteNumber1, 100, 30) { Channel = channel2 }, + new Rest(110, 190, channel1), + new Rest(130, 870, channel2), + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel1 }, + new Rest(350, 850, channel1), + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel2 }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel1 }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel2 }, + }); + } + + [TestCase(10, 10)] + [TestCase(10, 5)] + public void WithRests_Notes_RestsByNoteNumber_SameNoteNumber(byte channel1, byte channel2) + { + var noteNumber = (SevenBitNumber)10; + + WithRests( + keySelector: obj => (obj as Note)?.NoteNumber, + inputObjects: new ITimedObject[] + { + new Note(noteNumber, 100, 10) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber, 100, 30) { Channel = (FourBitNumber)channel1 }, + new Note(noteNumber, 50, 300) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber, 500, 1000) { Channel = (FourBitNumber)channel1 }, + new Note(noteNumber, 150, 1200) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber, 1000, 1300) { Channel = (FourBitNumber)channel1 }, + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 10, noteNumber), + new Note(noteNumber, 100, 10) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber, 100, 30) { Channel = (FourBitNumber)channel1 }, + new Rest(130, 170, noteNumber), + new Note(noteNumber, 50, 300) { Channel = (FourBitNumber)channel2 }, + new Rest(350, 650, noteNumber), + new Note(noteNumber, 500, 1000) { Channel = (FourBitNumber)channel1 }, + new Note(noteNumber, 150, 1200) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber, 1000, 1300) { Channel = (FourBitNumber)channel1 }, + }); + } + + [TestCase(10, 10)] + [TestCase(10, 5)] + public void WithRests_Notes_RestsByNoteNumber_DifferentNoteNumbers(byte channel1, byte channel2) + { + var noteNumber1 = (SevenBitNumber)10; + var noteNumber2 = (SevenBitNumber)100; + + WithRests( + keySelector: obj => (obj as Note)?.NoteNumber, + inputObjects: new ITimedObject[] + { + new Note(noteNumber1, 100, 0) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber2, 100, 30) { Channel = (FourBitNumber)channel1 }, + new Note(noteNumber1, 50, 300) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = (FourBitNumber)channel1 }, + }, + outputObjects: new ITimedObject[] + { + new Note(noteNumber1, 100, 0) { Channel = (FourBitNumber)channel2 }, + new Rest(0, 30, noteNumber2), + new Note(noteNumber2, 100, 30) { Channel = (FourBitNumber)channel1 }, + new Rest(100, 200, noteNumber1), + new Rest(130, 870, noteNumber2), + new Note(noteNumber1, 50, 300) { Channel = (FourBitNumber)channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = (FourBitNumber)channel1 }, + }); + } + + [Test] + public void WithRests_Notes_RestsByChannelAndNoteNumber() + { + var noteNumber1 = (SevenBitNumber)10; + var noteNumber2 = (SevenBitNumber)100; + var channel1 = (FourBitNumber)10; + var channel2 = (FourBitNumber)2; + + WithRests( + keySelector: obj => ((obj as Note)?.Channel, (obj as Note)?.NoteNumber), + inputObjects: new ITimedObject[] + { + new Note(noteNumber1, 100, 10) { Channel = channel1 }, + new Note(noteNumber2, 100, 30) { Channel = channel1 }, + new Note(noteNumber1, 50, 300) { Channel = channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = channel2 }, + new Note(noteNumber1, 150, 1200) { Channel = channel1 }, + new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 10, (channel1, noteNumber1)), + new Rest(0, 30, (channel1, noteNumber2)), + new Rest(0, 300, (channel2, noteNumber1)), + new Rest(0, 1000, (channel2, noteNumber2)), + new Note(noteNumber1, 100, 10) { Channel = channel1 }, + new Note(noteNumber2, 100, 30) { Channel = channel1 }, + new Rest(110, 1090, (channel1, noteNumber1)), + new Rest(130, 1170, (channel1, noteNumber2)), + new Note(noteNumber1, 50, 300) { Channel = channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = channel2 }, + new Note(noteNumber1, 150, 1200) { Channel = channel1 }, + new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, + }); + } + + [Test] + public void WithRests_Notes_RestsByChannelAndNoteNumber_WithTimedEvents() + { + var noteNumber1 = (SevenBitNumber)10; + var noteNumber2 = (SevenBitNumber)100; + var channel1 = (FourBitNumber)10; + var channel2 = (FourBitNumber)2; + + WithRests( + keySelector: obj => obj is Note + ? (((Note)obj).Channel, ((Note)obj).NoteNumber) + : ((FourBitNumber, SevenBitNumber)?)null, + inputObjects: new ITimedObject[] + { + new Note(noteNumber1, 100, 10) { Channel = channel1 }, + new TimedEvent(new TextEvent("A"), 15), + new Note(noteNumber2, 100, 30) { Channel = channel1 }, + new TimedEvent(new TextEvent("B"), 120), + new Note(noteNumber1, 50, 300) { Channel = channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = channel2 }, + new TimedEvent(new TextEvent("C"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new Note(noteNumber1, 150, 1200) { Channel = channel1 }, + new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, + new TimedEvent(new TextEvent("E"), 2000), + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 10, (channel1, noteNumber1)), + new Rest(0, 30, (channel1, noteNumber2)), + new Rest(0, 300, (channel2, noteNumber1)), + new Rest(0, 1000, (channel2, noteNumber2)), + new Note(noteNumber1, 100, 10) { Channel = channel1 }, + new TimedEvent(new TextEvent("A"), 15), + new Note(noteNumber2, 100, 30) { Channel = channel1 }, + new Rest(110, 1090, (channel1, noteNumber1)), + new TimedEvent(new TextEvent("B"), 120), + new Rest(130, 1170, (channel1, noteNumber2)), + new Note(noteNumber1, 50, 300) { Channel = channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = channel2 }, + new TimedEvent(new TextEvent("C"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new Note(noteNumber1, 150, 1200) { Channel = channel1 }, + new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, + new TimedEvent(new TextEvent("E"), 2000), + }); + } + + [Test] + public void WithRests_TimedEvents_1() + { + WithRests( + keySelector: obj => ((TextEvent)((TimedEvent)obj).Event).Text.Contains("A"), + inputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("CA"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new TimedEvent(new TextEvent("E"), 2000), + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 15, true), + new Rest(0, 120, false), + new TimedEvent(new TextEvent("A"), 15), + new Rest(15, 1085, true), + new TimedEvent(new TextEvent("B"), 120), + new Rest(120, 1030, false), + new TimedEvent(new TextEvent("CA"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new Rest(1150, 850, false), + new TimedEvent(new TextEvent("E"), 2000), + }); + } + + [Test] + public void WithRests_TimedEvents_2() + { + WithRests( + keySelector: obj => obj is TimedEvent ? SameKey : (int?)null, + inputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("C"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new TimedEvent(new TextEvent("E"), 2000), + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 15, SameKey), + new TimedEvent(new TextEvent("A"), 15), + new Rest(15, 105, SameKey), + new TimedEvent(new TextEvent("B"), 120), + new Rest(120, 980, SameKey), + new TimedEvent(new TextEvent("C"), 1100), + new Rest(1100, 50, SameKey), + new TimedEvent(new TextEvent("D"), 1150), + new Rest(1150, 850, SameKey), + new TimedEvent(new TextEvent("E"), 2000), + }); + } + + [Test] + public void WithRests_TimedEvents_3() + { + WithRests( + keySelector: obj => obj is TimedEvent ? SameKey : (int?)null, + inputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("B"), 120), + new Note((SevenBitNumber)100, 20, 130), + new TimedEvent(new TextEvent("C"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new TimedEvent(new TextEvent("E"), 2000), + new Note((SevenBitNumber)100, 20, 2000), + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 15, SameKey), + new TimedEvent(new TextEvent("A"), 15), + new Rest(15, 105, SameKey), + new TimedEvent(new TextEvent("B"), 120), + new Rest(120, 980, SameKey), + new Note((SevenBitNumber)100, 20, 130), + new TimedEvent(new TextEvent("C"), 1100), + new Rest(1100, 50, SameKey), + new TimedEvent(new TextEvent("D"), 1150), + new Rest(1150, 850, SameKey), + new TimedEvent(new TextEvent("E"), 2000), + new Note((SevenBitNumber)100, 20, 2000), + }); + } + + [TestCase(10, 10)] + [TestCase(10, 50)] + public void WithRests_NotesAndChords_RestsByChannel_SameChannel(byte noteNumber1, byte noteNumber2) + { + var channel = (FourBitNumber)10; + + WithRests( + keySelector: obj => (obj as Note)?.Channel ?? (obj as Chord)?.Channel, + inputObjects: new ITimedObject[] + { + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, + new Chord( + new Note((SevenBitNumber)noteNumber1, 100, 30), + new Note((SevenBitNumber)(noteNumber1 + 1), 100, 40)) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel }, + new Chord( + new Note((SevenBitNumber)noteNumber2, 1000, 10000), + new Note((SevenBitNumber)(noteNumber2 + 1), 1000, 10010), + new Note((SevenBitNumber)(noteNumber2 + 2), 1000, 10010)) { Channel = channel }, + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 10, channel), + new Note((SevenBitNumber)noteNumber1, 100, 10) { Channel = channel }, + new Chord( + new Note((SevenBitNumber)noteNumber1, 100, 30), + new Note((SevenBitNumber)(noteNumber1 + 1), 100, 40)) { Channel = channel }, + new Rest(140, 160, channel), + new Note((SevenBitNumber)noteNumber2, 50, 300) { Channel = channel }, + new Rest(350, 650, channel), + new Note((SevenBitNumber)noteNumber1, 500, 1000) { Channel = channel }, + new Note((SevenBitNumber)noteNumber2, 150, 1200) { Channel = channel }, + new Note((SevenBitNumber)noteNumber1, 1000, 1300) { Channel = channel }, + new Rest(2300, 7700, channel), + new Chord( + new Note((SevenBitNumber)noteNumber2, 1000, 10000), + new Note((SevenBitNumber)(noteNumber2 + 1), 1000, 10010), + new Note((SevenBitNumber)(noteNumber2 + 2), 1000, 10010)) { Channel = channel }, + }); + } + + [Test] + public void WithRests_TimedEvents_NullKeySelector() + { + WithRests( + keySelector: null, + inputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("CA"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new TimedEvent(new TextEvent("E"), 2000), + }, + outputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("CA"), 1100), + new TimedEvent(new TextEvent("D"), 1150), + new TimedEvent(new TextEvent("E"), 2000), + }); + } + + [Test] + public void WithRests_Notes_NullKeySelector() + { + var noteNumber1 = (SevenBitNumber)10; + var noteNumber2 = (SevenBitNumber)100; + var channel1 = (FourBitNumber)10; + var channel2 = (FourBitNumber)2; + + WithRests( + keySelector: null, + inputObjects: new ITimedObject[] + { + new Note(noteNumber1, 100, 10) { Channel = channel1 }, + new Note(noteNumber2, 100, 30) { Channel = channel1 }, + new Note(noteNumber1, 50, 300) { Channel = channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = channel2 }, + new Note(noteNumber1, 150, 1200) { Channel = channel1 }, + new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, + }, + outputObjects: new ITimedObject[] + { + new Note(noteNumber1, 100, 10) { Channel = channel1 }, + new Note(noteNumber2, 100, 30) { Channel = channel1 }, + new Note(noteNumber1, 50, 300) { Channel = channel2 }, + new Note(noteNumber2, 500, 1000) { Channel = channel2 }, + new Note(noteNumber1, 150, 1200) { Channel = channel1 }, + new Note(noteNumber2, 1000, 1300) { Channel = channel1 }, + }); + } + + [Test] + public void WithRests_UnsortedRandomCollection() + { + WithRests( + keySelector: obj => obj is TimedEvent ? SameKey : (int?)null, + inputObjects: new ITimedObject[] + { + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("C"), 1100), + new Note((SevenBitNumber)100, 20, 130), + new TimedEvent(new TextEvent("E"), 2000), + new TimedEvent(new TextEvent("D"), 1150), + new Note((SevenBitNumber)100, 20, 2000), + }, + outputObjects: new ITimedObject[] + { + new Rest(0, 15, SameKey), + new TimedEvent(new TextEvent("A"), 15), + new Rest(15, 105, SameKey), + new TimedEvent(new TextEvent("B"), 120), + new Rest(120, 980, SameKey), + new Note((SevenBitNumber)100, 20, 130), + new TimedEvent(new TextEvent("C"), 1100), + new Rest(1100, 50, SameKey), + new TimedEvent(new TextEvent("D"), 1150), + new Rest(1150, 850, SameKey), + new TimedEvent(new TextEvent("E"), 2000), + new Note((SevenBitNumber)100, 20, 2000), + }); + } + + [Test] + public void WithRests_AfterGetObjects() + { + var rawObjects = new ITimedObject[] + { + new TimedEvent(new TextEvent("B"), 120), + new TimedEvent(new TextEvent("A"), 15), + new TimedEvent(new TextEvent("C"), 1100), + new Note((SevenBitNumber)100, 20, 130), + new TimedEvent(new TextEvent("E"), 2000), + new TimedEvent(new TextEvent("D"), 1150), + new Note((SevenBitNumber)100, 20, 2000), + }; + + WithRests( + keySelector: obj => obj is TimedEvent ? SameKey : (int?)null, + inputObjects: rawObjects.GetObjects(ObjectType.Note | ObjectType.TimedEvent), + outputObjects: new ITimedObject[] + { + new Rest(0, 15, SameKey), + new TimedEvent(new TextEvent("A"), 15), + new Rest(15, 105, SameKey), + new TimedEvent(new TextEvent("B"), 120), + new Rest(120, 980, SameKey), + new Note((SevenBitNumber)100, 20, 130), + new TimedEvent(new TextEvent("C"), 1100), + new Rest(1100, 50, SameKey), + new TimedEvent(new TextEvent("D"), 1150), + new Rest(1150, 850, SameKey), + new TimedEvent(new TextEvent("E"), 2000), + new Note((SevenBitNumber)100, 20, 2000), + }); + } + + #endregion + + #region Private methods + + private void WithRests( + Func keySelector, + IEnumerable inputObjects, + IEnumerable outputObjects) + { + var actualObjects = inputObjects + .WithRests(new RestDetectionSettings + { + KeySelector = keySelector + }); + + MidiAsserts.AreEqual(outputObjects, actualObjects, true, 0, "Objects are invalid."); + } + + #endregion + } +} diff --git a/DryWetMidi.Tests/Multimedia/Recording/RecordingTests.cs b/DryWetMidi.Tests/Multimedia/Recording/RecordingTests.cs index f8705a270..0055ff603 100644 --- a/DryWetMidi.Tests/Multimedia/Recording/RecordingTests.cs +++ b/DryWetMidi.Tests/Multimedia/Recording/RecordingTests.cs @@ -103,11 +103,13 @@ public void CheckRecording() var sentEvents = new List(); var receivedEvents = new List(); + var recordedEvents = new List(); var stopwatch = new Stopwatch(); var expectedTimes = new List(); var expectedRecordedTimes = new List(); var currentTime = TimeSpan.Zero; + foreach (var eventToSend in eventsToSend) { currentTime += eventToSend.Delay; @@ -119,7 +121,6 @@ public void CheckRecording() using (var outputDevice = OutputDevice.GetByName(SendReceiveUtilities.DeviceToTestOnName)) { - //SendReceiveUtilities.WarmUpDevice(outputDevice); outputDevice.EventSent += (_, e) => sentEvents.Add(new SentEvent(e.Event, stopwatch.Elapsed)); using (var inputDevice = InputDevice.GetByName(SendReceiveUtilities.DeviceToTestOnName)) @@ -129,6 +130,8 @@ public void CheckRecording() using (var recording = new Recording(tempoMap, inputDevice)) { + recording.EventRecorded += (_, e) => recordedEvents.Add(new ReceivedEvent(e.Event, stopwatch.Elapsed)); + var sendingThread = new Thread(() => { SendReceiveUtilities.SendEvents(eventsToSend, outputDevice); @@ -152,9 +155,10 @@ public void CheckRecording() Assert.IsTrue(areEventsReceived, $"Events are not received for [{timeout}] (received are: {string.Join(", ", receivedEvents)})."); CompareSentReceivedEvents(sentEvents, receivedEvents, expectedTimes); + CompareSentReceivedEvents(sentEvents, recordedEvents, expectedTimes); - var recordedEvents = recording.GetEvents(); - CheckRecordedEvents(recordedEvents, expectedRecordedTimes, tempoMap); + var events = recording.GetEvents(); + CheckRecordedEvents(events.ToList(), expectedRecordedTimes, tempoMap); } } } 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..91f7d7988 --- /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\r\nbb\"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..4031a4a95 --- /dev/null +++ b/DryWetMidi.Tests/Tools/CsvSerializer/CsvSerializerTests.Serialize.cs @@ -0,0 +1,419 @@ +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.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\r\nbb\"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/Merger/MergerTests.MergeObjects.cs b/DryWetMidi.Tests/Tools/Merger/MergerTests.MergeObjects.cs index 57fbedcfd..c154e6a3f 100644 --- a/DryWetMidi.Tests/Tools/Merger/MergerTests.MergeObjects.cs +++ b/DryWetMidi.Tests/Tools/Merger/MergerTests.MergeObjects.cs @@ -331,52 +331,52 @@ public void MergeObjects_Objects_TimedEventsAndChords_Merging_3() => MergeObject public void MergeObjects_Objects_Rests_NoMerging_1() => MergeObjects_Objects( timedObjects: new ITimedObject[] { - new Rest(10, 30, null, (SevenBitNumber)70), - new Rest(40, 30, null, null), - new Rest(300, 10, null, (SevenBitNumber)70), - new Rest(310, 30, null, (SevenBitNumber)80), + new Rest(10, 30, (0, (SevenBitNumber)70)), + new Rest(40, 30, (0, 0)), + new Rest(300, 10, (0, (SevenBitNumber)70)), + new Rest(310, 30, (0, (SevenBitNumber)80)), }, settings: null, expectedObjects: new ITimedObject[] { - new Rest(10, 30, null, (SevenBitNumber)70), - new Rest(40, 30, null, null), - new Rest(300, 10, null, (SevenBitNumber)70), - new Rest(310, 30, null, (SevenBitNumber)80), + new Rest(10, 30, (0, (SevenBitNumber)70)), + new Rest(40, 30, (0, 0)), + new Rest(300, 10, (0, (SevenBitNumber)70)), + new Rest(310, 30, (0, (SevenBitNumber)80)), }); [Test] public void MergeObjects_Objects_Rests_NoMerging_2() => MergeObjects_Objects( timedObjects: new ITimedObject[] { - new Rest(10, 30, null, (SevenBitNumber)70), - new Rest(40, 30, (FourBitNumber)2, (SevenBitNumber)70), - new Rest(300, 10, (FourBitNumber)2, null), - new Rest(310, 30, (FourBitNumber)3, null), + new Rest(10, 30, (0, (SevenBitNumber)70)), + new Rest(40, 30, ((FourBitNumber)2, (SevenBitNumber)70)), + new Rest(300, 10, ((FourBitNumber)2, 0)), + new Rest(310, 30, ((FourBitNumber)3, 0)), }, settings: null, expectedObjects: new ITimedObject[] { - new Rest(10, 30, null, (SevenBitNumber)70), - new Rest(40, 30, (FourBitNumber)2, (SevenBitNumber)70), - new Rest(300, 10, (FourBitNumber)2, null), - new Rest(310, 30, (FourBitNumber)3, null), + new Rest(10, 30, (0, (SevenBitNumber)70)), + new Rest(40, 30, ((FourBitNumber) 2,(SevenBitNumber) 70)), + new Rest(300, 10, ((FourBitNumber)2, 0)), + new Rest(310, 30, ((FourBitNumber)3, 0)), }); [Test] public void MergeObjects_Objects_Rests_Merging_1() => MergeObjects_Objects( timedObjects: new ITimedObject[] { - new Rest(10, 30, null, (SevenBitNumber)70), - new Rest(40, 30, null, (SevenBitNumber)70), - new Rest(300, 10, (FourBitNumber)2, null), - new Rest(310, 30, (FourBitNumber)2, null), + new Rest(10, 30, (0, (SevenBitNumber)70)), + new Rest(40, 30, (0, (SevenBitNumber)70)), + new Rest(300, 10, ((FourBitNumber)2, 0)), + new Rest(310, 30, ((FourBitNumber)2, 0)), }, settings: null, expectedObjects: new ITimedObject[] { - new Rest(10, 60, null, (SevenBitNumber)70), - new Rest(300, 40, (FourBitNumber)2, null), + new Rest(10, 60, (0, (SevenBitNumber)70)), + new Rest(300, 40, ((FourBitNumber)2, 0)), }); [Test] diff --git a/DryWetMidi.Tests/Tools/Repeater/RepeaterTests.MultipleCollections.cs b/DryWetMidi.Tests/Tools/Repeater/RepeaterTests.MultipleCollections.cs index 8d78b80b5..d224f7187 100644 --- a/DryWetMidi.Tests/Tools/Repeater/RepeaterTests.MultipleCollections.cs +++ b/DryWetMidi.Tests/Tools/Repeater/RepeaterTests.MultipleCollections.cs @@ -80,6 +80,22 @@ public void CheckRepeat_MultipleCollections_TimedEvents() => CheckRepeat( } }); + [Test] + public void CheckRepeat_MultipleCollections_ZeroRepeatsNumber() => CheckRepeat_ZeroRepeatsNumber( + new[] + { + new[] + { + new TimedEvent(new TextEvent("A"), 0), + new TimedEvent(new ControlChangeEvent(), 20), + }, + new[] + { + new TimedEvent(new TextEvent("A"), 10), + new TimedEvent(new ControlChangeEvent(), 30), + } + }); + [Test] public void CheckRepeat_MultipleCollections_TimedEventsAndNotes() => CheckRepeat( inputObjects: new[] @@ -151,6 +167,15 @@ public void CheckRepeat_MultipleCollections_Custom() => CheckRepeat_Custom> inputObjects) => + CheckRepeat( + inputObjects, + 0, + TempoMap.Default, + null, + inputObjects.Select(objects => objects.Select(o => o.Clone()).ToArray()).ToArray()); + private void CheckRepeat( ICollection> inputObjects, int repeatsNumber, diff --git a/DryWetMidi.Tests/Tools/Repeater/RepeaterTests.SingleCollection.cs b/DryWetMidi.Tests/Tools/Repeater/RepeaterTests.SingleCollection.cs index c81ea4450..eab6076e0 100644 --- a/DryWetMidi.Tests/Tools/Repeater/RepeaterTests.SingleCollection.cs +++ b/DryWetMidi.Tests/Tools/Repeater/RepeaterTests.SingleCollection.cs @@ -150,6 +150,18 @@ public void CheckRepeat_SingleCollection_EmptyCollection([Values(1, 10)] int rep settings: null, expectedObjects: Array.Empty()); + [Test] + public void CheckRepeat_SingleCollection_ZeroRepeatsNumber_1() => CheckRepeat_ZeroRepeatsNumber( + new[] { new TimedEvent(new TextEvent("A")) }); + + [Test] + public void CheckRepeat_SingleCollection_ZeroRepeatsNumber_2() => CheckRepeat_ZeroRepeatsNumber( + new ITimedObject[] + { + new TimedEvent(new TextEvent("A")), + new Note(DryWetMidi.MusicTheory.NoteName.ASharp, 2), + }); + [Test] public void CheckRepeat_SingleCollection_SingleTimedEvent_DefaultSettings([Values(0, 10)] long eventTime, [Values(1, 10)] int repeatsNumber) => CheckRepeat( inputObjects: new[] @@ -512,7 +524,7 @@ public void CheckRepeat_SingleCollection_Nulls() => CheckRepeat( }); [Test] - public void CheckRepeat_NonPositiveRepeatsNumber([Values(0, -1)] int repeatsNumber) + public void CheckRepeat_NegativeRepeatsNumber([Values(-1)] int repeatsNumber) { var repeater = new Repeater(); @@ -577,6 +589,12 @@ public void CheckRepeat_NullTempoMap() #region Private methods + private void CheckRepeat_ZeroRepeatsNumber(ICollection inputObjects) => CheckRepeat( + inputObjects: inputObjects, + repeatsNumber: 0, + settings: null, + expectedObjects: inputObjects.Select(o => o.Clone()).ToArray()); + private void CheckRepeat( ICollection inputObjects, int repeatsNumber, diff --git a/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.NoteMinLength.cs b/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.NoteMinLength.cs index c99dc5b9f..f3d1ecbea 100644 --- a/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.NoteMinLength.cs +++ b/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.NoteMinLength.cs @@ -1,5 +1,4 @@ using Melanchall.DryWetMidi.Common; -using Melanchall.DryWetMidi.Composing; using Melanchall.DryWetMidi.Core; using Melanchall.DryWetMidi.Interaction; using Melanchall.DryWetMidi.Tools; @@ -62,7 +61,10 @@ public void Sanitize_NoteMinLength_NoNotes_5() => Sanitize( midiFile: new MidiFile( new TrackChunk( new TextEvent("A") { DeltaTime = 20 })), - settings: null, + settings: new SanitizingSettings + { + Trim = false + }, expectedMidiFile: new MidiFile( new TrackChunk( new TextEvent("A") { DeltaTime = 20 }))); diff --git a/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveEventsOnUnusedChannels.cs b/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveEventsOnUnusedChannels.cs index 441105a67..96ac00398 100644 --- a/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveEventsOnUnusedChannels.cs +++ b/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveEventsOnUnusedChannels.cs @@ -75,7 +75,8 @@ public void Sanitize_RemoveEventsOnUnusedChannels_2() => Sanitize( new NoteOffEvent() { Channel = (FourBitNumber) 7 })), settings: new SanitizingSettings { - RemoveEmptyTrackChunks = false + RemoveEmptyTrackChunks = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -199,7 +200,8 @@ public void Sanitize_RemoveEventsOnUnusedChannels_False_2() => Sanitize( settings: new SanitizingSettings { RemoveEmptyTrackChunks = false, - RemoveEventsOnUnusedChannels = false + RemoveEventsOnUnusedChannels = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( diff --git a/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveOrphanedNoteOffEvents.cs b/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveOrphanedNoteOffEvents.cs index 2c9df1a88..039428e8a 100644 --- a/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveOrphanedNoteOffEvents.cs +++ b/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveOrphanedNoteOffEvents.cs @@ -99,7 +99,8 @@ public void Sanitize_RemoveOrphanedNoteOffEvents_1() => Sanitize( new NoteOnEvent())), settings: new SanitizingSettings { - RemoveOrphanedNoteOnEvents = false + RemoveOrphanedNoteOnEvents = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -135,7 +136,10 @@ public void Sanitize_RemoveOrphanedNoteOffEvents_3() => Sanitize( new NoteOffEvent() { DeltaTime = 20 }, new NoteOffEvent() { DeltaTime = 25 }, new NoteOffEvent() { DeltaTime = 30 })), - settings: null, + settings: new SanitizingSettings + { + Trim = false + }, expectedMidiFile: new MidiFile( new TrackChunk( new NoteOnEvent() { DeltaTime = 15 }, @@ -152,7 +156,10 @@ public void Sanitize_RemoveOrphanedNoteOffEvents_4() => Sanitize( new NoteOffEvent() { DeltaTime = 25 }, new NoteOffEvent() { DeltaTime = 30 }, new TextEvent("C") { DeltaTime = 40 })), - settings: null, + settings: new SanitizingSettings + { + Trim = false + }, expectedMidiFile: new MidiFile( new TrackChunk( new NoteOnEvent() { DeltaTime = 15 }, @@ -170,7 +177,8 @@ public void Sanitize_RemoveOrphanedNoteOffEvents_False_1() => Sanitize( settings: new SanitizingSettings { RemoveOrphanedNoteOnEvents = false, - RemoveOrphanedNoteOffEvents = false + RemoveOrphanedNoteOffEvents = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -211,7 +219,8 @@ public void Sanitize_RemoveOrphanedNoteOffEvents_False_3() => Sanitize( new NoteOffEvent() { DeltaTime = 30 })), settings: new SanitizingSettings { - RemoveOrphanedNoteOffEvents = false + RemoveOrphanedNoteOffEvents = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -233,7 +242,8 @@ public void Sanitize_RemoveOrphanedNoteOffEvents_False_4() => Sanitize( new TextEvent("C") { DeltaTime = 40 })), settings: new SanitizingSettings { - RemoveOrphanedNoteOffEvents = false + RemoveOrphanedNoteOffEvents = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( diff --git a/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveOrphanedNoteOnEvents.cs b/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveOrphanedNoteOnEvents.cs index ddb36ad44..fef247029 100644 --- a/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveOrphanedNoteOnEvents.cs +++ b/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.RemoveOrphanedNoteOnEvents.cs @@ -100,7 +100,8 @@ public void Sanitize_RemoveOrphanedNoteOnEvents_1() => Sanitize( new NoteOnEvent())), settings: new SanitizingSettings { - RemoveOrphanedNoteOffEvents = false + RemoveOrphanedNoteOffEvents = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -140,7 +141,8 @@ public void Sanitize_RemoveOrphanedNoteOnEvents_3() => Sanitize( NoteDetectionSettings = new NoteDetectionSettings { NoteStartDetectionPolicy = NoteStartDetectionPolicy.FirstNoteOn - } + }, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -160,7 +162,8 @@ public void Sanitize_RemoveOrphanedNoteOnEvents_4() => Sanitize( NoteDetectionSettings = new NoteDetectionSettings { NoteStartDetectionPolicy = NoteStartDetectionPolicy.LastNoteOn - } + }, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -180,7 +183,8 @@ public void Sanitize_RemoveOrphanedNoteOnEvents_5() => Sanitize( NoteDetectionSettings = new NoteDetectionSettings { NoteStartDetectionPolicy = NoteStartDetectionPolicy.LastNoteOn - } + }, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -196,7 +200,8 @@ public void Sanitize_RemoveOrphanedNoteOnEvents_False_1() => Sanitize( settings: new SanitizingSettings { RemoveOrphanedNoteOffEvents = false, - RemoveOrphanedNoteOnEvents = false + RemoveOrphanedNoteOnEvents = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -240,7 +245,8 @@ public void Sanitize_RemoveOrphanedNoteOnEvents_False_3() => Sanitize( { NoteStartDetectionPolicy = NoteStartDetectionPolicy.FirstNoteOn }, - RemoveOrphanedNoteOnEvents = false + RemoveOrphanedNoteOnEvents = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -262,7 +268,8 @@ public void Sanitize_RemoveOrphanedNoteOnEvents_False_4() => Sanitize( { NoteStartDetectionPolicy = NoteStartDetectionPolicy.LastNoteOn }, - RemoveOrphanedNoteOnEvents = false + RemoveOrphanedNoteOnEvents = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( @@ -284,7 +291,8 @@ public void Sanitize_RemoveOrphanedNoteOnEvents_False_5() => Sanitize( { NoteStartDetectionPolicy = NoteStartDetectionPolicy.LastNoteOn }, - RemoveOrphanedNoteOnEvents = false + RemoveOrphanedNoteOnEvents = false, + Trim = false }, expectedMidiFile: new MidiFile( new TrackChunk( diff --git a/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.Trim.cs b/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.Trim.cs new file mode 100644 index 000000000..90fa038f3 --- /dev/null +++ b/DryWetMidi.Tests/Tools/Sanitizer/SanitizerTests.Trim.cs @@ -0,0 +1,110 @@ +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Tools; +using NUnit.Framework; +using System; + +namespace Melanchall.DryWetMidi.Tests.Tools +{ + [TestFixture] + public sealed partial class SanitizerTests + { + #region Test methods + + [Test] + public void Sanitize_Trim_EmptyFile() => Sanitize( + midiFile: new MidiFile(), + settings: null, + expectedMidiFile: new MidiFile()); + + [Test] + public void Sanitize_Trim_EmptyTrackChunks() => Sanitize( + midiFile: new MidiFile( + new TrackChunk(), + new TrackChunk()), + settings: new SanitizingSettings + { + RemoveEmptyTrackChunks = false + }, + expectedMidiFile: new MidiFile( + new TrackChunk(), + new TrackChunk())); + + [Test] + public void Sanitize_Trim_SingleTrackChunk_1([Values(0, 100, 1000)] long firstEventTime) => Sanitize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A") { DeltaTime = firstEventTime })), + settings: new SanitizingSettings + { + RemoveEmptyTrackChunks = false + }, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A")))); + + [Test] + public void Sanitize_Trim_SingleTrackChunk_2([Values(0, 100, 1000)] long firstEventTime) => Sanitize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A") { DeltaTime = firstEventTime }), + new TrackChunk()), + settings: new SanitizingSettings + { + RemoveEmptyTrackChunks = false + }, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A")), + new TrackChunk())); + + [Test] + public void Sanitize_Trim_MultipleTrackChunks_1( + [Values(0, 100, 1000)] long aFirstEventTime, + [Values(0, 300, 3000)] long bFirstEventTime) + { + var minTime = Math.Min(aFirstEventTime, bFirstEventTime); + Sanitize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A") { DeltaTime = aFirstEventTime }), + new TrackChunk( + new TextEvent("B") { DeltaTime = bFirstEventTime })), + settings: new SanitizingSettings + { + RemoveEmptyTrackChunks = false + }, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A") { DeltaTime = aFirstEventTime - minTime }), + new TrackChunk( + new TextEvent("B") { DeltaTime = bFirstEventTime - minTime }))); + } + + [Test] + public void Sanitize_Trim_MultipleTrackChunks_2( + [Values(0, 100, 1000)] long aFirstEventTime, + [Values(0, 300, 3000)] long bFirstEventTime) + { + var minTime = Math.Min(aFirstEventTime, bFirstEventTime); + Sanitize( + midiFile: new MidiFile( + new TrackChunk( + new TextEvent("A") { DeltaTime = aFirstEventTime }), + new TrackChunk(), + new TrackChunk( + new TextEvent("B") { DeltaTime = bFirstEventTime })), + settings: new SanitizingSettings + { + RemoveEmptyTrackChunks = false + }, + expectedMidiFile: new MidiFile( + new TrackChunk( + new TextEvent("A") { DeltaTime = aFirstEventTime - minTime }), + new TrackChunk(), + new TrackChunk( + new TextEvent("B") { DeltaTime = bFirstEventTime - minTime }))); + } + + #endregion + } +} diff --git a/DryWetMidi.Tests/Tools/Splitter/SplitterTests.SplitObjectsByStep.cs b/DryWetMidi.Tests/Tools/Splitter/SplitterTests.SplitObjectsByStep.cs index 9de826470..4ffc24189 100644 --- a/DryWetMidi.Tests/Tools/Splitter/SplitterTests.SplitObjectsByStep.cs +++ b/DryWetMidi.Tests/Tools/Splitter/SplitterTests.SplitObjectsByStep.cs @@ -192,7 +192,7 @@ public void SplitObjectsByStep_MixedObjects() => CheckSplitObjectsByStep( new Note((SevenBitNumber)70, 100, 0), new Note((SevenBitNumber)90, 100, 30), new Note((SevenBitNumber)70, 100, 60)), - new Rest(10, 80, null, null) + new Rest(10, 80, null) }, step: (MidiTimeSpan)40, expectedObjects: new ILengthedObject[] @@ -213,8 +213,8 @@ public void SplitObjectsByStep_MixedObjects() => CheckSplitObjectsByStep( new Chord( new Note((SevenBitNumber)90, 10, 120), new Note((SevenBitNumber)70, 40, 120)), - new Rest(10, 40, null, null), - new Rest(50, 40, null, null) + new Rest(10, 40, null), + new Rest(50, 40, null) }); [Test] 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 f87280a44..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 @@ -282,8 +282,7 @@ private static void AreEqual(Note expectedNote, Note actualNote, string message) private static void AreEqual(Rest expectedRest, Rest actualRest, string message) { - Assert.AreEqual(expectedRest.NoteNumber, actualRest.NoteNumber, $"{message} Note number is invalid."); - Assert.AreEqual(expectedRest.Channel, actualRest.Channel, $"{message} Channel is invalid."); + Assert.AreEqual(expectedRest.Key, actualRest.Key, $"{message} Key is invalid."); Assert.AreEqual(expectedRest.Time, actualRest.Time, $"{message} Time is invalid."); Assert.AreEqual(expectedRest.Length, actualRest.Length, $"{message} Length is invalid."); } 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/Common/MathUtilities.cs b/DryWetMidi/Common/MathUtilities.cs index 7d7d88e09..315a956be 100644 --- a/DryWetMidi/Common/MathUtilities.cs +++ b/DryWetMidi/Common/MathUtilities.cs @@ -143,13 +143,6 @@ public static long GreatestCommonDivisor(long a, long b) return a; } - // ax + by = 0 - public static Tuple SolveDiophantineEquation(long a, long b) - { - var greatestCommonDivisor = GreatestCommonDivisor(a, b); - return Tuple.Create(b / greatestCommonDivisor, -a / greatestCommonDivisor); - } - public static double Round(double value) { return Math.Round(value, MidpointRounding.AwayFromZero); diff --git a/DryWetMidi/Core/MidiFile.cs b/DryWetMidi/Core/MidiFile.cs index 112a58082..fe506c3d2 100644 --- a/DryWetMidi/Core/MidiFile.cs +++ b/DryWetMidi/Core/MidiFile.cs @@ -129,12 +129,12 @@ public MidiFile(params MidiChunk[] chunks) public ChunksCollection Chunks { get; } = new ChunksCollection(); /// - /// Gets original format of the file was read or null if the current + /// Gets original format of the file that was read or null if the current /// was created by constructor. /// /// File format is unknown. /// Unable to get original format of the file. It means - /// the current was created via constructor rather than via Read method. + /// the current was created not as a result of reading the file. public MidiFileFormat OriginalFormat { get diff --git a/DryWetMidi/Interaction/Chords/Chord.cs b/DryWetMidi/Interaction/Chords/Chord.cs index 4107663cb..5e345fcdc 100644 --- a/DryWetMidi/Interaction/Chords/Chord.cs +++ b/DryWetMidi/Interaction/Chords/Chord.cs @@ -101,6 +101,11 @@ public Chord(IEnumerable notes, long time) /// /// Gets or sets absolute time of the chord in units defined by the time division of a MIDI file. /// + /// + /// Note that the returned value will be in ticks (not seconds, not milliseconds and so on). + /// Please read Time and length article to learn how you can + /// get the time in different representations. + /// /// is negative. public long Time { @@ -126,6 +131,11 @@ public long Time /// /// Gets or sets the length of the chord in units defined by the time division of a MIDI file. /// + /// + /// Note that the returned value will be in ticks (not seconds, not milliseconds and so on). + /// Please read Time and length article to learn how you can + /// get the length in different representations. + /// /// is negative. public long Length { diff --git a/DryWetMidi/Interaction/Chords/ChordsManagingUtilities.cs b/DryWetMidi/Interaction/Chords/ChordsManagingUtilities.cs index 406ddec93..6c3daae24 100644 --- a/DryWetMidi/Interaction/Chords/ChordsManagingUtilities.cs +++ b/DryWetMidi/Interaction/Chords/ChordsManagingUtilities.cs @@ -215,7 +215,7 @@ public static ICollection GetChords(this IEnumerable midiEvent result.Add(chord); } - return result; + return new SortedTimedObjectsImmutableCollection(result); } /// @@ -241,7 +241,7 @@ public static ICollection GetChords(this EventsCollection eventsCollectio var chords = chordsBuilder.GetChordsLazy(new[] { eventsCollection }.GetTimedEventsLazy(eventsCollection.Count, settings?.NoteDetectionSettings?.TimedEventDetectionSettings)); result.AddRange(chords.Select(c => c.Object)); - return result; + return new SortedTimedObjectsImmutableCollection(result); } /// @@ -296,7 +296,7 @@ public static ICollection GetChords(this IEnumerable trackChu var chords = chordsBuilder.GetChordsLazy(eventsCollections.GetTimedEventsLazy(eventsCount, settings?.NoteDetectionSettings?.TimedEventDetectionSettings)); result.AddRange(chords.Select(c => c.Object)); - return result; + return new SortedTimedObjectsImmutableCollection(result); } /// @@ -330,7 +330,7 @@ public static IEnumerable GetChords(this IEnumerable notes, ChordDe { ThrowIfArgument.IsNull(nameof(notes), notes); - return notes.GetChordsAndNotesAndTimedEventsLazy(settings).OfType().ToArray(); + return new SortedTimedObjectsImmutableCollection(notes.GetChordsAndNotesAndTimedEventsLazy(settings).OfType().ToArray()); } /// diff --git a/DryWetMidi/Interaction/GetObjects/GetObjectsUtilities.cs b/DryWetMidi/Interaction/GetObjects/GetObjectsUtilities.cs index 5a52c7bff..2bbb3467f 100644 --- a/DryWetMidi/Interaction/GetObjects/GetObjectsUtilities.cs +++ b/DryWetMidi/Interaction/GetObjects/GetObjectsUtilities.cs @@ -12,39 +12,6 @@ namespace Melanchall.DryWetMidi.Interaction /// public static class GetObjectsUtilities { - #region Constants - - private static readonly object NoSeparationNoteDescriptor = new object(); - - private static readonly Dictionary> NoteDescriptorProviders = - new Dictionary> - { - [RestSeparationPolicy.NoSeparation] = n => NoSeparationNoteDescriptor, - [RestSeparationPolicy.SeparateByChannel] = n => n.Channel, - [RestSeparationPolicy.SeparateByNoteNumber] = n => n.NoteNumber, - [RestSeparationPolicy.SeparateByChannelAndNoteNumber] = n => n.GetObjectId() - }; - - private static readonly Dictionary SetRestChannel = - new Dictionary - { - [RestSeparationPolicy.NoSeparation] = false, - [RestSeparationPolicy.SeparateByChannel] = true, - [RestSeparationPolicy.SeparateByNoteNumber] = false, - [RestSeparationPolicy.SeparateByChannelAndNoteNumber] = true - }; - - private static readonly Dictionary SetRestNoteNumber = - new Dictionary - { - [RestSeparationPolicy.NoSeparation] = false, - [RestSeparationPolicy.SeparateByChannel] = false, - [RestSeparationPolicy.SeparateByNoteNumber] = true, - [RestSeparationPolicy.SeparateByChannelAndNoteNumber] = true - }; - - #endregion - #region Methods /// @@ -77,19 +44,12 @@ public static ICollection GetObjects( /// Settings according to which objects should be detected and built. /// A lazy collection of objects built on top of . /// is null. - /// contains the - /// which is not supported by the method. public static IEnumerable EnumerateObjects( this IEnumerable midiEvents, ObjectType objectType, ObjectDetectionSettings settings = null) { ThrowIfArgument.IsNull(nameof(midiEvents), midiEvents); - ThrowIfArgument.DoesntSatisfyCondition( - nameof(objectType), - objectType, - t => !t.HasFlag(ObjectType.Rest), - "Rest object type specified."); return midiEvents .GetTimedEventsLazy(GetTimedEventDetectionSettings(objectType, settings)) @@ -162,7 +122,7 @@ public static ICollection GetObjects( ?? (objectType.HasFlag(ObjectType.Chord) ? settings?.ChordDetectionSettings?.NoteDetectionSettings?.TimedEventDetectionSettings : null)); var timedObjects = (IEnumerable)timedEvents.Select(o => o.Object); - if (objectType.HasFlag(ObjectType.Chord) || objectType.HasFlag(ObjectType.Note) || objectType.HasFlag(ObjectType.Rest)) + if (objectType.HasFlag(ObjectType.Chord) || objectType.HasFlag(ObjectType.Note)) { timedObjects = !objectType.HasFlag(ObjectType.Chord) ? timedEvents.GetNotesAndTimedEventsLazy(settings?.NoteDetectionSettings ?? new NoteDetectionSettings()).Select(o => o.Object) @@ -286,7 +246,6 @@ private static ICollection GetObjectsFromSortedTimedObjects( { var getChords = objectType.HasFlag(ObjectType.Chord); var getNotes = objectType.HasFlag(ObjectType.Note); - var getRests = objectType.HasFlag(ObjectType.Rest); var getTimedEvents = objectType.HasFlag(ObjectType.TimedEvent); settings = settings ?? new ObjectDetectionSettings(); @@ -294,11 +253,10 @@ private static ICollection GetObjectsFromSortedTimedObjects( ?? (getChords ? settings.ChordDetectionSettings?.NoteDetectionSettings : null) ?? new NoteDetectionSettings(); var chordDetectionSettings = settings.ChordDetectionSettings ?? new ChordDetectionSettings(); - var restDetectionSettings = settings.RestDetectionSettings ?? new RestDetectionSettings(); var timedObjects = processedTimedObjects; - if (createNotes && (getChords || getNotes || getRests)) + if (createNotes && (getChords || getNotes)) { var notesAndTimedEvents = processedTimedObjects.GetNotesAndTimedEventsLazy(noteDetectionSettings, true); @@ -313,11 +271,6 @@ private static ICollection GetObjectsFromSortedTimedObjects( ? new List(resultCollectionSize) : new List(); - var notesLastEndTimes = new Dictionary(); - var noteDescriptorProvider = NoteDescriptorProviders[restDetectionSettings.RestSeparationPolicy]; - var setRestChannel = SetRestChannel[restDetectionSettings.RestSeparationPolicy]; - var setRestNoteNumber = SetRestNoteNumber[restDetectionSettings.RestSeparationPolicy]; - foreach (var timedObject in timedObjects) { var processed = false; @@ -351,51 +304,10 @@ private static ICollection GetObjectsFromSortedTimedObjects( } } } - - if (getRests) - { - var note = timedObject as Note; - if (note != null) - { - var noteDescriptor = noteDescriptorProvider(note); - - long lastEndTime; - notesLastEndTimes.TryGetValue(noteDescriptor, out lastEndTime); - - if (note.Time > lastEndTime) - { - var rest = new Rest( - lastEndTime, - note.Time - lastEndTime, - setRestChannel ? (FourBitNumber?)note.Channel : null, - setRestNoteNumber ? (SevenBitNumber?)note.NoteNumber : null); - if (result.Count > 0) - { - var i = result.Count - 1; - - for (; i >= 0; i--) - { - if (rest.Time >= result[i].Time) - break; - } - - i++; - if (i >= result.Count) - result.Add(rest); - else - result.Insert(i, rest); - } - else - result.Add(rest); - } - - notesLastEndTimes[noteDescriptor] = Math.Max(lastEndTime, note.EndTime); - } - } } result.TrimExcess(); - return result; + return new SortedTimedObjectsImmutableCollection(result); } private static IEnumerable EnumerateObjectsFromSortedTimedObjects( @@ -406,7 +318,6 @@ private static IEnumerable EnumerateObjectsFromSortedTimedObjects( { var getChords = objectType.HasFlag(ObjectType.Chord); var getNotes = objectType.HasFlag(ObjectType.Note); - var getRests = objectType.HasFlag(ObjectType.Rest); var getTimedEvents = objectType.HasFlag(ObjectType.TimedEvent); settings = settings ?? new ObjectDetectionSettings(); @@ -417,7 +328,7 @@ private static IEnumerable EnumerateObjectsFromSortedTimedObjects( var timedObjects = processedTimedObjects; - if (createNotes && (getChords || getNotes || getRests)) + if (createNotes && (getChords || getNotes)) { var notesAndTimedEvents = processedTimedObjects.GetNotesAndTimedEventsLazy(noteDetectionSettings, true); diff --git a/DryWetMidi/Interaction/GetObjects/ObjectDetectionSettings.cs b/DryWetMidi/Interaction/GetObjects/ObjectDetectionSettings.cs index 1a0aae8bb..27ca46e87 100644 --- a/DryWetMidi/Interaction/GetObjects/ObjectDetectionSettings.cs +++ b/DryWetMidi/Interaction/GetObjects/ObjectDetectionSettings.cs @@ -22,11 +22,6 @@ public sealed class ObjectDetectionSettings /// public ChordDetectionSettings ChordDetectionSettings { get; set; } - /// - /// Gets or sets settings which define how rests should be detected and built. - /// - public RestDetectionSettings RestDetectionSettings { get; set; } - #endregion } } diff --git a/DryWetMidi/Interaction/GetObjects/ObjectType.cs b/DryWetMidi/Interaction/GetObjects/ObjectType.cs index e7d0c0194..662191174 100644 --- a/DryWetMidi/Interaction/GetObjects/ObjectType.cs +++ b/DryWetMidi/Interaction/GetObjects/ObjectType.cs @@ -22,10 +22,5 @@ public enum ObjectType /// Represents a chord (see ). /// Chord = 1 << 2, - - /// - /// Represents a rest (see ). - /// - Rest = 1 << 3, } } diff --git a/DryWetMidi/Interaction/GetObjects/RestSeparationPolicy.cs b/DryWetMidi/Interaction/GetObjects/RestSeparationPolicy.cs deleted file mode 100644 index f722436fa..000000000 --- a/DryWetMidi/Interaction/GetObjects/RestSeparationPolicy.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Melanchall.DryWetMidi.Interaction -{ - /// - /// Determines a rule for creating rests. The default value is . - /// More info in the - /// Getting objects: GetObjects: Rests article. - /// - public enum RestSeparationPolicy - { - /// - /// Rests should be constructed only when there are no notes at all on any channel. - /// - NoSeparation = 0, - - /// - /// Rests should be constructed individually for each channel ignoring note number. - /// - SeparateByChannel, - - /// - /// Rests should be constructed individually for each note number ignoring channel. - /// - SeparateByNoteNumber, - - /// - /// Rests should be constructed individually for each channel and note number. - /// - SeparateByChannelAndNoteNumber - } -} diff --git a/DryWetMidi/Interaction/GetObjects/Settings/RestDetectionSettings.cs b/DryWetMidi/Interaction/GetObjects/Settings/RestDetectionSettings.cs deleted file mode 100644 index 84af04b0a..000000000 --- a/DryWetMidi/Interaction/GetObjects/Settings/RestDetectionSettings.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.ComponentModel; -using Melanchall.DryWetMidi.Common; - -namespace Melanchall.DryWetMidi.Interaction -{ - /// - /// Settings which define how rests should be detected and built. - /// - /// - /// Please see Getting objects - /// (section GetObjects → Rests) article to learn more. - /// - /// - public sealed class RestDetectionSettings - { - #region Fields - - private RestSeparationPolicy _restSeparationPolicy = RestSeparationPolicy.NoSeparation; - - #endregion - - #region Properties - - /// - /// Gets or sets a value that defines a rule for creating rests. The default value is - /// . - /// - /// specified an invalid value. - public RestSeparationPolicy RestSeparationPolicy - { - get { return _restSeparationPolicy; } - set - { - ThrowIfArgument.IsInvalidEnumValue(nameof(value), value); - - _restSeparationPolicy = value; - } - } - - #endregion - } -} diff --git a/DryWetMidi/Interaction/Notes/Note.cs b/DryWetMidi/Interaction/Notes/Note.cs index 736c5a494..d1bee32ec 100644 --- a/DryWetMidi/Interaction/Notes/Note.cs +++ b/DryWetMidi/Interaction/Notes/Note.cs @@ -201,6 +201,11 @@ internal Note(TimedEvent timedNoteOnEvent, TimedEvent timedNoteOffEvent, bool ch /// /// Gets or sets absolute time of the note in units defined by the time division of a MIDI file. /// + /// + /// Note that the returned value will be in ticks (not seconds, not milliseconds and so on). + /// Please read Time and length article to learn how you can + /// get the time in different representations. + /// /// is negative. public long Time { @@ -223,6 +228,11 @@ public long Time /// /// Gets or sets the length of the note in units defined by the time division of a MIDI file. /// + /// + /// Note that the returned value will be in ticks (not seconds, not milliseconds and so on). + /// Please read Time and length article to learn how you can + /// get the length in different representations. + /// /// is negative. public long Length { diff --git a/DryWetMidi/Interaction/Notes/NotesManagingUtilities.cs b/DryWetMidi/Interaction/Notes/NotesManagingUtilities.cs index cf1d6134b..1b20cc09f 100644 --- a/DryWetMidi/Interaction/Notes/NotesManagingUtilities.cs +++ b/DryWetMidi/Interaction/Notes/NotesManagingUtilities.cs @@ -264,7 +264,7 @@ public static ICollection GetNotes(this IEnumerable midiEvents, var notes = notesBuilder.GetNotesLazy(midiEvents.GetTimedEventsLazy(settings?.TimedEventDetectionSettings)); result.AddRange(notes); - return result; + return new SortedTimedObjectsImmutableCollection(result); } /// @@ -290,7 +290,7 @@ public static ICollection GetNotes(this EventsCollection eventsCollection, var notes = notesBuilder.GetNotesLazy(eventsCollection.GetTimedEventsLazy(settings?.TimedEventDetectionSettings)); result.AddRange(notes); - return result; + return new SortedTimedObjectsImmutableCollection(result); } /// @@ -345,7 +345,7 @@ public static ICollection GetNotes(this IEnumerable trackChunk var notes = notesBuilder.GetNotesLazy(eventsCollections.GetTimedEventsLazy(eventsCount, settings?.TimedEventDetectionSettings)); result.AddRange(notes.Select(n => n.Object)); - return result; + return new SortedTimedObjectsImmutableCollection(result); } /// diff --git a/DryWetMidi/Interaction/ObjectId/ObjectIdUtilities.cs b/DryWetMidi/Interaction/ObjectId/ObjectIdUtilities.cs index 4e39dc672..81afdc300 100644 --- a/DryWetMidi/Interaction/ObjectId/ObjectIdUtilities.cs +++ b/DryWetMidi/Interaction/ObjectId/ObjectIdUtilities.cs @@ -26,7 +26,7 @@ public static object GetObjectId(this ITimedObject obj) var rest = obj as Rest; if (rest != null) - return new RestId(rest.Channel, rest.NoteNumber); + return new RestId(rest.Key); var registeredParameter = obj as RegisteredParameter; if (registeredParameter != null) diff --git a/DryWetMidi/Interaction/ObjectId/RestId.cs b/DryWetMidi/Interaction/ObjectId/RestId.cs index 543a282d5..d07bdf46e 100644 --- a/DryWetMidi/Interaction/ObjectId/RestId.cs +++ b/DryWetMidi/Interaction/ObjectId/RestId.cs @@ -1,24 +1,19 @@ -using Melanchall.DryWetMidi.Common; - -namespace Melanchall.DryWetMidi.Interaction +namespace Melanchall.DryWetMidi.Interaction { internal sealed class RestId { #region Constructor - public RestId(FourBitNumber? channel, SevenBitNumber? noteNumber) + public RestId(object key) { - Channel = channel; - NoteNumber = noteNumber; + Key = key; } #endregion #region Properties - public FourBitNumber? Channel { get; } - - public SevenBitNumber? NoteNumber { get; } + public object Key { get; } #endregion @@ -33,13 +28,12 @@ public override bool Equals(object obj) if (ReferenceEquals(restId, null)) return false; - return Channel == restId.Channel && - NoteNumber == restId.NoteNumber; + return Key?.Equals(restId.Key) == true; } public override int GetHashCode() { - return (Channel ?? 20) * 1000 + (NoteNumber ?? 200); + return Key?.GetHashCode() ?? 0; } #endregion diff --git a/DryWetMidi/Interaction/Parameters/Parameter.cs b/DryWetMidi/Interaction/Parameters/Parameter.cs index 02a77bfac..4b4b9ec54 100644 --- a/DryWetMidi/Interaction/Parameters/Parameter.cs +++ b/DryWetMidi/Interaction/Parameters/Parameter.cs @@ -53,6 +53,11 @@ public ParameterValueType ValueType /// /// Gets or sets absolute time of the parameter data in units defined by the time division of a MIDI file. /// + /// + /// Note that the returned value will be in ticks (not seconds, not milliseconds and so on). + /// Please read Time and length article to learn how you can + /// get the time in different representations. + /// /// is negative. public long Time { diff --git a/DryWetMidi/Interaction/GetObjects/Rest.cs b/DryWetMidi/Interaction/Rests/Rest.cs similarity index 82% rename from DryWetMidi/Interaction/GetObjects/Rest.cs rename to DryWetMidi/Interaction/Rests/Rest.cs index d3f71fe14..13444eb51 100644 --- a/DryWetMidi/Interaction/GetObjects/Rest.cs +++ b/DryWetMidi/Interaction/Rests/Rest.cs @@ -1,12 +1,7 @@ using System; -using Melanchall.DryWetMidi.Common; namespace Melanchall.DryWetMidi.Interaction { - /// - /// Represents a musical rest. More info in the - /// Getting objects: GetObjects: Rests article. - /// public sealed class Rest : ILengthedObject, INotifyTimeChanged, INotifyLengthChanged { #region Events @@ -32,13 +27,12 @@ public sealed class Rest : ILengthedObject, INotifyTimeChanged, INotifyLengthCha #region Constructor - internal Rest(long time, long length, FourBitNumber? channel, SevenBitNumber? noteNumber) + internal Rest(long time, long length, object key) { _time = time; _length = length; - Channel = channel; - NoteNumber = noteNumber; + Key = key; } #endregion @@ -48,6 +42,12 @@ internal Rest(long time, long length, FourBitNumber? channel, SevenBitNumber? no /// /// Gets start time of an object. /// + /// + /// Note that the returned value will be in ticks (not seconds, not milliseconds and so on). + /// Please read Time and length article to learn how you can + /// get the time in different representations. + /// + /// is negative. public long Time { get { return _time; } @@ -67,6 +67,12 @@ public long Time /// /// Gets length of an object. /// + /// + /// Note that the returned value will be in ticks (not seconds, not milliseconds and so on). + /// Please read Time and length article to learn how you can + /// get the length in different representations. + /// + /// is negative. public long Length { get { return _length; } @@ -88,15 +94,7 @@ public long Length /// public long EndTime => Time + Length; - /// - /// Gets a channel the rest was constructed for. - /// - public FourBitNumber? Channel { get; } - - /// - /// Gets a note number the rest was constructed for. - /// - public SevenBitNumber? NoteNumber { get; } + public object Key { get; } #endregion @@ -118,8 +116,7 @@ public long Length return rest1.Time == rest2.Time && rest1.Length == rest2.Length && - rest1.Channel == rest2.Channel && - rest1.NoteNumber == rest2.NoteNumber; + rest1.Key.Equals(rest2.Key); } /// @@ -143,7 +140,7 @@ public long Length /// Copy of the object. public ITimedObject Clone() { - return new Rest(Time, Length, Channel, NoteNumber); + return new Rest(Time, Length, Key); } #endregion @@ -184,7 +181,7 @@ public SplitLengthedObject Split(long time) /// A string that represents the current object. public override string ToString() { - return $"Rest (channel = {Channel}, note number = {NoteNumber})"; + return $"Rest (key = {Key})"; } /// @@ -208,8 +205,7 @@ public override int GetHashCode() var result = 17; result = result * 23 + Time.GetHashCode(); result = result * 23 + Length.GetHashCode(); - result = result * 23 + Channel.GetHashCode(); - result = result * 23 + NoteNumber.GetHashCode(); + result = result * 23 + Key.GetHashCode(); return result; } } diff --git a/DryWetMidi/Interaction/Rests/RestDetectionSettings.cs b/DryWetMidi/Interaction/Rests/RestDetectionSettings.cs new file mode 100644 index 000000000..f3698539b --- /dev/null +++ b/DryWetMidi/Interaction/Rests/RestDetectionSettings.cs @@ -0,0 +1,13 @@ +using System; + +namespace Melanchall.DryWetMidi.Interaction +{ + public sealed class RestDetectionSettings + { + #region Properties + + public Func KeySelector { get; set; } + + #endregion + } +} diff --git a/DryWetMidi/Interaction/Rests/RestsUtilities.cs b/DryWetMidi/Interaction/Rests/RestsUtilities.cs new file mode 100644 index 000000000..51b65aedd --- /dev/null +++ b/DryWetMidi/Interaction/Rests/RestsUtilities.cs @@ -0,0 +1,117 @@ +using Melanchall.DryWetMidi.Common; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Melanchall.DryWetMidi.Interaction +{ + public static class RestsUtilities + { + #region Methods + + public static IEnumerable WithRests( + this IEnumerable timedObjects, + RestDetectionSettings settings) + { + ThrowIfArgument.IsNull(nameof(timedObjects), timedObjects); + + timedObjects = GetSortedObjects(timedObjects); + var rests = GetSortedRestsFromObjects(timedObjects, settings); + return EnumerateObjectsAndRests(timedObjects, rests); + } + + public static ICollection GetRests( + this IEnumerable timedObjects, + RestDetectionSettings settings) + { + ThrowIfArgument.IsNull(nameof(timedObjects), timedObjects); + + timedObjects = GetSortedObjects(timedObjects); + return GetSortedRestsFromObjects(timedObjects, settings); + } + + private static IEnumerable GetSortedObjects( + IEnumerable timedObjects) + { + return timedObjects is ISortedTimedObjectsImmutableCollection + ? timedObjects + : timedObjects.OrderBy(o => o.Time); + } + + private static IEnumerable EnumerateObjectsAndRests( + IEnumerable objects, + IEnumerable rests) + { + var objectsEnumerator = objects.GetEnumerator(); + var objectCanBeTaken = objectsEnumerator.MoveNext(); + + var restsEnumerator = rests.GetEnumerator(); + var restCanBeTaken = restsEnumerator.MoveNext(); + + while (objectCanBeTaken && restCanBeTaken) + { + var rest = restsEnumerator.Current; + var obj = objectsEnumerator.Current; + + if (obj.Time <= rest.Time) + { + yield return obj; + objectCanBeTaken = objectsEnumerator.MoveNext(); + } + else + { + yield return rest; + restCanBeTaken = restsEnumerator.MoveNext(); + } + } + + while (objectCanBeTaken) + { + yield return objectsEnumerator.Current; + objectCanBeTaken = objectsEnumerator.MoveNext(); + } + } + + private static ICollection GetSortedRestsFromObjects( + IEnumerable objects, + RestDetectionSettings restDetectionSettings) + { + restDetectionSettings = restDetectionSettings ?? new RestDetectionSettings(); + + var endTimes = new Dictionary(); + var keySelector = restDetectionSettings.KeySelector; + + var rests = new List(); + + foreach (var obj in objects) + { + var key = keySelector?.Invoke(obj); + if (key == null) + continue; + + long lastEndTime; + endTimes.TryGetValue(key, out lastEndTime); + + if (obj.Time > lastEndTime) + { + var rest = new Rest( + lastEndTime, + obj.Time - lastEndTime, + key); + + rests.Add(rest); + } + + endTimes[key] = Math.Max( + lastEndTime, + obj is ILengthedObject ? ((ILengthedObject)obj).EndTime : obj.Time); + } + + rests.Sort((r1, r2) => Math.Sign(r1.Time - r2.Time)); + + return rests; + } + + #endregion + } +} diff --git a/DryWetMidi/Interaction/TimeSpan/Converters/MusicalTimeSpanConverter.cs b/DryWetMidi/Interaction/TimeSpan/Converters/MusicalTimeSpanConverter.cs index 8d785fe11..078cac2e9 100644 --- a/DryWetMidi/Interaction/TimeSpan/Converters/MusicalTimeSpanConverter.cs +++ b/DryWetMidi/Interaction/TimeSpan/Converters/MusicalTimeSpanConverter.cs @@ -17,8 +17,8 @@ public ITimeSpan ConvertTo(long timeSpan, long time, TempoMap tempoMap) if (timeSpan == 0) return new MusicalTimeSpan(); - var xy = MathUtilities.SolveDiophantineEquation(4 * ticksPerQuarterNoteTimeDivision.TicksPerQuarterNote, -timeSpan); - return new MusicalTimeSpan(Math.Abs(xy.Item1), Math.Abs(xy.Item2)); + var gcd = MathUtilities.GreatestCommonDivisor(timeSpan, 4 * ticksPerQuarterNoteTimeDivision.TicksPerQuarterNote); + return new MusicalTimeSpan(timeSpan / gcd, 4 * ticksPerQuarterNoteTimeDivision.TicksPerQuarterNote / gcd); } public long ConvertFrom(ITimeSpan timeSpan, long time, TempoMap tempoMap) diff --git a/DryWetMidi/Interaction/TimedEvents/TimedEvent.cs b/DryWetMidi/Interaction/TimedEvents/TimedEvent.cs index f861b48e5..55fa7138b 100644 --- a/DryWetMidi/Interaction/TimedEvents/TimedEvent.cs +++ b/DryWetMidi/Interaction/TimedEvents/TimedEvent.cs @@ -62,6 +62,11 @@ public TimedEvent(MidiEvent midiEvent, long time) /// /// Gets or sets absolute time of the event in units defined by the time division of a MIDI file. /// + /// + /// Note that the returned value will be in ticks (not seconds, not milliseconds and so on). + /// Please read Time and length article to learn how you can + /// get the time in different representations. + /// /// is negative. public long Time { diff --git a/DryWetMidi/Interaction/TimedEvents/TimedEventsManagingUtilities.cs b/DryWetMidi/Interaction/TimedEvents/TimedEventsManagingUtilities.cs index 2d3e537f4..555b59408 100644 --- a/DryWetMidi/Interaction/TimedEvents/TimedEventsManagingUtilities.cs +++ b/DryWetMidi/Interaction/TimedEvents/TimedEventsManagingUtilities.cs @@ -82,7 +82,7 @@ public static ICollection GetTimedEvents(this EventsCollection event result.Add(timedEvent); } - return result; + return new SortedTimedObjectsImmutableCollection(result); } /// @@ -131,7 +131,7 @@ public static ICollection GetTimedEvents(this IEnumerable(result); } /// diff --git a/DryWetMidi/Interaction/TimedObject/ISortedTimedObjectsImmutableCollection.cs b/DryWetMidi/Interaction/TimedObject/ISortedTimedObjectsImmutableCollection.cs new file mode 100644 index 000000000..16ab5c12c --- /dev/null +++ b/DryWetMidi/Interaction/TimedObject/ISortedTimedObjectsImmutableCollection.cs @@ -0,0 +1,6 @@ +namespace Melanchall.DryWetMidi.Interaction +{ + internal interface ISortedTimedObjectsImmutableCollection + { + } +} diff --git a/DryWetMidi/Interaction/TimedObject/SortedTimedObjectsImmutableCollection.cs b/DryWetMidi/Interaction/TimedObject/SortedTimedObjectsImmutableCollection.cs new file mode 100644 index 000000000..747a8cda3 --- /dev/null +++ b/DryWetMidi/Interaction/TimedObject/SortedTimedObjectsImmutableCollection.cs @@ -0,0 +1,73 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Melanchall.DryWetMidi.Interaction +{ + internal sealed class SortedTimedObjectsImmutableCollection : ISortedTimedObjectsImmutableCollection, ICollection + { + #region Fields + + private readonly ICollection _objects; + + #endregion + + #region Constructor + + public SortedTimedObjectsImmutableCollection( + ICollection objects) + { + _objects = objects; + } + + #endregion + + #region Properties + + public int Count + { + get { return _objects.Count; } + } + + public bool IsReadOnly + { + get { return true; } + } + + #endregion + + public void Add(TObject item) + { + throw new System.NotSupportedException(); + } + + public void Clear() + { + throw new System.NotSupportedException(); + } + + public bool Contains(TObject item) + { + return _objects.Contains(item); + } + + public void CopyTo(TObject[] array, int arrayIndex) + { + _objects.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return _objects.GetEnumerator(); + } + + public bool Remove(TObject item) + { + throw new System.NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_objects).GetEnumerator(); + } + } +} diff --git a/DryWetMidi/Interaction/ValueLine/ValueChange.cs b/DryWetMidi/Interaction/ValueLine/ValueChange.cs index 908c9ae50..d4d89e763 100644 --- a/DryWetMidi/Interaction/ValueLine/ValueChange.cs +++ b/DryWetMidi/Interaction/ValueLine/ValueChange.cs @@ -41,6 +41,12 @@ internal ValueChange(long time, TValue value) /// /// Gets the MIDI time when value is changed. /// + /// + /// Note that the returned value will be in ticks (not seconds, not milliseconds and so on). + /// Please read Time and length article to learn how you can + /// get the time in different representations. + /// + /// Setting time of value change object is not allowed. public long Time { get { return _time; } diff --git a/DryWetMidi/Multimedia/Recording/MidiEventRecordedEventArgs.cs b/DryWetMidi/Multimedia/Recording/MidiEventRecordedEventArgs.cs new file mode 100644 index 000000000..e9c2db282 --- /dev/null +++ b/DryWetMidi/Multimedia/Recording/MidiEventRecordedEventArgs.cs @@ -0,0 +1,25 @@ +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; + +namespace Melanchall.DryWetMidi.Multimedia +{ + public sealed class MidiEventRecordedEventArgs + { + #region Constructor + + internal MidiEventRecordedEventArgs(MidiEvent midiEvent) + { + ThrowIfArgument.IsNull(nameof(midiEvent), midiEvent); + + Event = midiEvent; + } + + #endregion + + #region Properties + + public MidiEvent Event { get; } + + #endregion + } +} diff --git a/DryWetMidi/Multimedia/Recording/Recording.cs b/DryWetMidi/Multimedia/Recording/Recording.cs index eebf01e5c..da99ccbd2 100644 --- a/DryWetMidi/Multimedia/Recording/Recording.cs +++ b/DryWetMidi/Multimedia/Recording/Recording.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Linq; using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; using Melanchall.DryWetMidi.Interaction; namespace Melanchall.DryWetMidi.Multimedia @@ -26,6 +27,8 @@ public sealed class Recording : IDisposable /// public event EventHandler Stopped; + public event EventHandler EventRecorded; + #endregion #region Fields @@ -120,11 +123,11 @@ public TTimeSpan GetDuration() /// Gets MIDI events recorded by the current . /// /// MIDI events recorded by the current . - public IReadOnlyList GetEvents() + public ICollection GetEvents() { - return _events.Select(e => new TimedEvent(e.Event, TimeConverter.ConvertFrom((MetricTimeSpan)e.Time, TempoMap))) - .ToList() - .AsReadOnly(); + return _events + .Select(e => new TimedEvent(e.Event, TimeConverter.ConvertFrom((MetricTimeSpan)e.Time, TempoMap))) + .ToArray(); } /// @@ -172,6 +175,13 @@ private void OnEventReceived(object sender, MidiEventReceivedEventArgs e) return; _events.Add(new RecordingEvent(e.Event, _stopwatch.Elapsed)); + + OnEventRecorded(e.Event); + } + + private void OnEventRecorded(MidiEvent midiEvent) + { + EventRecorded?.Invoke(this, new MidiEventRecordedEventArgs(midiEvent)); } #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/DryWetMidi/Tools/Merger/ObjectsMerger.cs b/DryWetMidi/Tools/Merger/ObjectsMerger.cs index 853853d65..298e29566 100644 --- a/DryWetMidi/Tools/Merger/ObjectsMerger.cs +++ b/DryWetMidi/Tools/Merger/ObjectsMerger.cs @@ -21,7 +21,7 @@ public class ObjectsMerger /// /// The type of objects in the current group. /// - protected readonly ObjectType _objectsType; + protected readonly Type _objectsType; #endregion @@ -92,15 +92,14 @@ public virtual ILengthedObject MergeObjects(ObjectsMergingSettings settings) if (_objects.Count == 1) return (ILengthedObject)_objects.First().Clone(); - switch (_objectsType) - { - case ObjectType.Note: - return MergeNotes(settings); - case ObjectType.Rest: - return MergeRests(); - case ObjectType.Chord: - return MergeChords(settings); - } + if (_objectsType == typeof(Note)) + return MergeNotes(settings); + + if (_objectsType == typeof(Chord)) + return MergeChords(settings); + + if (_objectsType == typeof(Rest)) + return MergeRests(); throw new NotImplementedException($"Merging objects of {_objectsType} type is not implemented."); } @@ -161,18 +160,9 @@ private static SevenBitNumber MergeVelocities( throw new NotImplementedException($"Merging velocities by {velocityMergingPolicy} policy is not implemented."); } - private ObjectType GetObjectsType() + private Type GetObjectsType() { - var firstObject = _objects.First(); - - if (firstObject is Note) - return ObjectType.Note; - if (firstObject is Chord) - return ObjectType.Chord; - if (firstObject is Rest) - return ObjectType.Rest; - - throw new NotImplementedException($"Getting object type for {firstObject} is not implemented."); + return _objects.First().GetType(); } #endregion diff --git a/DryWetMidi/Tools/Repeater/Repeater.cs b/DryWetMidi/Tools/Repeater/Repeater.cs index 7557050b5..29cc2e61b 100644 --- a/DryWetMidi/Tools/Repeater/Repeater.cs +++ b/DryWetMidi/Tools/Repeater/Repeater.cs @@ -24,15 +24,18 @@ public class Repeater /// A new instance of the which is the /// repeated times using . /// is null. - /// is zero or negative. + /// is negative. /// of the /// is null for fixed-value shift. public MidiFile Repeat(MidiFile midiFile, int repeatsNumber, RepeatingSettings settings = null) { ThrowIfArgument.IsNull(nameof(midiFile), midiFile); - ThrowIfArgument.IsNonpositive(nameof(repeatsNumber), repeatsNumber, "Repeats number is zero or negative."); + ThrowIfArgument.IsNegative(nameof(repeatsNumber), repeatsNumber, "Repeats number is negative."); CheckSettings(settings); + if (repeatsNumber == 0) + return midiFile.Clone(); + settings = settings ?? new RepeatingSettings(); var tempoMap = midiFile.GetTempoMap(); @@ -64,16 +67,19 @@ public MidiFile Repeat(MidiFile midiFile, int repeatsNumber, RepeatingSettings s /// /// /// - /// is zero or negative. + /// is negative. /// of the /// is null for fixed-value shift. public ICollection Repeat(IEnumerable trackChunks, int repeatsNumber, TempoMap tempoMap, RepeatingSettings settings = null) { ThrowIfArgument.IsNull(nameof(trackChunks), trackChunks); ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); - ThrowIfArgument.IsNonpositive(nameof(repeatsNumber), repeatsNumber, "Repeats number is zero or negative."); + ThrowIfArgument.IsNegative(nameof(repeatsNumber), repeatsNumber, "Repeats number is negative."); CheckSettings(settings); + if (repeatsNumber == 0) + return trackChunks.Select(c => (TrackChunk)c.Clone()).ToArray(); + settings = settings ?? new RepeatingSettings(); var timedEventsCollections = trackChunks.Select(trackChunk => trackChunk.GetTimedEvents()).ToArray(); @@ -105,16 +111,19 @@ public ICollection Repeat(IEnumerable trackChunks, int r /// /// /// - /// is zero or negative. + /// is negative. /// of the /// is null for fixed-value shift. public TrackChunk Repeat(TrackChunk trackChunk, int repeatsNumber, TempoMap tempoMap, RepeatingSettings settings = null) { ThrowIfArgument.IsNull(nameof(trackChunk), trackChunk); ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); - ThrowIfArgument.IsNonpositive(nameof(repeatsNumber), repeatsNumber, "Repeats number is zero or negative."); + ThrowIfArgument.IsNegative(nameof(repeatsNumber), repeatsNumber, "Repeats number is negative."); CheckSettings(settings); + if (repeatsNumber == 0) + return (TrackChunk)trackChunk.Clone(); + settings = settings ?? new RepeatingSettings(); var timedObjects = trackChunk.GetTimedEvents(); @@ -144,16 +153,19 @@ public TrackChunk Repeat(TrackChunk trackChunk, int repeatsNumber, TempoMap temp /// /// /// - /// is zero or negative. + /// is negative. /// of the /// is null for fixed-value shift. public ICollection Repeat(IEnumerable timedObjects, int repeatsNumber, TempoMap tempoMap, RepeatingSettings settings = null) { ThrowIfArgument.IsNull(nameof(timedObjects), timedObjects); ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); - ThrowIfArgument.IsNonpositive(nameof(repeatsNumber), repeatsNumber, "Repeats number is zero or negative."); + ThrowIfArgument.IsNegative(nameof(repeatsNumber), repeatsNumber, "Repeats number is negative."); CheckSettings(settings); + if (repeatsNumber == 0) + return timedObjects.Select(o => o.Clone()).ToArray(); + settings = settings ?? new RepeatingSettings(); var maxTime = timedObjects.Select(obj => (obj?.Time ?? 0) + ((obj as ILengthedObject)?.Length ?? 0)).DefaultIfEmpty(0).Max(); diff --git a/DryWetMidi/Tools/Repeater/RepeaterUtilities.cs b/DryWetMidi/Tools/Repeater/RepeaterUtilities.cs index 967a41860..0cd5211d9 100644 --- a/DryWetMidi/Tools/Repeater/RepeaterUtilities.cs +++ b/DryWetMidi/Tools/Repeater/RepeaterUtilities.cs @@ -24,13 +24,13 @@ public static class RepeaterUtilities /// A new instance of the which is the /// repeated times using . /// is null. - /// is zero or negative. + /// is negative. /// of the /// is null for fixed-value shift. public static MidiFile Repeat(this MidiFile midiFile, int repeatsNumber, RepeatingSettings settings = null) { ThrowIfArgument.IsNull(nameof(midiFile), midiFile); - ThrowIfArgument.IsNonpositive(nameof(repeatsNumber), repeatsNumber, "Repeats number is zero or negative."); + ThrowIfArgument.IsNegative(nameof(repeatsNumber), repeatsNumber, "Repeats number is negative."); return new Repeater().Repeat(midiFile, repeatsNumber, settings); } @@ -56,14 +56,14 @@ public static MidiFile Repeat(this MidiFile midiFile, int repeatsNumber, Repeati /// /// /// - /// is zero or negative. + /// is negative. /// of the /// is null for fixed-value shift. public static TrackChunk Repeat(this TrackChunk trackChunk, int repeatsNumber, TempoMap tempoMap, RepeatingSettings settings = null) { ThrowIfArgument.IsNull(nameof(trackChunk), trackChunk); ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); - ThrowIfArgument.IsNonpositive(nameof(repeatsNumber), repeatsNumber, "Repeats number is zero or negative."); + ThrowIfArgument.IsNegative(nameof(repeatsNumber), repeatsNumber, "Repeats number is negative."); return new Repeater().Repeat(trackChunk, repeatsNumber, tempoMap, settings); } @@ -89,14 +89,14 @@ public static TrackChunk Repeat(this TrackChunk trackChunk, int repeatsNumber, T /// /// /// - /// is zero or negative. + /// is negative. /// of the /// is null for fixed-value shift. public static ICollection Repeat(this IEnumerable trackChunks, int repeatsNumber, TempoMap tempoMap, RepeatingSettings settings = null) { ThrowIfArgument.IsNull(nameof(trackChunks), trackChunks); ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); - ThrowIfArgument.IsNonpositive(nameof(repeatsNumber), repeatsNumber, "Repeats number is zero or negative."); + ThrowIfArgument.IsNegative(nameof(repeatsNumber), repeatsNumber, "Repeats number is negative."); return new Repeater().Repeat(trackChunks, repeatsNumber, tempoMap, settings); } @@ -122,14 +122,14 @@ public static ICollection Repeat(this IEnumerable trackC /// /// /// - /// is zero or negative. + /// is negative. /// of the /// is null for fixed-value shift. public static ICollection Repeat(this IEnumerable timedObjects, int repeatsNumber, TempoMap tempoMap, RepeatingSettings settings = null) { ThrowIfArgument.IsNull(nameof(timedObjects), timedObjects); ThrowIfArgument.IsNull(nameof(tempoMap), tempoMap); - ThrowIfArgument.IsNonpositive(nameof(repeatsNumber), repeatsNumber, "Repeats number is zero or negative."); + ThrowIfArgument.IsNegative(nameof(repeatsNumber), repeatsNumber, "Repeats number is negative."); return new Repeater().Repeat(timedObjects, repeatsNumber, tempoMap, settings); } diff --git a/DryWetMidi/Tools/Sanitizer/Sanitizer.cs b/DryWetMidi/Tools/Sanitizer/Sanitizer.cs index d31ef1ebb..35e27c66a 100644 --- a/DryWetMidi/Tools/Sanitizer/Sanitizer.cs +++ b/DryWetMidi/Tools/Sanitizer/Sanitizer.cs @@ -32,6 +32,31 @@ public static void Sanitize(this MidiFile midiFile, SanitizingSettings settings var usedChannels = RemoveDuplicatedEventsCollectingUsedChannels(midiFile, settings); RemoveEventsOnUnusedChannels(midiFile, settings, usedChannels); RemoveEmptyTrackChunks(midiFile, settings); + TrimFile(midiFile, settings); + } + + private static void TrimFile( + MidiFile midiFile, + SanitizingSettings settings) + { + if (!settings.Trim) + return; + + var nonEmptyTrackChunks = midiFile + .GetTrackChunks() + .Where(c => c.Events.Any()) + .ToArray(); + + if (!nonEmptyTrackChunks.Any()) + return; + + var minStartTime = nonEmptyTrackChunks + .Min(c => c.Events.First().DeltaTime); + + foreach (var trackChunk in nonEmptyTrackChunks) + { + trackChunk.Events.First().DeltaTime -= minStartTime; + } } private static void RemoveEmptyTrackChunks( diff --git a/DryWetMidi/Tools/Sanitizer/SanitizingSettings.cs b/DryWetMidi/Tools/Sanitizer/SanitizingSettings.cs index 761396b61..6c644f5cc 100644 --- a/DryWetMidi/Tools/Sanitizer/SanitizingSettings.cs +++ b/DryWetMidi/Tools/Sanitizer/SanitizingSettings.cs @@ -78,6 +78,8 @@ public sealed class SanitizingSettings /// public bool RemoveEventsOnUnusedChannels { get; set; } = true; + public bool Trim { get; set; } = true; + #endregion } } diff --git a/Resources/CI/run-static-analysis.yaml b/Resources/CI/run-static-analysis.yaml index af0408b16..3804623c0 100644 --- a/Resources/CI/run-static-analysis.yaml +++ b/Resources/CI/run-static-analysis.yaml @@ -42,7 +42,7 @@ stages: - task: CmdLine@2 displayName: Run analysis inputs: - script: 'jb inspectcode Melanchall.DryWetMidi.sln --properties:Configuration=DebugTest --exclude="DryWetMidi.Benchmarks\**.*" -o=ReSharperReport.xml' + script: 'jb inspectcode Melanchall.DryWetMidi.sln --properties:Configuration=DebugTest --exclude="DryWetMidi.Benchmarks\**.*" -o=ReSharperReport.xml -f="xml"' - task: DotNetCoreCLI@2 displayName: Convert report to HTML