From 935e24b052ef8cb51ffca528cc263c6506c38f45 Mon Sep 17 00:00:00 2001 From: Scott Doxey Date: Wed, 13 Nov 2024 01:52:20 -0500 Subject: [PATCH 1/4] Added ReadMidiFile method. --- RhythmGameUtilities.Tests/MidiTest.cs | 53 ++++++++ RhythmGameUtilities.Tests/Mocks/song.mid | Bin 0 -> 219 bytes RhythmGameUtilities/Scripts/Midi.cs | 42 ++++++ UnityPackage/Editor/Tests/MidiTest.cs | 53 ++++++++ UnityPackage/Scripts/Midi.cs | 42 ++++++ include/RhythmGameUtilities/Midi.hpp | 121 ++++++++++++++++++ include/RhythmGameUtilities/MidiInternal.hpp | 39 ++++++ .../RhythmGameUtilities.cpp | 2 + tests/Mocks/song.mid | Bin 0 -> 219 bytes tests/RhythmGameUtilities/Midi.cpp | 52 ++++++++ 10 files changed, 404 insertions(+) create mode 100644 RhythmGameUtilities.Tests/MidiTest.cs create mode 100644 RhythmGameUtilities.Tests/Mocks/song.mid create mode 100644 RhythmGameUtilities/Scripts/Midi.cs create mode 100644 UnityPackage/Editor/Tests/MidiTest.cs create mode 100644 UnityPackage/Scripts/Midi.cs create mode 100644 include/RhythmGameUtilities/Midi.hpp create mode 100644 include/RhythmGameUtilities/MidiInternal.hpp create mode 100644 tests/Mocks/song.mid create mode 100644 tests/RhythmGameUtilities/Midi.cpp diff --git a/RhythmGameUtilities.Tests/MidiTest.cs b/RhythmGameUtilities.Tests/MidiTest.cs new file mode 100644 index 0000000..e481246 --- /dev/null +++ b/RhythmGameUtilities.Tests/MidiTest.cs @@ -0,0 +1,53 @@ +using System.IO; +using NUnit.Framework; + +namespace RhythmGameUtilities.Tests +{ + + public class MidiTest + { + + [Test] + public void TestReadMidiFile() + { + var directory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + var path = Path.GetFullPath(Path.Combine(directory, "../../../Mocks/song.mid")); + + var notes = Midi.ReadMidiFile(path); + + Assert.That(notes.Count, Is.EqualTo(10)); + + Assert.That(notes[0].Position, Is.EqualTo(0)); + Assert.That(notes[0].HandPosition, Is.EqualTo(48)); + + Assert.That(notes[1].Position, Is.EqualTo(480)); + Assert.That(notes[1].HandPosition, Is.EqualTo(50)); + + Assert.That(notes[2].Position, Is.EqualTo(960)); + Assert.That(notes[2].HandPosition, Is.EqualTo(52)); + + Assert.That(notes[3].Position, Is.EqualTo(1440)); + Assert.That(notes[3].HandPosition, Is.EqualTo(54)); + + Assert.That(notes[4].Position, Is.EqualTo(1920)); + Assert.That(notes[4].HandPosition, Is.EqualTo(56)); + + Assert.That(notes[5].Position, Is.EqualTo(2400)); + Assert.That(notes[5].HandPosition, Is.EqualTo(48)); + + Assert.That(notes[6].Position, Is.EqualTo(2400)); + Assert.That(notes[6].HandPosition, Is.EqualTo(50)); + + Assert.That(notes[7].Position, Is.EqualTo(2400)); + Assert.That(notes[7].HandPosition, Is.EqualTo(52)); + + Assert.That(notes[8].Position, Is.EqualTo(2400)); + Assert.That(notes[8].HandPosition, Is.EqualTo(54)); + + Assert.That(notes[9].Position, Is.EqualTo(2400)); + Assert.That(notes[9].HandPosition, Is.EqualTo(56)); + } + + } + +} diff --git a/RhythmGameUtilities.Tests/Mocks/song.mid b/RhythmGameUtilities.Tests/Mocks/song.mid new file mode 100644 index 0000000000000000000000000000000000000000..383958aa7a075f991ee2cb5c9f2ec4f3cfd3797f GIT binary patch literal 219 zcmXYrF%E)25JletS&Z6iX=Uq%5JKq%C`>3kz?zuY5IZa723*0$dXL~yxXC&LR`dS+ zH_0^Z)dLnoV#cPu-z05_Da3P@CEEq@l;-ap@xZ_S5FS#C5|&3o?g^_W;Ud{51S`9V z&CGk*Pg$m2Ga|i#FAUE2-OJH;L!neCl}e>jrBo@^O0|;nh>KWA&)hhWTjUDy5mUeY E0bdn73jhEB literal 0 HcmV?d00001 diff --git a/RhythmGameUtilities/Scripts/Midi.cs b/RhythmGameUtilities/Scripts/Midi.cs new file mode 100644 index 0000000..9c05062 --- /dev/null +++ b/RhythmGameUtilities/Scripts/Midi.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace RhythmGameUtilities +{ + + internal static class MidiInternal + { + + [DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr ReadMidiFileInternal(string filename, out int size); + + } + + public static class Midi + { + + public static List ReadMidiFile(string path) + { + var notes = new List(); + + var ptrArray = MidiInternal.ReadMidiFileInternal(path, out var size); + + var noteSize = Marshal.SizeOf(typeof(Note)); + + for (var i = 0; i < size; i += 1) + { + var noteSizePtr = new IntPtr(ptrArray.ToInt64() + noteSize * i); + var note = Marshal.PtrToStructure(noteSizePtr); + + notes.Add(note); + } + + Marshal.FreeHGlobal(ptrArray); + + return notes; + } + + } + +} diff --git a/UnityPackage/Editor/Tests/MidiTest.cs b/UnityPackage/Editor/Tests/MidiTest.cs new file mode 100644 index 0000000..e481246 --- /dev/null +++ b/UnityPackage/Editor/Tests/MidiTest.cs @@ -0,0 +1,53 @@ +using System.IO; +using NUnit.Framework; + +namespace RhythmGameUtilities.Tests +{ + + public class MidiTest + { + + [Test] + public void TestReadMidiFile() + { + var directory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + var path = Path.GetFullPath(Path.Combine(directory, "../../../Mocks/song.mid")); + + var notes = Midi.ReadMidiFile(path); + + Assert.That(notes.Count, Is.EqualTo(10)); + + Assert.That(notes[0].Position, Is.EqualTo(0)); + Assert.That(notes[0].HandPosition, Is.EqualTo(48)); + + Assert.That(notes[1].Position, Is.EqualTo(480)); + Assert.That(notes[1].HandPosition, Is.EqualTo(50)); + + Assert.That(notes[2].Position, Is.EqualTo(960)); + Assert.That(notes[2].HandPosition, Is.EqualTo(52)); + + Assert.That(notes[3].Position, Is.EqualTo(1440)); + Assert.That(notes[3].HandPosition, Is.EqualTo(54)); + + Assert.That(notes[4].Position, Is.EqualTo(1920)); + Assert.That(notes[4].HandPosition, Is.EqualTo(56)); + + Assert.That(notes[5].Position, Is.EqualTo(2400)); + Assert.That(notes[5].HandPosition, Is.EqualTo(48)); + + Assert.That(notes[6].Position, Is.EqualTo(2400)); + Assert.That(notes[6].HandPosition, Is.EqualTo(50)); + + Assert.That(notes[7].Position, Is.EqualTo(2400)); + Assert.That(notes[7].HandPosition, Is.EqualTo(52)); + + Assert.That(notes[8].Position, Is.EqualTo(2400)); + Assert.That(notes[8].HandPosition, Is.EqualTo(54)); + + Assert.That(notes[9].Position, Is.EqualTo(2400)); + Assert.That(notes[9].HandPosition, Is.EqualTo(56)); + } + + } + +} diff --git a/UnityPackage/Scripts/Midi.cs b/UnityPackage/Scripts/Midi.cs new file mode 100644 index 0000000..9c05062 --- /dev/null +++ b/UnityPackage/Scripts/Midi.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace RhythmGameUtilities +{ + + internal static class MidiInternal + { + + [DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr ReadMidiFileInternal(string filename, out int size); + + } + + public static class Midi + { + + public static List ReadMidiFile(string path) + { + var notes = new List(); + + var ptrArray = MidiInternal.ReadMidiFileInternal(path, out var size); + + var noteSize = Marshal.SizeOf(typeof(Note)); + + for (var i = 0; i < size; i += 1) + { + var noteSizePtr = new IntPtr(ptrArray.ToInt64() + noteSize * i); + var note = Marshal.PtrToStructure(noteSizePtr); + + notes.Add(note); + } + + Marshal.FreeHGlobal(ptrArray); + + return notes; + } + + } + +} diff --git a/include/RhythmGameUtilities/Midi.hpp b/include/RhythmGameUtilities/Midi.hpp new file mode 100644 index 0000000..3a10fa5 --- /dev/null +++ b/include/RhythmGameUtilities/Midi.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include + +#include "Structs/Note.hpp" + +namespace RhythmGameUtilities +{ + +template T ByteSwap(T value, int length = 8) +{ + return (value >> length) | (value << length); +} + +template T ReadChunk(std::ifstream &file, int length = sizeof(T)) +{ + T chunk{}; + file.read(reinterpret_cast(&chunk), length); + return chunk; +} + +std::string ReadString(std::ifstream &file, int length) +{ + std::string chunk(length, '\0'); + file.read(&chunk[0], length); + return file.gcount() == length ? chunk : ""; +} + +uint32_t ReadVarLen(std::ifstream &file) +{ + uint32_t value = 0; + uint8_t byte = 0; + do + { + byte = ReadChunk(file); + value = (value << 7) | (byte & 0x7F); + } while (byte & 0x80); + return value; +} + +auto STATUS_NOTE_EVENT = 0x90; +auto STATUS_META_EVENT = 0xFF; +auto TYPE_END_OF_TRACK = 0x2F; + +std::vector ReadMidiFile(const std::string &path) +{ + std::ifstream file(path, std::ios::binary); + + if (!file.is_open()) + { + throw std::runtime_error("Cannot open MIDI file"); + } + + if (ReadString(file, 4) != "MThd") + { + throw std::runtime_error("Invalid MIDI file header"); + } + + auto headerLength = ByteSwap(ReadChunk(file)); + auto format = ByteSwap(ReadChunk(file)); + auto tracks = ByteSwap(ReadChunk(file)); + auto resolution = ByteSwap(ReadChunk(file)); + + std::vector notes; + + for (int t = 0; t < tracks; t += 1) + { + if (ReadString(file, 4) != "MTrk") + { + throw std::runtime_error("Invalid track header"); + } + + auto trackLength = ByteSwap(ReadChunk(file)); + + uint32_t absoluteTick = 0; + + while (true) + { + absoluteTick += ReadVarLen(file); + + auto status = ReadChunk(file); + + if ((status & 0xF0) == STATUS_NOTE_EVENT) + { + Note note{.Position = static_cast(absoluteTick), + .HandPosition = ReadChunk(file)}; + + auto velocity = ReadChunk(file); + + notes.push_back(note); + } + else if (status == STATUS_META_EVENT) + { + auto type = ReadChunk(file); + + auto length = ReadVarLen(file); + + file.seekg(length, std::ios::cur); + + if (type == TYPE_END_OF_TRACK) + { + break; + } + } + else if ((status & 0xF0) == 0xC0 || (status & 0xF0) == 0xD0) + { + file.seekg(1, std::ios::cur); + } + else + { + file.seekg(2, std::ios::cur); + } + } + } + + return notes; +} + +} // namespace RhythmGameUtilities diff --git a/include/RhythmGameUtilities/MidiInternal.hpp b/include/RhythmGameUtilities/MidiInternal.hpp new file mode 100644 index 0000000..72d5b8c --- /dev/null +++ b/include/RhythmGameUtilities/MidiInternal.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "Structs/Note.hpp" + +#include "Midi.hpp" + +#ifdef _WIN32 +#define PACKAGE_API __declspec(dllexport) +#else +#define PACKAGE_API +#endif + +namespace RhythmGameUtilities +{ + +extern "C" +{ + + PACKAGE_API Note *ReadMidiFileInternal(const std::string &path, + int *outSize) + { + auto internalNotes = ReadMidiFile(path); + + *outSize = internalNotes.size(); + + auto notes = (Note *)malloc(internalNotes.size() * sizeof(Note)); + + for (auto i = 0; i < internalNotes.size(); i += 1) + { + notes[i] = internalNotes[i]; + } + + return notes; + } +} + +} // namespace RhythmGameUtilities diff --git a/include/RhythmGameUtilities/RhythmGameUtilities.cpp b/include/RhythmGameUtilities/RhythmGameUtilities.cpp index ef3c4bd..9d1d6af 100644 --- a/include/RhythmGameUtilities/RhythmGameUtilities.cpp +++ b/include/RhythmGameUtilities/RhythmGameUtilities.cpp @@ -2,6 +2,8 @@ #include "Audio.hpp" #include "Common.hpp" +#include "Midi.hpp" +#include "MidiInternal.hpp" #include "Parsers.hpp" #include "ParsersInternal.hpp" #include "Utilities.hpp" diff --git a/tests/Mocks/song.mid b/tests/Mocks/song.mid new file mode 100644 index 0000000000000000000000000000000000000000..383958aa7a075f991ee2cb5c9f2ec4f3cfd3797f GIT binary patch literal 219 zcmXYrF%E)25JletS&Z6iX=Uq%5JKq%C`>3kz?zuY5IZa723*0$dXL~yxXC&LR`dS+ zH_0^Z)dLnoV#cPu-z05_Da3P@CEEq@l;-ap@xZ_S5FS#C5|&3o?g^_W;Ud{51S`9V z&CGk*Pg$m2Ga|i#FAUE2-OJH;L!neCl}e>jrBo@^O0|;nh>KWA&)hhWTjUDy5mUeY E0bdn73jhEB literal 0 HcmV?d00001 diff --git a/tests/RhythmGameUtilities/Midi.cpp b/tests/RhythmGameUtilities/Midi.cpp new file mode 100644 index 0000000..f616f24 --- /dev/null +++ b/tests/RhythmGameUtilities/Midi.cpp @@ -0,0 +1,52 @@ +#include +#include + +#include "RhythmGameUtilities/Midi.hpp" + +using namespace RhythmGameUtilities; + +void testReadMidiFile() +{ + auto notes = ReadMidiFile("./tests/Mocks/song.mid"); + + assert(size(notes) == 10); + + assert(notes[0].Position == 0); + assert(notes[0].HandPosition == 48); + + assert(notes[1].Position == 480); + assert(notes[1].HandPosition == 50); + + assert(notes[2].Position == 960); + assert(notes[2].HandPosition == 52); + + assert(notes[3].Position == 1440); + assert(notes[3].HandPosition == 54); + + assert(notes[4].Position == 1920); + assert(notes[4].HandPosition == 56); + + assert(notes[5].Position == 2400); + assert(notes[5].HandPosition == 48); + + assert(notes[6].Position == 2400); + assert(notes[6].HandPosition == 50); + + assert(notes[7].Position == 2400); + assert(notes[7].HandPosition == 52); + + assert(notes[8].Position == 2400); + assert(notes[8].HandPosition == 54); + + assert(notes[9].Position == 2400); + assert(notes[9].HandPosition == 56); + + std::cout << "."; +} + +int main() +{ + testReadMidiFile(); + + return 0; +} From c49b8355ffddd4ca7fb649972222bcaa39177356 Mon Sep 17 00:00:00 2001 From: Scott Doxey Date: Sat, 18 Jan 2025 17:43:38 -0500 Subject: [PATCH 2/4] Parse Midi from byte array as well as string. --- RhythmGameUtilities.Tests/MidiTest.cs | 43 +++++++++++ RhythmGameUtilities/Scripts/Midi.cs | 24 ++++++ UnityPackage/Editor/Tests/MidiTest.cs | 43 +++++++++++ UnityPackage/Editor/Tests/MidiTest.cs.meta | 2 + UnityPackage/Scripts/Midi.cs | 24 ++++++ UnityPackage/Scripts/Midi.cs.meta | 2 + include/RhythmGameUtilities/Midi.hpp | 79 ++++++++++++-------- include/RhythmGameUtilities/MidiInternal.hpp | 19 +++++ tests/RhythmGameUtilities/Midi.cpp | 43 +++++++++++ 9 files changed, 249 insertions(+), 30 deletions(-) create mode 100644 UnityPackage/Editor/Tests/MidiTest.cs.meta create mode 100644 UnityPackage/Scripts/Midi.cs.meta diff --git a/RhythmGameUtilities.Tests/MidiTest.cs b/RhythmGameUtilities.Tests/MidiTest.cs index e481246..d3b9f2e 100644 --- a/RhythmGameUtilities.Tests/MidiTest.cs +++ b/RhythmGameUtilities.Tests/MidiTest.cs @@ -7,6 +7,49 @@ namespace RhythmGameUtilities.Tests public class MidiTest { + [Test] + public void TestReadMidiData() + { + var directory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + var path = Path.GetFullPath(Path.Combine(directory, "../../../Mocks/song.mid")); + + var content = File.ReadAllBytes(path); + + var notes = Midi.ReadMidiData(content); + + Assert.That(notes.Count, Is.EqualTo(10)); + + Assert.That(notes[0].Position, Is.EqualTo(0)); + Assert.That(notes[0].HandPosition, Is.EqualTo(48)); + + Assert.That(notes[1].Position, Is.EqualTo(480)); + Assert.That(notes[1].HandPosition, Is.EqualTo(50)); + + Assert.That(notes[2].Position, Is.EqualTo(960)); + Assert.That(notes[2].HandPosition, Is.EqualTo(52)); + + Assert.That(notes[3].Position, Is.EqualTo(1440)); + Assert.That(notes[3].HandPosition, Is.EqualTo(54)); + + Assert.That(notes[4].Position, Is.EqualTo(1920)); + Assert.That(notes[4].HandPosition, Is.EqualTo(56)); + + Assert.That(notes[5].Position, Is.EqualTo(2400)); + Assert.That(notes[5].HandPosition, Is.EqualTo(48)); + + Assert.That(notes[6].Position, Is.EqualTo(2400)); + Assert.That(notes[6].HandPosition, Is.EqualTo(50)); + + Assert.That(notes[7].Position, Is.EqualTo(2400)); + Assert.That(notes[7].HandPosition, Is.EqualTo(52)); + + Assert.That(notes[8].Position, Is.EqualTo(2400)); + Assert.That(notes[8].HandPosition, Is.EqualTo(54)); + + Assert.That(notes[9].Position, Is.EqualTo(2400)); + Assert.That(notes[9].HandPosition, Is.EqualTo(56)); + } + [Test] public void TestReadMidiFile() { diff --git a/RhythmGameUtilities/Scripts/Midi.cs b/RhythmGameUtilities/Scripts/Midi.cs index 9c05062..98cb4bc 100644 --- a/RhythmGameUtilities/Scripts/Midi.cs +++ b/RhythmGameUtilities/Scripts/Midi.cs @@ -8,6 +8,9 @@ namespace RhythmGameUtilities internal static class MidiInternal { + [DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr ReadMidiDataInternal(byte[] bytes, int dataSize, out int size); + [DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr ReadMidiFileInternal(string filename, out int size); @@ -16,6 +19,27 @@ internal static class MidiInternal public static class Midi { + public static List ReadMidiData(byte[] bytes) + { + var notes = new List(); + + var ptrArray = MidiInternal.ReadMidiDataInternal(bytes, bytes.Length, out var size); + + var noteSize = Marshal.SizeOf(typeof(Note)); + + for (var i = 0; i < size; i += 1) + { + var noteSizePtr = new IntPtr(ptrArray.ToInt64() + noteSize * i); + var note = Marshal.PtrToStructure(noteSizePtr); + + notes.Add(note); + } + + Marshal.FreeHGlobal(ptrArray); + + return notes; + } + public static List ReadMidiFile(string path) { var notes = new List(); diff --git a/UnityPackage/Editor/Tests/MidiTest.cs b/UnityPackage/Editor/Tests/MidiTest.cs index e481246..d3b9f2e 100644 --- a/UnityPackage/Editor/Tests/MidiTest.cs +++ b/UnityPackage/Editor/Tests/MidiTest.cs @@ -7,6 +7,49 @@ namespace RhythmGameUtilities.Tests public class MidiTest { + [Test] + public void TestReadMidiData() + { + var directory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + var path = Path.GetFullPath(Path.Combine(directory, "../../../Mocks/song.mid")); + + var content = File.ReadAllBytes(path); + + var notes = Midi.ReadMidiData(content); + + Assert.That(notes.Count, Is.EqualTo(10)); + + Assert.That(notes[0].Position, Is.EqualTo(0)); + Assert.That(notes[0].HandPosition, Is.EqualTo(48)); + + Assert.That(notes[1].Position, Is.EqualTo(480)); + Assert.That(notes[1].HandPosition, Is.EqualTo(50)); + + Assert.That(notes[2].Position, Is.EqualTo(960)); + Assert.That(notes[2].HandPosition, Is.EqualTo(52)); + + Assert.That(notes[3].Position, Is.EqualTo(1440)); + Assert.That(notes[3].HandPosition, Is.EqualTo(54)); + + Assert.That(notes[4].Position, Is.EqualTo(1920)); + Assert.That(notes[4].HandPosition, Is.EqualTo(56)); + + Assert.That(notes[5].Position, Is.EqualTo(2400)); + Assert.That(notes[5].HandPosition, Is.EqualTo(48)); + + Assert.That(notes[6].Position, Is.EqualTo(2400)); + Assert.That(notes[6].HandPosition, Is.EqualTo(50)); + + Assert.That(notes[7].Position, Is.EqualTo(2400)); + Assert.That(notes[7].HandPosition, Is.EqualTo(52)); + + Assert.That(notes[8].Position, Is.EqualTo(2400)); + Assert.That(notes[8].HandPosition, Is.EqualTo(54)); + + Assert.That(notes[9].Position, Is.EqualTo(2400)); + Assert.That(notes[9].HandPosition, Is.EqualTo(56)); + } + [Test] public void TestReadMidiFile() { diff --git a/UnityPackage/Editor/Tests/MidiTest.cs.meta b/UnityPackage/Editor/Tests/MidiTest.cs.meta new file mode 100644 index 0000000..271fe58 --- /dev/null +++ b/UnityPackage/Editor/Tests/MidiTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 192d05baf03de47b484e890166b20c1b \ No newline at end of file diff --git a/UnityPackage/Scripts/Midi.cs b/UnityPackage/Scripts/Midi.cs index 9c05062..98cb4bc 100644 --- a/UnityPackage/Scripts/Midi.cs +++ b/UnityPackage/Scripts/Midi.cs @@ -8,6 +8,9 @@ namespace RhythmGameUtilities internal static class MidiInternal { + [DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr ReadMidiDataInternal(byte[] bytes, int dataSize, out int size); + [DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr ReadMidiFileInternal(string filename, out int size); @@ -16,6 +19,27 @@ internal static class MidiInternal public static class Midi { + public static List ReadMidiData(byte[] bytes) + { + var notes = new List(); + + var ptrArray = MidiInternal.ReadMidiDataInternal(bytes, bytes.Length, out var size); + + var noteSize = Marshal.SizeOf(typeof(Note)); + + for (var i = 0; i < size; i += 1) + { + var noteSizePtr = new IntPtr(ptrArray.ToInt64() + noteSize * i); + var note = Marshal.PtrToStructure(noteSizePtr); + + notes.Add(note); + } + + Marshal.FreeHGlobal(ptrArray); + + return notes; + } + public static List ReadMidiFile(string path) { var notes = new List(); diff --git a/UnityPackage/Scripts/Midi.cs.meta b/UnityPackage/Scripts/Midi.cs.meta new file mode 100644 index 0000000..092e6c7 --- /dev/null +++ b/UnityPackage/Scripts/Midi.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4532fba60d1fc4e578d72a714cfd9ac1 \ No newline at end of file diff --git a/include/RhythmGameUtilities/Midi.hpp b/include/RhythmGameUtilities/Midi.hpp index 3a10fa5..93b8f88 100644 --- a/include/RhythmGameUtilities/Midi.hpp +++ b/include/RhythmGameUtilities/Midi.hpp @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include @@ -14,27 +16,28 @@ template T ByteSwap(T value, int length = 8) return (value >> length) | (value << length); } -template T ReadChunk(std::ifstream &file, int length = sizeof(T)) +template +T ReadChunk(std::istringstream &stream, int length = sizeof(T)) { T chunk{}; - file.read(reinterpret_cast(&chunk), length); + stream.read(reinterpret_cast(&chunk), length); return chunk; } -std::string ReadString(std::ifstream &file, int length) +std::string ReadString(std::istringstream &stream, int length) { std::string chunk(length, '\0'); - file.read(&chunk[0], length); - return file.gcount() == length ? chunk : ""; + stream.read(&chunk[0], length); + return stream.gcount() == length ? chunk : ""; } -uint32_t ReadVarLen(std::ifstream &file) +uint32_t ReadVarLen(std::istringstream &stream) { uint32_t value = 0; uint8_t byte = 0; do { - byte = ReadChunk(file); + byte = ReadChunk(stream); value = (value << 7) | (byte & 0x7F); } while (byte & 0x80); return value; @@ -44,60 +47,56 @@ auto STATUS_NOTE_EVENT = 0x90; auto STATUS_META_EVENT = 0xFF; auto TYPE_END_OF_TRACK = 0x2F; -std::vector ReadMidiFile(const std::string &path) +std::vector ReadMidiData(const std::vector &data) { - std::ifstream file(path, std::ios::binary); - - if (!file.is_open()) - { - throw std::runtime_error("Cannot open MIDI file"); - } + std::istringstream stream(std::string(data.begin(), data.end()), + std::ios::binary); - if (ReadString(file, 4) != "MThd") + if (ReadString(stream, 4) != "MThd") { throw std::runtime_error("Invalid MIDI file header"); } - auto headerLength = ByteSwap(ReadChunk(file)); - auto format = ByteSwap(ReadChunk(file)); - auto tracks = ByteSwap(ReadChunk(file)); - auto resolution = ByteSwap(ReadChunk(file)); + auto headerLength = ByteSwap(ReadChunk(stream)); + auto format = ByteSwap(ReadChunk(stream)); + auto tracks = ByteSwap(ReadChunk(stream)); + auto resolution = ByteSwap(ReadChunk(stream)); std::vector notes; for (int t = 0; t < tracks; t += 1) { - if (ReadString(file, 4) != "MTrk") + if (ReadString(stream, 4) != "MTrk") { throw std::runtime_error("Invalid track header"); } - auto trackLength = ByteSwap(ReadChunk(file)); + auto trackLength = ByteSwap(ReadChunk(stream)); uint32_t absoluteTick = 0; while (true) { - absoluteTick += ReadVarLen(file); + absoluteTick += ReadVarLen(stream); - auto status = ReadChunk(file); + auto status = ReadChunk(stream); if ((status & 0xF0) == STATUS_NOTE_EVENT) { Note note{.Position = static_cast(absoluteTick), - .HandPosition = ReadChunk(file)}; + .HandPosition = ReadChunk(stream)}; - auto velocity = ReadChunk(file); + auto velocity = ReadChunk(stream); notes.push_back(note); } else if (status == STATUS_META_EVENT) { - auto type = ReadChunk(file); + auto type = ReadChunk(stream); - auto length = ReadVarLen(file); + auto length = ReadVarLen(stream); - file.seekg(length, std::ios::cur); + stream.seekg(length, std::ios::cur); if (type == TYPE_END_OF_TRACK) { @@ -106,11 +105,11 @@ std::vector ReadMidiFile(const std::string &path) } else if ((status & 0xF0) == 0xC0 || (status & 0xF0) == 0xD0) { - file.seekg(1, std::ios::cur); + stream.seekg(1, std::ios::cur); } else { - file.seekg(2, std::ios::cur); + stream.seekg(2, std::ios::cur); } } } @@ -118,4 +117,24 @@ std::vector ReadMidiFile(const std::string &path) return notes; } +std::vector ReadMidiFile(const std::string &path) +{ + std::ifstream file(path, std::ios::binary | std::ios::ate); + + if (!file.is_open()) + { + throw std::runtime_error("Cannot open MIDI file"); + } + + auto fileSize = file.tellg(); + + std::vector buffer(static_cast(fileSize)); + + file.seekg(0, std::ios::beg); + + file.read(reinterpret_cast(buffer.data()), fileSize); + + return ReadMidiData(buffer); +} + } // namespace RhythmGameUtilities diff --git a/include/RhythmGameUtilities/MidiInternal.hpp b/include/RhythmGameUtilities/MidiInternal.hpp index 72d5b8c..37b21df 100644 --- a/include/RhythmGameUtilities/MidiInternal.hpp +++ b/include/RhythmGameUtilities/MidiInternal.hpp @@ -18,6 +18,25 @@ namespace RhythmGameUtilities extern "C" { + PACKAGE_API Note *ReadMidiDataInternal(const uint8_t *data, int dataSize, + int *outSize) + { + std::vector byteVector(data, data + dataSize); + + auto internalNotes = ReadMidiData(byteVector); + + *outSize = internalNotes.size(); + + auto notes = (Note *)malloc(internalNotes.size() * sizeof(Note)); + + for (auto i = 0; i < internalNotes.size(); i += 1) + { + notes[i] = internalNotes[i]; + } + + return notes; + } + PACKAGE_API Note *ReadMidiFileInternal(const std::string &path, int *outSize) { diff --git a/tests/RhythmGameUtilities/Midi.cpp b/tests/RhythmGameUtilities/Midi.cpp index f616f24..d314d02 100644 --- a/tests/RhythmGameUtilities/Midi.cpp +++ b/tests/RhythmGameUtilities/Midi.cpp @@ -1,10 +1,52 @@ #include #include +#include "RhythmGameUtilities/File.hpp" #include "RhythmGameUtilities/Midi.hpp" using namespace RhythmGameUtilities; +void testReadMidiData() +{ + auto bytes = ReadBytesFromFile("./tests/Mocks/song.mid"); + + auto notes = ReadMidiData(bytes); + + assert(size(notes) == 10); + + assert(notes[0].Position == 0); + assert(notes[0].HandPosition == 48); + + assert(notes[1].Position == 480); + assert(notes[1].HandPosition == 50); + + assert(notes[2].Position == 960); + assert(notes[2].HandPosition == 52); + + assert(notes[3].Position == 1440); + assert(notes[3].HandPosition == 54); + + assert(notes[4].Position == 1920); + assert(notes[4].HandPosition == 56); + + assert(notes[5].Position == 2400); + assert(notes[5].HandPosition == 48); + + assert(notes[6].Position == 2400); + assert(notes[6].HandPosition == 50); + + assert(notes[7].Position == 2400); + assert(notes[7].HandPosition == 52); + + assert(notes[8].Position == 2400); + assert(notes[8].HandPosition == 54); + + assert(notes[9].Position == 2400); + assert(notes[9].HandPosition == 56); + + std::cout << "."; +} + void testReadMidiFile() { auto notes = ReadMidiFile("./tests/Mocks/song.mid"); @@ -46,6 +88,7 @@ void testReadMidiFile() int main() { + testReadMidiData(); testReadMidiFile(); return 0; From cf4c47476758a355bb7663716b2cde0b0f821935 Mon Sep 17 00:00:00 2001 From: Scott Doxey Date: Sat, 18 Jan 2025 19:57:18 -0500 Subject: [PATCH 3/4] Return array instead of list from method. --- RhythmGameUtilities.Tests/MidiTest.cs | 4 ++-- RhythmGameUtilities/Scripts/Midi.cs | 8 ++++---- UnityPackage/Editor/Tests/MidiTest.cs | 4 ++-- UnityPackage/Scripts/Midi.cs | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/RhythmGameUtilities.Tests/MidiTest.cs b/RhythmGameUtilities.Tests/MidiTest.cs index d3b9f2e..522e041 100644 --- a/RhythmGameUtilities.Tests/MidiTest.cs +++ b/RhythmGameUtilities.Tests/MidiTest.cs @@ -17,7 +17,7 @@ public void TestReadMidiData() var notes = Midi.ReadMidiData(content); - Assert.That(notes.Count, Is.EqualTo(10)); + Assert.That(notes.Length, Is.EqualTo(10)); Assert.That(notes[0].Position, Is.EqualTo(0)); Assert.That(notes[0].HandPosition, Is.EqualTo(48)); @@ -58,7 +58,7 @@ public void TestReadMidiFile() var notes = Midi.ReadMidiFile(path); - Assert.That(notes.Count, Is.EqualTo(10)); + Assert.That(notes.Length, Is.EqualTo(10)); Assert.That(notes[0].Position, Is.EqualTo(0)); Assert.That(notes[0].HandPosition, Is.EqualTo(48)); diff --git a/RhythmGameUtilities/Scripts/Midi.cs b/RhythmGameUtilities/Scripts/Midi.cs index 98cb4bc..e4641d7 100644 --- a/RhythmGameUtilities/Scripts/Midi.cs +++ b/RhythmGameUtilities/Scripts/Midi.cs @@ -19,7 +19,7 @@ internal static class MidiInternal public static class Midi { - public static List ReadMidiData(byte[] bytes) + public static Note[] ReadMidiData(byte[] bytes) { var notes = new List(); @@ -37,10 +37,10 @@ public static List ReadMidiData(byte[] bytes) Marshal.FreeHGlobal(ptrArray); - return notes; + return notes.ToArray(); } - public static List ReadMidiFile(string path) + public static Note[] ReadMidiFile(string path) { var notes = new List(); @@ -58,7 +58,7 @@ public static List ReadMidiFile(string path) Marshal.FreeHGlobal(ptrArray); - return notes; + return notes.ToArray(); } } diff --git a/UnityPackage/Editor/Tests/MidiTest.cs b/UnityPackage/Editor/Tests/MidiTest.cs index d3b9f2e..522e041 100644 --- a/UnityPackage/Editor/Tests/MidiTest.cs +++ b/UnityPackage/Editor/Tests/MidiTest.cs @@ -17,7 +17,7 @@ public void TestReadMidiData() var notes = Midi.ReadMidiData(content); - Assert.That(notes.Count, Is.EqualTo(10)); + Assert.That(notes.Length, Is.EqualTo(10)); Assert.That(notes[0].Position, Is.EqualTo(0)); Assert.That(notes[0].HandPosition, Is.EqualTo(48)); @@ -58,7 +58,7 @@ public void TestReadMidiFile() var notes = Midi.ReadMidiFile(path); - Assert.That(notes.Count, Is.EqualTo(10)); + Assert.That(notes.Length, Is.EqualTo(10)); Assert.That(notes[0].Position, Is.EqualTo(0)); Assert.That(notes[0].HandPosition, Is.EqualTo(48)); diff --git a/UnityPackage/Scripts/Midi.cs b/UnityPackage/Scripts/Midi.cs index 98cb4bc..e4641d7 100644 --- a/UnityPackage/Scripts/Midi.cs +++ b/UnityPackage/Scripts/Midi.cs @@ -19,7 +19,7 @@ internal static class MidiInternal public static class Midi { - public static List ReadMidiData(byte[] bytes) + public static Note[] ReadMidiData(byte[] bytes) { var notes = new List(); @@ -37,10 +37,10 @@ public static List ReadMidiData(byte[] bytes) Marshal.FreeHGlobal(ptrArray); - return notes; + return notes.ToArray(); } - public static List ReadMidiFile(string path) + public static Note[] ReadMidiFile(string path) { var notes = new List(); @@ -58,7 +58,7 @@ public static List ReadMidiFile(string path) Marshal.FreeHGlobal(ptrArray); - return notes; + return notes.ToArray(); } } From 98f862dd5386955c579a26b6957be80ab46a2aeb Mon Sep 17 00:00:00 2001 From: Scott Doxey Date: Wed, 11 Jun 2025 01:38:37 -0400 Subject: [PATCH 4/4] Cleaned up midi parsing. Syntax updates. --- include/RhythmGameUtilities/Midi.hpp | 36 ++++++++++++-------- include/RhythmGameUtilities/MidiInternal.hpp | 14 ++++---- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/include/RhythmGameUtilities/Midi.hpp b/include/RhythmGameUtilities/Midi.hpp index 93b8f88..0c90034 100644 --- a/include/RhythmGameUtilities/Midi.hpp +++ b/include/RhythmGameUtilities/Midi.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -11,27 +13,27 @@ namespace RhythmGameUtilities { -template T ByteSwap(T value, int length = 8) +template inline auto ByteSwap(T value, int length = 8) -> T { return (value >> length) | (value << length); } template -T ReadChunk(std::istringstream &stream, int length = sizeof(T)) +inline auto ReadChunk(std::istringstream &stream, int length = sizeof(T)) -> T { T chunk{}; stream.read(reinterpret_cast(&chunk), length); return chunk; } -std::string ReadString(std::istringstream &stream, int length) +inline auto ReadString(std::istringstream &stream, int length) -> std::string { std::string chunk(length, '\0'); - stream.read(&chunk[0], length); + stream.read(chunk.data(), length); return stream.gcount() == length ? chunk : ""; } -uint32_t ReadVarLen(std::istringstream &stream) +inline auto ReadVarLen(std::istringstream &stream) -> uint32_t { uint32_t value = 0; uint8_t byte = 0; @@ -39,15 +41,20 @@ uint32_t ReadVarLen(std::istringstream &stream) { byte = ReadChunk(stream); value = (value << 7) | (byte & 0x7F); - } while (byte & 0x80); + } while ((byte & 0x80) > 0); return value; } -auto STATUS_NOTE_EVENT = 0x90; -auto STATUS_META_EVENT = 0xFF; -auto TYPE_END_OF_TRACK = 0x2F; +const auto TYPE_END_OF_TRACK = 0x2F; + +const auto SYSTEM_COMMAND = 0xF0; +const auto NOTE_OFF_COMMAND = 0x80; +const auto NOTE_ON_COMMAND = 0x90; +const auto CONTROL_CHANGE_COMMAND = 0xB0; +const auto PROGRAM_CHANGE_COMMAND = 0xC0; +const auto CHANNEL_PRESSURE_COMMAND = 0xD0; -std::vector ReadMidiData(const std::vector &data) +inline auto ReadMidiData(const std::vector &data) -> std::vector { std::istringstream stream(std::string(data.begin(), data.end()), std::ios::binary); @@ -81,7 +88,7 @@ std::vector ReadMidiData(const std::vector &data) auto status = ReadChunk(stream); - if ((status & 0xF0) == STATUS_NOTE_EVENT) + if ((status & SYSTEM_COMMAND) == NOTE_ON_COMMAND) { Note note{.Position = static_cast(absoluteTick), .HandPosition = ReadChunk(stream)}; @@ -90,7 +97,7 @@ std::vector ReadMidiData(const std::vector &data) notes.push_back(note); } - else if (status == STATUS_META_EVENT) + else if ((status & SYSTEM_COMMAND) == SYSTEM_COMMAND) { auto type = ReadChunk(stream); @@ -103,7 +110,8 @@ std::vector ReadMidiData(const std::vector &data) break; } } - else if ((status & 0xF0) == 0xC0 || (status & 0xF0) == 0xD0) + else if ((status & SYSTEM_COMMAND) == PROGRAM_CHANGE_COMMAND || + (status & SYSTEM_COMMAND) == CHANNEL_PRESSURE_COMMAND) { stream.seekg(1, std::ios::cur); } @@ -117,7 +125,7 @@ std::vector ReadMidiData(const std::vector &data) return notes; } -std::vector ReadMidiFile(const std::string &path) +auto ReadMidiFile(const std::string &path) -> std::vector { std::ifstream file(path, std::ios::binary | std::ios::ate); diff --git a/include/RhythmGameUtilities/MidiInternal.hpp b/include/RhythmGameUtilities/MidiInternal.hpp index 37b21df..1b44524 100644 --- a/include/RhythmGameUtilities/MidiInternal.hpp +++ b/include/RhythmGameUtilities/MidiInternal.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include "Structs/Note.hpp" #include "Midi.hpp" @@ -18,8 +16,8 @@ namespace RhythmGameUtilities extern "C" { - PACKAGE_API Note *ReadMidiDataInternal(const uint8_t *data, int dataSize, - int *outSize) + PACKAGE_API auto ReadMidiDataInternal(const uint8_t *data, int dataSize, + int *outSize) -> Note * { std::vector byteVector(data, data + dataSize); @@ -27,7 +25,7 @@ extern "C" *outSize = internalNotes.size(); - auto notes = (Note *)malloc(internalNotes.size() * sizeof(Note)); + auto *notes = (Note *)malloc(internalNotes.size() * sizeof(Note)); for (auto i = 0; i < internalNotes.size(); i += 1) { @@ -37,14 +35,14 @@ extern "C" return notes; } - PACKAGE_API Note *ReadMidiFileInternal(const std::string &path, - int *outSize) + PACKAGE_API auto ReadMidiFileInternal(const std::string &path, int *outSize) + -> Note * { auto internalNotes = ReadMidiFile(path); *outSize = internalNotes.size(); - auto notes = (Note *)malloc(internalNotes.size() * sizeof(Note)); + auto *notes = (Note *)malloc(internalNotes.size() * sizeof(Note)); for (auto i = 0; i < internalNotes.size(); i += 1) {