Skip to content

Commit 27691d2

Browse files
authored
Merge pull request #1 from DataGreed/feature/song-parsing
Feature/song parsing
2 parents 4021a3a + 5401fc3 commit 27691d2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+507
-57
lines changed

exporters/midi.py

Lines changed: 140 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,35 @@
1+
from typing import Optional
2+
13
from midiutil.MidiFile import NoteOff
24

35
from parsers.patterns import Pattern, Note
46
from midiutil import MIDIFile
57

8+
from parsers.project import Song
9+
610

7-
class PatternToMidiExporter:
11+
class BaseMidiExporter:
12+
"""Base class for all midi exporters"""
813

914
# midi utils uses either ticks of beats (quarter notes) as time
1015
# beats are expressed in floats
1116
# tracker uses 1/16 of s note
1217
# so this value is a tracker step duration to use with MIDIUtil
1318
MIDI_16TH_NOTE_TIME_VALUE = 0.25
1419

20+
# def generate_midi(self) -> MIDIFile:
21+
# raise NotImplementedError()
22+
23+
def write_midi_file(self, path: str):
24+
25+
midi_file = self.generate_midi()
26+
27+
with open(path, "wb") as output_file:
28+
midi_file.writeFile(output_file)
29+
30+
31+
class PatternToMidiExporter(BaseMidiExporter):
32+
1533
def __init__(self, pattern: Pattern, tempo_bpm=120):
1634

1735
self.pattern = pattern
@@ -43,38 +61,62 @@ def get_midi_note_value(note: Note):
4361
# tracker C4 is 48
4462
return note.value+12
4563

46-
def generate_midi(self) -> MIDIFile:
64+
def generate_midi(self, midi_file: MIDIFile = None,
65+
instrument_to_midi_track_map: dict = None,
66+
start_time_offset: float = 0) -> MIDIFile:
67+
4768
degrees = [60, 62, 64, 65, 67, 69, 71, 72] # MIDI note number
4869

49-
# tracker tracks are not actual tracks, but voices,
50-
# since every track can use any instrument at even given time and
51-
# every track is monophonic.
52-
# midi tracks typically represent different instruments and are polyphonic
53-
# so we should count number of instruments in pattern and use it as
54-
# number of tracks
55-
instruments = self.get_list_of_instruments()
56-
midi_tracks_count = len(instruments)
70+
if not instrument_to_midi_track_map:
71+
# tracker tracks are not actual tracks, but voices,
72+
# since every track can use any instrument at even given time and
73+
# every track is monophonic.
74+
# midi tracks typically represent different instruments and are polyphonic
75+
# so we should count number of instruments in pattern and use it as
76+
# number of tracks
77+
instruments = self.get_list_of_instruments()
78+
79+
80+
# this maps allows us to quickly find midi track for given instrument
81+
# this should be faster than calling instruments.indexOf()
82+
instrument_to_midi_track_map = {}
83+
84+
for i in range(len(instruments)):
85+
# todo: get actual track names from project file (or are they stored in instrument files?)
86+
# todo: instrument 48 is midi instrument 1 the next 15 are also midi instruments - set their names
87+
# midi_file.addTrackName(track=i, time=0, trackName=f"Instrument {instruments[i]}")
88+
89+
instrument_to_midi_track_map[instruments[i]] = i
90+
91+
else:
92+
# instrument_to_midi_track_map is supploed in case we render
93+
# a song. In this case we may have different instruments in different patterns
94+
# and need a mappign for all of them. We also need to create a midi file
95+
# with tracks for all used instruments.
96+
instruments = instrument_to_midi_track_map.keys()
5797

58-
track = 0
5998
channel = 0
60-
time = 0 # In beats (is it 4:4?)
61-
default_duration = 1 # In beats (is it 4:4?)
62-
tempo = 60 # In BPM
6399
default_volume = 127 # 0-127, as per the MIDI standard
64100

65-
# this maps allows us to quickly find midi track for given instrument
66-
# this should be faster than calling instruments.indexOf()
67-
instrument_to_midi_track_map = {}
101+
if not midi_file:
102+
# if we are not supplied with a midi file,
103+
# create a new one (we are supplied with one, e.g. if we render a song
104+
# and we need to append pattern mido to existing file)
68105

69-
midi_file = MIDIFile(midi_tracks_count)
70-
midi_file.addTempo(track=0, time=0, tempo=self.tempo_bpm)
106+
track = 0
71107

72-
for i in range(len(instruments)):
73-
# todo: get actual track names from project file (or are they stored in instrument files?)
74-
# todo: instrument 48 is midi instrument 1 the next 15 are also midi instruments - set their names
75-
midi_file.addTrackName(track=i, time=0, trackName=f"Instrument {instruments[i]}")
108+
time = 0 # In beats (is it 4:4?)
109+
default_duration = 1 # In beats (is it 4:4?)
110+
tempo = 60 # In BPM
76111

77-
instrument_to_midi_track_map[instruments[i]] = i
112+
midi_tracks_count = len(instruments)
113+
midi_file = MIDIFile(midi_tracks_count)
114+
midi_file.addTempo(track=0, time=0, tempo=self.tempo_bpm)
115+
116+
for i in range(len(instruments)):
117+
# todo: get actual track names from project file (or are they stored in instrument files?)
118+
# todo: instrument 48 is midi instrument 1 the next 15 are also midi instruments - set their names
119+
midi_file.addTrackName(track=i, time=0, trackName=f"Instrument {instruments[i]}")
78120

79121
for track in self.pattern.tracks:
80122

@@ -119,10 +161,7 @@ def generate_midi(self) -> MIDIFile:
119161

120162
duration = (note_end_position - step_number) * PatternToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE
121163

122-
123-
# TODO: add support for chord fx
124-
# TODO: add support for arp fx
125-
164+
# add actual notes to midi file data
126165
if step.get_arp():
127166
# arpeggio
128167
arp = step.get_arp()
@@ -169,7 +208,7 @@ def generate_midi(self) -> MIDIFile:
169208
midi_file.addNote(track=instrument_to_midi_track_map[step.instrument_number],
170209
channel=channel,
171210
pitch=PatternToMidiExporter.get_midi_note_value(note),
172-
time=arp_note_start_time,
211+
time=start_time_offset + arp_note_start_time,
173212
duration=arp_note_duration,
174213
# TODO: write velocity fx value if set (needs to be converted to 0...127!!!)
175214
volume=default_volume,
@@ -187,7 +226,7 @@ def generate_midi(self) -> MIDIFile:
187226
midi_file.addNote(track=instrument_to_midi_track_map[step.instrument_number],
188227
channel=channel,
189228
pitch=PatternToMidiExporter.get_midi_note_value(note),
190-
time=step_number * PatternToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE,
229+
time=start_time_offset + step_number * PatternToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE,
191230
duration=duration,
192231
# TODO: write velocity fx value if set (needs to be converted to 0...127!!!)
193232
volume=default_volume,
@@ -198,20 +237,86 @@ def generate_midi(self) -> MIDIFile:
198237
midi_file.addNote(track=instrument_to_midi_track_map[step.instrument_number],
199238
channel=channel,
200239
pitch=PatternToMidiExporter.get_midi_note_value(step.note),
201-
time=step_number*PatternToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE,
240+
time=start_time_offset + step_number*PatternToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE,
202241
duration=duration,
203242
# TODO: write velocity fx value if set (needs to be converted to 0...127!!!)
204243
volume=default_volume,
205244
)
206245

207246
return midi_file
208247

209-
def write_midi_file(self, path: str):
210248

211-
midi_file = self.generate_midi()
249+
class SongToMidiExporter(BaseMidiExporter):
212250

213-
with open(path, "wb") as output_file:
214-
midi_file.writeFile(output_file)
251+
def __init__(self, song: Song):
252+
self.song = song
253+
254+
def get_list_of_instruments(self):
255+
"""
256+
Gets list of all actually used instruments
257+
across all patterns of the song.
258+
Used to create midi file with proper instrument tracks.
259+
:return:
260+
"""
261+
instruments = set()
262+
# iterate over unique patterns only
263+
for pattern in self.song.pattern_mapping.values():
264+
instruments.update(PatternToMidiExporter(pattern=pattern).get_list_of_instruments())
265+
266+
return sorted(instruments)
267+
268+
def generate_midi(self) -> MIDIFile:
269+
# raise NotImplementedError()
270+
271+
# tracker tracks are not actual tracks, but voices,
272+
# since every track can use any instrument at even given time and
273+
# every track is monophonic.
274+
# midi tracks typically represent different instruments and are polyphonic
275+
# so we should count number of instruments in pattern and use it as
276+
# number of tracks
277+
instruments = self.get_list_of_instruments()
278+
midi_tracks_count = len(instruments)
279+
280+
# this maps allows us to quickly find midi track for given instrument
281+
# this should be faster than calling instruments.indexOf()
282+
instrument_to_midi_track_map = {}
215283

284+
midi_file = MIDIFile(midi_tracks_count)
285+
#FIXME: write bpm to song to get it from there
286+
midi_file.addTempo(track=0, time=0, tempo=self.song.bpm)
216287

288+
for i in range(len(instruments)):
289+
# todo: get actual track names from project file (or are they stored in instrument files?)
290+
# todo: instrument 48 is midi instrument 1 the next 15 are also midi instruments - set their names
291+
midi_file.addTrackName(track=i, time=0, trackName=f"Instrument {instruments[i]}")
217292

293+
instrument_to_midi_track_map[instruments[i]] = i
294+
295+
print(f"instruments: {instruments}")
296+
print(f"instrument_to_midi_track_map: {instrument_to_midi_track_map}")
297+
298+
previous_pattern: Optional[Pattern] = None
299+
300+
j = 0
301+
start_time_offset = 0
302+
print(self.song.pattern_chain)
303+
for pattern in self.song.get_song_as_patterns(): #fixme: temporary slice for debug
304+
j+=1
305+
print(f"Rendering song slot {j}")
306+
exporter = PatternToMidiExporter(pattern=pattern)
307+
308+
if previous_pattern:
309+
# every next pattern should write midi data
310+
# after the previous pattern ended,
311+
# so we have to add time offset for every pattern
312+
start_time_offset += previous_pattern.tracks[0].length * SongToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE
313+
314+
# todo: add arguments and handle them
315+
# todo: no need to do value declaration here really, we already pass it by reference
316+
midi_file = exporter.generate_midi(midi_file=midi_file,
317+
instrument_to_midi_track_map=instrument_to_midi_track_map,
318+
start_time_offset=start_time_offset)
319+
320+
previous_pattern = pattern
321+
322+
return midi_file

main.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11

22
if __name__ == '__main__':
33

4-
from parsers import patterns
5-
6-
# todo: remove this and implement tests
7-
p = patterns.PatternParser(
8-
# NOTE: this file was created with firmware 1.3.1 or older version
9-
filename="./reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_06.mtp")
10-
parsed_pattern = p.parse()
11-
# print(parsed_pattern.render_as_table())
12-
from exporters import midi
13-
14-
midi_exporter = midi.PatternToMidiExporter(pattern=parsed_pattern)
15-
print(midi_exporter.generate_midi())
16-
midi_exporter.write_midi_file("./test_midi_file.mid")
4+
from parsers import patterns, project
5+
6+
# # todo: remove this and implement tests
7+
# p = patterns.PatternParser(
8+
# # NOTE: this file was created with firmware 1.3.1 or older version
9+
# filename="./reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_06.mtp")
10+
# parsed_pattern = p.parse()
11+
# # print(parsed_pattern.render_as_table())
12+
# from exporters import midi
13+
#
14+
# midi_exporter = midi.PatternToMidiExporter(pattern=parsed_pattern)
15+
# print(midi_exporter.generate_midi())
16+
# midi_exporter.write_midi_file("./test_midi_file.mid")
17+
18+
19+
project = project.ProjectParser(
20+
filename_or_folder="./reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/"
21+
)
22+
23+
parsed_project = project.parse()
24+
print("Finished parsing project.")
25+
print(f"BPM: {parsed_project.song.bpm}")
26+
print(f"pattern mapping: {parsed_project.song.pattern_mapping}")
27+
print(f"pattern chain: {parsed_project.song.pattern_chain}")
28+
print(f"song as patterns: {parsed_project.song.get_song_as_patterns()}")
1729

1830

0 commit comments

Comments
 (0)