Skip to content

Commit b0f2114

Browse files
committed
Added ReadMidiFile method.
1 parent 4205faf commit b0f2114

File tree

10 files changed

+404
-0
lines changed

10 files changed

+404
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.IO;
2+
using NUnit.Framework;
3+
4+
namespace RhythmGameUtilities.Tests
5+
{
6+
7+
public class MidiTest
8+
{
9+
10+
[Test]
11+
public void TestReadMidiFile()
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 notes = Midi.ReadMidiFile(path);
17+
18+
Assert.That(notes.Count, Is.EqualTo(10));
19+
20+
Assert.That(notes[0].Position, Is.EqualTo(0));
21+
Assert.That(notes[0].HandPosition, Is.EqualTo(48));
22+
23+
Assert.That(notes[1].Position, Is.EqualTo(480));
24+
Assert.That(notes[1].HandPosition, Is.EqualTo(50));
25+
26+
Assert.That(notes[2].Position, Is.EqualTo(960));
27+
Assert.That(notes[2].HandPosition, Is.EqualTo(52));
28+
29+
Assert.That(notes[3].Position, Is.EqualTo(1440));
30+
Assert.That(notes[3].HandPosition, Is.EqualTo(54));
31+
32+
Assert.That(notes[4].Position, Is.EqualTo(1920));
33+
Assert.That(notes[4].HandPosition, Is.EqualTo(56));
34+
35+
Assert.That(notes[5].Position, Is.EqualTo(2400));
36+
Assert.That(notes[5].HandPosition, Is.EqualTo(48));
37+
38+
Assert.That(notes[6].Position, Is.EqualTo(2400));
39+
Assert.That(notes[6].HandPosition, Is.EqualTo(50));
40+
41+
Assert.That(notes[7].Position, Is.EqualTo(2400));
42+
Assert.That(notes[7].HandPosition, Is.EqualTo(52));
43+
44+
Assert.That(notes[8].Position, Is.EqualTo(2400));
45+
Assert.That(notes[8].HandPosition, Is.EqualTo(54));
46+
47+
Assert.That(notes[9].Position, Is.EqualTo(2400));
48+
Assert.That(notes[9].HandPosition, Is.EqualTo(56));
49+
}
50+
51+
}
52+
53+
}
219 Bytes
Binary file not shown.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Runtime.InteropServices;
4+
5+
namespace RhythmGameUtilities
6+
{
7+
8+
internal static class MidiInternal
9+
{
10+
11+
[DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)]
12+
public static extern IntPtr ReadMidiFileInternal(string filename, out int size);
13+
14+
}
15+
16+
public static class Midi
17+
{
18+
19+
public static List<Note> ReadMidiFile(string path)
20+
{
21+
var notes = new List<Note>();
22+
23+
var ptrArray = MidiInternal.ReadMidiFileInternal(path, out var size);
24+
25+
var noteSize = Marshal.SizeOf(typeof(Note));
26+
27+
for (var i = 0; i < size; i += 1)
28+
{
29+
var noteSizePtr = new IntPtr(ptrArray.ToInt64() + noteSize * i);
30+
var note = Marshal.PtrToStructure<Note>(noteSizePtr);
31+
32+
notes.Add(note);
33+
}
34+
35+
Marshal.FreeHGlobal(ptrArray);
36+
37+
return notes;
38+
}
39+
40+
}
41+
42+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.IO;
2+
using NUnit.Framework;
3+
4+
namespace RhythmGameUtilities.Tests
5+
{
6+
7+
public class MidiTest
8+
{
9+
10+
[Test]
11+
public void TestReadMidiFile()
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 notes = Midi.ReadMidiFile(path);
17+
18+
Assert.That(notes.Count, Is.EqualTo(10));
19+
20+
Assert.That(notes[0].Position, Is.EqualTo(0));
21+
Assert.That(notes[0].HandPosition, Is.EqualTo(48));
22+
23+
Assert.That(notes[1].Position, Is.EqualTo(480));
24+
Assert.That(notes[1].HandPosition, Is.EqualTo(50));
25+
26+
Assert.That(notes[2].Position, Is.EqualTo(960));
27+
Assert.That(notes[2].HandPosition, Is.EqualTo(52));
28+
29+
Assert.That(notes[3].Position, Is.EqualTo(1440));
30+
Assert.That(notes[3].HandPosition, Is.EqualTo(54));
31+
32+
Assert.That(notes[4].Position, Is.EqualTo(1920));
33+
Assert.That(notes[4].HandPosition, Is.EqualTo(56));
34+
35+
Assert.That(notes[5].Position, Is.EqualTo(2400));
36+
Assert.That(notes[5].HandPosition, Is.EqualTo(48));
37+
38+
Assert.That(notes[6].Position, Is.EqualTo(2400));
39+
Assert.That(notes[6].HandPosition, Is.EqualTo(50));
40+
41+
Assert.That(notes[7].Position, Is.EqualTo(2400));
42+
Assert.That(notes[7].HandPosition, Is.EqualTo(52));
43+
44+
Assert.That(notes[8].Position, Is.EqualTo(2400));
45+
Assert.That(notes[8].HandPosition, Is.EqualTo(54));
46+
47+
Assert.That(notes[9].Position, Is.EqualTo(2400));
48+
Assert.That(notes[9].HandPosition, Is.EqualTo(56));
49+
}
50+
51+
}
52+
53+
}

UnityPackage/Scripts/Midi.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Runtime.InteropServices;
4+
5+
namespace RhythmGameUtilities
6+
{
7+
8+
internal static class MidiInternal
9+
{
10+
11+
[DllImport("libRhythmGameUtilities", CallingConvention = CallingConvention.Cdecl)]
12+
public static extern IntPtr ReadMidiFileInternal(string filename, out int size);
13+
14+
}
15+
16+
public static class Midi
17+
{
18+
19+
public static List<Note> ReadMidiFile(string path)
20+
{
21+
var notes = new List<Note>();
22+
23+
var ptrArray = MidiInternal.ReadMidiFileInternal(path, out var size);
24+
25+
var noteSize = Marshal.SizeOf(typeof(Note));
26+
27+
for (var i = 0; i < size; i += 1)
28+
{
29+
var noteSizePtr = new IntPtr(ptrArray.ToInt64() + noteSize * i);
30+
var note = Marshal.PtrToStructure<Note>(noteSizePtr);
31+
32+
notes.Add(note);
33+
}
34+
35+
Marshal.FreeHGlobal(ptrArray);
36+
37+
return notes;
38+
}
39+
40+
}
41+
42+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#pragma once
2+
3+
#include <fstream>
4+
#include <string>
5+
#include <vector>
6+
7+
#include "Structs/Note.h"
8+
9+
namespace RhythmGameUtilities
10+
{
11+
12+
template <typename T> T ByteSwap(T value, int length = 8)
13+
{
14+
return (value >> length) | (value << length);
15+
}
16+
17+
template <typename T> T ReadChunk(std::ifstream &file, int length = sizeof(T))
18+
{
19+
T chunk{};
20+
file.read(reinterpret_cast<char *>(&chunk), length);
21+
return chunk;
22+
}
23+
24+
std::string ReadString(std::ifstream &file, int length)
25+
{
26+
std::string chunk(length, '\0');
27+
file.read(&chunk[0], length);
28+
return file.gcount() == length ? chunk : "";
29+
}
30+
31+
uint32_t ReadVarLen(std::ifstream &file)
32+
{
33+
uint32_t value = 0;
34+
uint8_t byte = 0;
35+
do
36+
{
37+
byte = ReadChunk<uint8_t>(file);
38+
value = (value << 7) | (byte & 0x7F);
39+
} while (byte & 0x80);
40+
return value;
41+
}
42+
43+
auto STATUS_NOTE_EVENT = 0x90;
44+
auto STATUS_META_EVENT = 0xFF;
45+
auto TYPE_END_OF_TRACK = 0x2F;
46+
47+
std::vector<Note> ReadMidiFile(const std::string &path)
48+
{
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+
}
55+
56+
if (ReadString(file, 4) != "MThd")
57+
{
58+
throw std::runtime_error("Invalid MIDI file header");
59+
}
60+
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));
65+
66+
std::vector<Note> notes;
67+
68+
for (int t = 0; t < tracks; t += 1)
69+
{
70+
if (ReadString(file, 4) != "MTrk")
71+
{
72+
throw std::runtime_error("Invalid track header");
73+
}
74+
75+
auto trackLength = ByteSwap(ReadChunk<uint32_t>(file));
76+
77+
uint32_t absoluteTick = 0;
78+
79+
while (true)
80+
{
81+
absoluteTick += ReadVarLen(file);
82+
83+
auto status = ReadChunk<uint8_t>(file);
84+
85+
if ((status & 0xF0) == STATUS_NOTE_EVENT)
86+
{
87+
Note note{.Position = static_cast<int>(absoluteTick),
88+
.HandPosition = ReadChunk<uint8_t>(file)};
89+
90+
auto velocity = ReadChunk<uint8_t>(file);
91+
92+
notes.push_back(note);
93+
}
94+
else if (status == STATUS_META_EVENT)
95+
{
96+
auto type = ReadChunk<uint8_t>(file);
97+
98+
auto length = ReadVarLen(file);
99+
100+
file.seekg(length, std::ios::cur);
101+
102+
if (type == TYPE_END_OF_TRACK)
103+
{
104+
break;
105+
}
106+
}
107+
else if ((status & 0xF0) == 0xC0 || (status & 0xF0) == 0xD0)
108+
{
109+
file.seekg(1, std::ios::cur);
110+
}
111+
else
112+
{
113+
file.seekg(2, std::ios::cur);
114+
}
115+
}
116+
}
117+
118+
return notes;
119+
}
120+
121+
} // namespace RhythmGameUtilities
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#pragma once
2+
3+
#include <map>
4+
5+
#include "Structs/Note.h"
6+
7+
#include "Midi.hpp"
8+
9+
#ifdef _WIN32
10+
#define PACKAGE_API __declspec(dllexport)
11+
#else
12+
#define PACKAGE_API
13+
#endif
14+
15+
namespace RhythmGameUtilities
16+
{
17+
18+
extern "C"
19+
{
20+
21+
PACKAGE_API Note *ReadMidiFileInternal(const std::string &path,
22+
int *outSize)
23+
{
24+
auto internalNotes = ReadMidiFile(path);
25+
26+
*outSize = internalNotes.size();
27+
28+
auto notes = (Note *)malloc(internalNotes.size() * sizeof(Note));
29+
30+
for (auto i = 0; i < internalNotes.size(); i += 1)
31+
{
32+
notes[i] = internalNotes[i];
33+
}
34+
35+
return notes;
36+
}
37+
}
38+
39+
} // namespace RhythmGameUtilities

include/RhythmGameUtilities/RhythmGameUtilities.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
#include "Audio.hpp"
44
#include "Common.hpp"
5+
#include "Midi.hpp"
6+
#include "MidiInternal.hpp"
57
#include "Parsers.hpp"
68
#include "ParsersInternal.hpp"
79
#include "Utilities.hpp"

tests/Mocks/song.mid

219 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)