Skip to content

Commit c73cf7c

Browse files
committed
Parse hold notes from MIDI
1 parent eb2b3b4 commit c73cf7c

File tree

9 files changed

+159
-72
lines changed

9 files changed

+159
-72
lines changed
25 Bytes
Binary file not shown.

Assets/src/MidiParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ ref byte data2
8080
switch (metaEventType) {
8181
case (byte)MetaEventType.Tempo:
8282
var mspqn = (data[position + 1] << 16) | (data[position + 2] << 8) | data[position + 3];
83-
data1 = (byte)(60000000.0 / mspqn);
83+
data1 = (byte)Math.Round(60000000.0 / mspqn);
8484
position += 4;
8585
return true;
8686

Assets/src/RhythmGame.cs

Lines changed: 1 addition & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,8 @@
11
using System.Collections.Generic;
22
using System.Linq;
3-
using MidiParser;
43
using TMPro;
54
using UnityEngine;
65

7-
public enum RhythmGameNoteType {
8-
Left = 0,
9-
Down = 1,
10-
Up = 2,
11-
Right = 3,
12-
};
13-
14-
[System.Serializable]
15-
public class RhythmGameNote {
16-
public RhythmGameNoteType Type;
17-
public float Time;
18-
public float Duration = 0f;
19-
20-
public bool IsHold => Duration > 0f;
21-
public bool IsInstant => !IsHold;
22-
23-
public float RelativeTime(float currentTime, bool end = false) =>
24-
Time - currentTime + (end ? Duration : 0f);
25-
}
26-
27-
[System.Serializable]
28-
public class RhythmGameSong {
29-
public float BeatsPerMinute;
30-
public List<RhythmGameNote> Notes;
31-
32-
private static readonly Dictionary<int, RhythmGameNoteType> PITCH_TO_NOTE_TYPE = new()
33-
{
34-
{ 65, RhythmGameNoteType.Left },
35-
{ 69, RhythmGameNoteType.Down },
36-
{ 72, RhythmGameNoteType.Up },
37-
{ 76, RhythmGameNoteType.Right },
38-
};
39-
40-
public static RhythmGameSong ParseMidi(string path) {
41-
MidiFile midi = new MidiFile(path);
42-
43-
if (midi.Tracks.Length != 1)
44-
throw new System.Exception("MIDI file should have exactly one track");
45-
46-
MidiTrack track = midi.Tracks[0];
47-
48-
List<RhythmGameNote> notes = new();
49-
float bpm = 120f;
50-
51-
foreach (MidiEvent midiEvent in track.MidiEvents) {
52-
switch (midiEvent.MidiEventType) {
53-
case MidiEventType.MetaEvent:
54-
if (midiEvent.MetaEventType == MetaEventType.Tempo) {
55-
// TODO: Track a list of tempo change events on the song object
56-
bpm = (int)midiEvent.Arg2;
57-
}
58-
break;
59-
60-
case MidiEventType.NoteOn:
61-
RhythmGameNoteType type = PITCH_TO_NOTE_TYPE[midiEvent.Note];
62-
float beat = (float)midiEvent.Time / midi.TicksPerQuarterNote;
63-
float time = beat * 60f / bpm;
64-
notes.Add(new() { Type = type, Time = time });
65-
break;
66-
67-
// TODO: Parse hold notes
68-
}
69-
}
70-
71-
return new RhythmGameSong { BeatsPerMinute = bpm, Notes = notes };
72-
}
73-
}
74-
756
public class RhythmGame : MonoBehaviour {
767
public RhythmGameNotesView NotesView;
778
public TMP_Text ScoreText;
@@ -106,7 +37,7 @@ private readonly (float, string)[] NOTE_GRADES = {
10637
private int MaxPossibleScore;
10738

10839
void Start() {
109-
RhythmGameSong song = RhythmGameSong.ParseMidi(
40+
RhythmGameSong song = RhythmGameSongParser.ParseMidi(
11041
System.IO.Path.Combine(Application.streamingAssetsPath, "Rhythm Game Data", "Demo.mid")
11142
);
11243

@@ -151,7 +82,6 @@ void Update() {
15182
if (WrappedInput.GetButtonUp("Rhythm Game Left")) {
15283
HandleButtonUp(RhythmGameNoteType.Left);
15384
}
154-
15585
if (WrappedInput.GetButtonUp("Rhythm Game Down")) {
15686
HandleButtonUp(RhythmGameNoteType.Down);
15787
}

Assets/src/RhythmGameNote.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
public enum RhythmGameNoteType {
2+
Left = 0,
3+
Down = 1,
4+
Up = 2,
5+
Right = 3,
6+
};
7+
8+
public class RhythmGameNote {
9+
public RhythmGameNoteType Type;
10+
public float Time;
11+
public float Duration = 0f;
12+
13+
public bool IsHold => Duration > 0f;
14+
public bool IsInstant => !IsHold;
15+
16+
public float RelativeTime(float currentTime, bool end = false) =>
17+
Time - currentTime + (end ? Duration : 0f);
18+
}

Assets/src/RhythmGameNote.cs.meta

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

Assets/src/RhythmGameSong.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using System.Collections.Generic;
2+
3+
public class RhythmGameSong {
4+
public float BeatsPerMinute;
5+
public List<RhythmGameNote> Notes;
6+
}

Assets/src/RhythmGameSong.cs.meta

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

Assets/src/RhythmGameSongParser.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System.Collections.Generic;
2+
using MidiParser;
3+
4+
public class RhythmGameSongParser {
5+
/**
6+
* Each of the four note types is mapped to F4, A4, C5 and E5 respectively
7+
* (the gaps on a treble stave in C Major). If a note's duration should be
8+
* taken into consideration, it should be shifted up one degree in the C Major
9+
* scale (i.e. on a line instead of a gap).
10+
*/
11+
private readonly Dictionary<int, (RhythmGameNoteType, bool IsHold)> MIDI_PITCH_DATA = new()
12+
{
13+
{ 65, (RhythmGameNoteType.Left, false) }, // F4
14+
{ 67, (RhythmGameNoteType.Left, true) }, // G4
15+
{ 69, (RhythmGameNoteType.Down, false) }, // A4
16+
{ 71, (RhythmGameNoteType.Down, true) }, // B4
17+
{ 72, (RhythmGameNoteType.Up, false) }, // C5
18+
{ 74, (RhythmGameNoteType.Up, true) }, // D5
19+
{ 76, (RhythmGameNoteType.Right, false) }, // E5
20+
{ 77, (RhythmGameNoteType.Right, true) }, // F5
21+
};
22+
23+
private List<RhythmGameNote> Notes = new();
24+
private int TicksPerBeat;
25+
private float BeatsPerMinute = 120f;
26+
private Dictionary<int, MidiEvent> NoteOnEvents = new();
27+
28+
public static RhythmGameSong ParseMidi(string path) {
29+
MidiFile midi = new MidiFile(path);
30+
31+
if (midi.Tracks.Length != 1)
32+
throw new System.Exception("MIDI file should have exactly one track");
33+
34+
RhythmGameSongParser parser = new(ticksPerBeat: midi.TicksPerQuarterNote);
35+
midi.Tracks[0].MidiEvents.ForEach(parser.ParseMidiEvent);
36+
return parser.GetSong();
37+
}
38+
39+
private RhythmGameSongParser(int ticksPerBeat) {
40+
TicksPerBeat = ticksPerBeat;
41+
}
42+
43+
private void ParseMidiEvent(MidiEvent midiEvent) {
44+
switch (midiEvent.MidiEventType) {
45+
case MidiEventType.MetaEvent:
46+
ParseMetaEvent(midiEvent);
47+
break;
48+
49+
case MidiEventType.NoteOn:
50+
ParseNoteOn(midiEvent);
51+
break;
52+
53+
case MidiEventType.NoteOff:
54+
ParseNoteOff(midiEvent);
55+
break;
56+
}
57+
}
58+
59+
private void ParseMetaEvent(MidiEvent midiEvent) {
60+
if (midiEvent.MetaEventType == MetaEventType.Tempo) {
61+
// TODO: Track a list of tempo change events on the song object
62+
BeatsPerMinute = (int)midiEvent.Arg2;
63+
}
64+
}
65+
66+
private void ParseNoteOn(MidiEvent midiEvent) {
67+
/**
68+
* Store the note on event so that we can reference it from the note off
69+
* event.
70+
*/
71+
NoteOnEvents[midiEvent.Note] = midiEvent;
72+
}
73+
74+
private void ParseNoteOff(MidiEvent midiEvent) {
75+
int pitch = midiEvent.Note;
76+
var (type, isHold) = MIDI_PITCH_DATA[pitch];
77+
78+
float onTime = TicksToSeconds(NoteOnEvents[pitch].Time);
79+
float offTime = TicksToSeconds(midiEvent.Time);
80+
float actualDuration = offTime - onTime;
81+
float effectiveDuration = isHold ? actualDuration : 0f;
82+
83+
AddNote(type, onTime, effectiveDuration);
84+
}
85+
86+
private void AddNote(RhythmGameNoteType type, float time, float duration) {
87+
Notes.Add(
88+
new()
89+
{
90+
Type = type,
91+
Time = time,
92+
Duration = duration,
93+
}
94+
);
95+
}
96+
97+
private float TicksToSeconds(float ticks) => ticks / TicksPerBeat * 60f / BeatsPerMinute;
98+
99+
private RhythmGameSong GetSong() => new() { BeatsPerMinute = BeatsPerMinute, Notes = Notes };
100+
}

Assets/src/RhythmGameSongParser.cs.meta

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

0 commit comments

Comments
 (0)