Skip to content

Commit 26bab69

Browse files
committed
Parse Midi from byte array as well as string.
1 parent 805073d commit 26bab69

File tree

9 files changed

+249
-30
lines changed

9 files changed

+249
-30
lines changed

RhythmGameUtilities.Tests/MidiTest.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,49 @@ namespace RhythmGameUtilities.Tests
77
public class MidiTest
88
{
99

10+
[Test]
11+
public void TestReadMidiData()
12+
{
13+
var directory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
14+
var path = Path.GetFullPath(Path.Combine(directory, "../../../Mocks/song.mid"));
15+
16+
var content = File.ReadAllBytes(path);
17+
18+
var notes = Midi.ReadMidiData(content);
19+
20+
Assert.That(notes.Count, Is.EqualTo(10));
21+
22+
Assert.That(notes[0].Position, Is.EqualTo(0));
23+
Assert.That(notes[0].HandPosition, Is.EqualTo(48));
24+
25+
Assert.That(notes[1].Position, Is.EqualTo(480));
26+
Assert.That(notes[1].HandPosition, Is.EqualTo(50));
27+
28+
Assert.That(notes[2].Position, Is.EqualTo(960));
29+
Assert.That(notes[2].HandPosition, Is.EqualTo(52));
30+
31+
Assert.That(notes[3].Position, Is.EqualTo(1440));
32+
Assert.That(notes[3].HandPosition, Is.EqualTo(54));
33+
34+
Assert.That(notes[4].Position, Is.EqualTo(1920));
35+
Assert.That(notes[4].HandPosition, Is.EqualTo(56));
36+
37+
Assert.That(notes[5].Position, Is.EqualTo(2400));
38+
Assert.That(notes[5].HandPosition, Is.EqualTo(48));
39+
40+
Assert.That(notes[6].Position, Is.EqualTo(2400));
41+
Assert.That(notes[6].HandPosition, Is.EqualTo(50));
42+
43+
Assert.That(notes[7].Position, Is.EqualTo(2400));
44+
Assert.That(notes[7].HandPosition, Is.EqualTo(52));
45+
46+
Assert.That(notes[8].Position, Is.EqualTo(2400));
47+
Assert.That(notes[8].HandPosition, Is.EqualTo(54));
48+
49+
Assert.That(notes[9].Position, Is.EqualTo(2400));
50+
Assert.That(notes[9].HandPosition, Is.EqualTo(56));
51+
}
52+
1053
[Test]
1154
public void TestReadMidiFile()
1255
{

RhythmGameUtilities/Scripts/Midi.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ namespace RhythmGameUtilities
88
internal static class MidiInternal
99
{
1010

11+
[DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)]
12+
public static extern IntPtr ReadMidiDataInternal(byte[] bytes, int dataSize, out int size);
13+
1114
[DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)]
1215
public static extern IntPtr ReadMidiFileInternal(string filename, out int size);
1316

@@ -16,6 +19,27 @@ internal static class MidiInternal
1619
public static class Midi
1720
{
1821

22+
public static List<Note> ReadMidiData(byte[] bytes)
23+
{
24+
var notes = new List<Note>();
25+
26+
var ptrArray = MidiInternal.ReadMidiDataInternal(bytes, bytes.Length, out var size);
27+
28+
var noteSize = Marshal.SizeOf(typeof(Note));
29+
30+
for (var i = 0; i < size; i += 1)
31+
{
32+
var noteSizePtr = new IntPtr(ptrArray.ToInt64() + noteSize * i);
33+
var note = Marshal.PtrToStructure<Note>(noteSizePtr);
34+
35+
notes.Add(note);
36+
}
37+
38+
Marshal.FreeHGlobal(ptrArray);
39+
40+
return notes;
41+
}
42+
1943
public static List<Note> ReadMidiFile(string path)
2044
{
2145
var notes = new List<Note>();

UnityPackage/Editor/Tests/MidiTest.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,49 @@ namespace RhythmGameUtilities.Tests
77
public class MidiTest
88
{
99

10+
[Test]
11+
public void TestReadMidiData()
12+
{
13+
var directory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
14+
var path = Path.GetFullPath(Path.Combine(directory, "../../../Mocks/song.mid"));
15+
16+
var content = File.ReadAllBytes(path);
17+
18+
var notes = Midi.ReadMidiData(content);
19+
20+
Assert.That(notes.Count, Is.EqualTo(10));
21+
22+
Assert.That(notes[0].Position, Is.EqualTo(0));
23+
Assert.That(notes[0].HandPosition, Is.EqualTo(48));
24+
25+
Assert.That(notes[1].Position, Is.EqualTo(480));
26+
Assert.That(notes[1].HandPosition, Is.EqualTo(50));
27+
28+
Assert.That(notes[2].Position, Is.EqualTo(960));
29+
Assert.That(notes[2].HandPosition, Is.EqualTo(52));
30+
31+
Assert.That(notes[3].Position, Is.EqualTo(1440));
32+
Assert.That(notes[3].HandPosition, Is.EqualTo(54));
33+
34+
Assert.That(notes[4].Position, Is.EqualTo(1920));
35+
Assert.That(notes[4].HandPosition, Is.EqualTo(56));
36+
37+
Assert.That(notes[5].Position, Is.EqualTo(2400));
38+
Assert.That(notes[5].HandPosition, Is.EqualTo(48));
39+
40+
Assert.That(notes[6].Position, Is.EqualTo(2400));
41+
Assert.That(notes[6].HandPosition, Is.EqualTo(50));
42+
43+
Assert.That(notes[7].Position, Is.EqualTo(2400));
44+
Assert.That(notes[7].HandPosition, Is.EqualTo(52));
45+
46+
Assert.That(notes[8].Position, Is.EqualTo(2400));
47+
Assert.That(notes[8].HandPosition, Is.EqualTo(54));
48+
49+
Assert.That(notes[9].Position, Is.EqualTo(2400));
50+
Assert.That(notes[9].HandPosition, Is.EqualTo(56));
51+
}
52+
1053
[Test]
1154
public void TestReadMidiFile()
1255
{

UnityPackage/Editor/Tests/MidiTest.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

UnityPackage/Scripts/Midi.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ namespace RhythmGameUtilities
88
internal static class MidiInternal
99
{
1010

11+
[DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)]
12+
public static extern IntPtr ReadMidiDataInternal(byte[] bytes, int dataSize, out int size);
13+
1114
[DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)]
1215
public static extern IntPtr ReadMidiFileInternal(string filename, out int size);
1316

@@ -16,6 +19,27 @@ internal static class MidiInternal
1619
public static class Midi
1720
{
1821

22+
public static List<Note> ReadMidiData(byte[] bytes)
23+
{
24+
var notes = new List<Note>();
25+
26+
var ptrArray = MidiInternal.ReadMidiDataInternal(bytes, bytes.Length, out var size);
27+
28+
var noteSize = Marshal.SizeOf(typeof(Note));
29+
30+
for (var i = 0; i < size; i += 1)
31+
{
32+
var noteSizePtr = new IntPtr(ptrArray.ToInt64() + noteSize * i);
33+
var note = Marshal.PtrToStructure<Note>(noteSizePtr);
34+
35+
notes.Add(note);
36+
}
37+
38+
Marshal.FreeHGlobal(ptrArray);
39+
40+
return notes;
41+
}
42+
1943
public static List<Note> ReadMidiFile(string path)
2044
{
2145
var notes = new List<Note>();

UnityPackage/Scripts/Midi.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#pragma once
22

3+
#include <cstdint>
34
#include <fstream>
5+
#include <sstream>
46
#include <string>
57
#include <vector>
68

@@ -14,27 +16,28 @@ template <typename T> T ByteSwap(T value, int length = 8)
1416
return (value >> length) | (value << length);
1517
}
1618

17-
template <typename T> T ReadChunk(std::ifstream &file, int length = sizeof(T))
19+
template <typename T>
20+
T ReadChunk(std::istringstream &stream, int length = sizeof(T))
1821
{
1922
T chunk{};
20-
file.read(reinterpret_cast<char *>(&chunk), length);
23+
stream.read(reinterpret_cast<char *>(&chunk), length);
2124
return chunk;
2225
}
2326

24-
std::string ReadString(std::ifstream &file, int length)
27+
std::string ReadString(std::istringstream &stream, int length)
2528
{
2629
std::string chunk(length, '\0');
27-
file.read(&chunk[0], length);
28-
return file.gcount() == length ? chunk : "";
30+
stream.read(&chunk[0], length);
31+
return stream.gcount() == length ? chunk : "";
2932
}
3033

31-
uint32_t ReadVarLen(std::ifstream &file)
34+
uint32_t ReadVarLen(std::istringstream &stream)
3235
{
3336
uint32_t value = 0;
3437
uint8_t byte = 0;
3538
do
3639
{
37-
byte = ReadChunk<uint8_t>(file);
40+
byte = ReadChunk<uint8_t>(stream);
3841
value = (value << 7) | (byte & 0x7F);
3942
} while (byte & 0x80);
4043
return value;
@@ -44,60 +47,56 @@ auto STATUS_NOTE_EVENT = 0x90;
4447
auto STATUS_META_EVENT = 0xFF;
4548
auto TYPE_END_OF_TRACK = 0x2F;
4649

47-
std::vector<Note> ReadMidiFile(const std::string &path)
50+
std::vector<Note> ReadMidiData(const std::vector<uint8_t> &data)
4851
{
49-
std::ifstream file(path, std::ios::binary);
50-
51-
if (!file.is_open())
52-
{
53-
throw std::runtime_error("Cannot open MIDI file");
54-
}
52+
std::istringstream stream(std::string(data.begin(), data.end()),
53+
std::ios::binary);
5554

56-
if (ReadString(file, 4) != "MThd")
55+
if (ReadString(stream, 4) != "MThd")
5756
{
5857
throw std::runtime_error("Invalid MIDI file header");
5958
}
6059

61-
auto headerLength = ByteSwap(ReadChunk<uint32_t>(file));
62-
auto format = ByteSwap(ReadChunk<uint16_t>(file));
63-
auto tracks = ByteSwap(ReadChunk<uint16_t>(file));
64-
auto resolution = ByteSwap(ReadChunk<uint16_t>(file));
60+
auto headerLength = ByteSwap(ReadChunk<uint32_t>(stream));
61+
auto format = ByteSwap(ReadChunk<uint16_t>(stream));
62+
auto tracks = ByteSwap(ReadChunk<uint16_t>(stream));
63+
auto resolution = ByteSwap(ReadChunk<uint16_t>(stream));
6564

6665
std::vector<Note> notes;
6766

6867
for (int t = 0; t < tracks; t += 1)
6968
{
70-
if (ReadString(file, 4) != "MTrk")
69+
if (ReadString(stream, 4) != "MTrk")
7170
{
7271
throw std::runtime_error("Invalid track header");
7372
}
7473

75-
auto trackLength = ByteSwap(ReadChunk<uint32_t>(file));
74+
auto trackLength = ByteSwap(ReadChunk<uint32_t>(stream));
7675

7776
uint32_t absoluteTick = 0;
7877

7978
while (true)
8079
{
81-
absoluteTick += ReadVarLen(file);
80+
absoluteTick += ReadVarLen(stream);
8281

83-
auto status = ReadChunk<uint8_t>(file);
82+
auto status = ReadChunk<uint8_t>(stream);
8483

8584
if ((status & 0xF0) == STATUS_NOTE_EVENT)
8685
{
8786
Note note{.Position = static_cast<int>(absoluteTick),
88-
.HandPosition = ReadChunk<uint8_t>(file)};
87+
.HandPosition = ReadChunk<uint8_t>(stream)};
8988

90-
auto velocity = ReadChunk<uint8_t>(file);
89+
auto velocity = ReadChunk<uint8_t>(stream);
9190

9291
notes.push_back(note);
9392
}
9493
else if (status == STATUS_META_EVENT)
9594
{
96-
auto type = ReadChunk<uint8_t>(file);
95+
auto type = ReadChunk<uint8_t>(stream);
9796

98-
auto length = ReadVarLen(file);
97+
auto length = ReadVarLen(stream);
9998

100-
file.seekg(length, std::ios::cur);
99+
stream.seekg(length, std::ios::cur);
101100

102101
if (type == TYPE_END_OF_TRACK)
103102
{
@@ -106,16 +105,36 @@ std::vector<Note> ReadMidiFile(const std::string &path)
106105
}
107106
else if ((status & 0xF0) == 0xC0 || (status & 0xF0) == 0xD0)
108107
{
109-
file.seekg(1, std::ios::cur);
108+
stream.seekg(1, std::ios::cur);
110109
}
111110
else
112111
{
113-
file.seekg(2, std::ios::cur);
112+
stream.seekg(2, std::ios::cur);
114113
}
115114
}
116115
}
117116

118117
return notes;
119118
}
120119

120+
std::vector<Note> ReadMidiFile(const std::string &path)
121+
{
122+
std::ifstream file(path, std::ios::binary | std::ios::ate);
123+
124+
if (!file.is_open())
125+
{
126+
throw std::runtime_error("Cannot open MIDI file");
127+
}
128+
129+
auto fileSize = file.tellg();
130+
131+
std::vector<uint8_t> buffer(static_cast<size_t>(fileSize));
132+
133+
file.seekg(0, std::ios::beg);
134+
135+
file.read(reinterpret_cast<char *>(buffer.data()), fileSize);
136+
137+
return ReadMidiData(buffer);
138+
}
139+
121140
} // namespace RhythmGameUtilities

include/RhythmGameUtilities/MidiInternal.hpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,25 @@ namespace RhythmGameUtilities
1818
extern "C"
1919
{
2020

21+
PACKAGE_API Note *ReadMidiDataInternal(const uint8_t *data, int dataSize,
22+
int *outSize)
23+
{
24+
std::vector<uint8_t> byteVector(data, data + dataSize);
25+
26+
auto internalNotes = ReadMidiData(byteVector);
27+
28+
*outSize = internalNotes.size();
29+
30+
auto notes = (Note *)malloc(internalNotes.size() * sizeof(Note));
31+
32+
for (auto i = 0; i < internalNotes.size(); i += 1)
33+
{
34+
notes[i] = internalNotes[i];
35+
}
36+
37+
return notes;
38+
}
39+
2140
PACKAGE_API Note *ReadMidiFileInternal(const std::string &path,
2241
int *outSize)
2342
{

0 commit comments

Comments
 (0)