-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstepmidi.py
192 lines (156 loc) · 6.72 KB
/
stepmidi.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# _____ __ __ ___________ ____
# / ___// /____ ____ / |/ / _/ __ \/ _/
# \__ \/ __/ _ \/ __ \/ /|_/ // // / / // /
# ___/ / /_/ __/ /_/ / / / // // /_/ // /
# /____/\__/\___/ .___/_/ /_/___/_____/___/
# /_/
# StepMIDI by @sugoku
# converts between rhythm game file formats and MIDI
# SPDX-License-Identifier: MIT
# currently this code is only made so I can convert between sheet music and gitadora drum, ultimately
from __future__ import annotations
from dataclasses import dataclass, field
from collections import deque
from typing import Type, List
import sys
import mido
import simfile
from simfile.notes import Note as NoteSM
from simfile.notes import NoteType, NoteData
from simfile.timing import Beat, BeatValue, BeatValues
from simfile.ssc import SSCSimfile, SSCChart
from config import *
@dataclass
class Note:
pitch: int # MIDI, 0-127
beat_start: float
beat_end: float
'''measure: int
start: Fraction # in measures
length: Fraction # in measures
def get_midi_on_time(self, ppq: int) -> float:
return ppq * (some_timesig_thing) * (self.measure + float(self.start))
def get_midi_off_time(self, ppq: int) -> float:
return ppq * (Some_timesig_thing) * (self.measure + float(self.start) + float(self.length))'''
@dataclass
class Track: # also Chart
name: str = ''
notes: List[Note] = field(default_factory=list)
difficulty: int = 10
@dataclass
class Tempo: # tempo event
start: float = 0.0 # in measures
value: float = 120.0
@dataclass
class TimeSig: # time signature event
start: float = 0.0 # in measures
numerator: int = 0
denominator: int = 0
@dataclass
class Song:
tracks: List[Track] = field(default_factory=list)
title: str = 'a song'
artist: str = 'github.com/sugoku'
tempos: List[Tempo] = field(default_factory=list)
timesignatures: List[TimeSig] = field(default_factory=list)
ppq: int = 0
# time signature is not used in MIDI or SSC so don't worry about that
def to_midi(self):
pass
def to_ssc(self):
sim = SSCSimfile.blank()
sim.title = self.title
sim.artist = self.artist
t_vals = sorted(BeatValue(tempo.start, tempo.value) for tempo in self.tempos)
if not len(t_vals):
t_vals = [BeatValue(0.0, 120.0)]
sim.bpms = str(BeatValues([t for n, t in enumerate(t_vals) if t.value not in [x.value for x in t_vals[:n]]]))
# based on simfile BeatValues __str__ function
sim.timesignatures = ',\n'.join(f'{ts.start}={ts.numerator}={ts.denominator}' for ts in self.timesignatures)
# print(sim.bpms)
for track in self.tracks:
if not len(track.notes):
continue
chart = SSCChart.blank()
chart.stepstype = gamemode
chart.difficulty = track.difficulty
notes = []
for note in track.notes:
if note.pitch not in midi_to_gddm:
continue
notes.append(NoteSM(Beat(note.beat_start), midi_to_gddm[note.pitch], NoteType.TAP))
continue
if (note.beat_end - note.beat_start) < 0.125:
# beat, column, type
notes.append(NoteSM(Beat(note.beat_start), midi_to_gddm[note.pitch], NoteType.TAP))
# to do: if pedal on off beat 16th/24th/whatever and previous and next 16th/24th is a kick pedal, do left pedal instead
# this can be done afterwards
else:
notes.append(NoteSM(Beat(note.beat_start), midi_to_gddm[note.pitch], NoteType.HOLD_HEAD))
notes.append(NoteSM(Beat(note.beat_end), midi_to_gddm[note.pitch], NoteType.TAIL))
if not len(notes):
continue
nd = NoteData.from_notes(sorted(notes), 10)
chart.notes = str(nd)
sim.charts.append(chart)
# print(sim.charts)
return sim
@classmethod
def from_midi(cls: Song, midi: mido.MidiFile) -> Song:
s = Song()
s.ppq = midi.ticks_per_beat
# print(s.ppq)
# print(len(midi.tracks))
for mt in midi.tracks:
t = Track()
for i in range(1, len(mt)):
mt[i].time += mt[i-1].time
mt.sort(key=lambda m: m.time)
# for event in mt:
# print(event.time)
s.tempos += [Tempo(msg.time / midi.ticks_per_beat, mido.tempo2bpm(msg.tempo)) for msg in mt if msg.type == 'set_tempo']
s.timesignatures += [TimeSig(msg.time / midi.ticks_per_beat, msg.numerator, msg.denominator) for msg in mt if msg.type == 'time_signature']
note_ons = deque([msg for msg in mt if msg.type == 'note_on' and msg.velocity > 0])
note_offs = [msg for msg in mt if msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity == 0)]
while len(note_ons):
on_msg = note_ons.popleft()
i = 0
while i < len(note_offs):
off_msg = note_offs[i]
if off_msg.note == on_msg.note:
# print(on_msg.time)
t.notes.append(Note(
pitch = on_msg.note,
beat_start = on_msg.time / midi.ticks_per_beat,
beat_end = off_msg.time / midi.ticks_per_beat
))
'''beat_start = on_msg.time // midi.ticks_per_beat,
beat_end = 3,
measure = on_msg.time // midi.ticks_per_beat / 4,
start = Fraction((on_msg.time / midi.ticks_per_beat / 4) % 1.0),
length = Fraction((off_msg.time / midi.ticks_per_beat / 4) % 1.0)'''
# print(t.notes[-1])
del note_offs[i]
if not len(note_ons):
break
on_msg = note_ons.popleft()
i = 0
else:
i += 1
# print(t)
s.tracks.append(t)
return s
@classmethod
def from_ssc(cls: Song, ssc) -> Song:
s = Song()
return s
def main():
s = Song.from_midi(mido.MidiFile(sys.argv[1]))
# print(s.tracks)
fn = sys.argv[2]
with open(fn, 'w') as f:
ssc = s.to_ssc()
# print(ssc)
ssc.serialize(f)
if __name__ == '__main__':
main()