1
+ from typing import Optional
2
+
1
3
from midiutil .MidiFile import NoteOff
2
4
3
5
from parsers .patterns import Pattern , Note
4
6
from midiutil import MIDIFile
5
7
8
+ from parsers .project import Song
9
+
6
10
7
- class PatternToMidiExporter :
11
+ class BaseMidiExporter :
12
+ """Base class for all midi exporters"""
8
13
9
14
# midi utils uses either ticks of beats (quarter notes) as time
10
15
# beats are expressed in floats
11
16
# tracker uses 1/16 of s note
12
17
# so this value is a tracker step duration to use with MIDIUtil
13
18
MIDI_16TH_NOTE_TIME_VALUE = 0.25
14
19
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
+
15
33
def __init__ (self , pattern : Pattern , tempo_bpm = 120 ):
16
34
17
35
self .pattern = pattern
@@ -43,38 +61,62 @@ def get_midi_note_value(note: Note):
43
61
# tracker C4 is 48
44
62
return note .value + 12
45
63
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
+
47
68
degrees = [60 , 62 , 64 , 65 , 67 , 69 , 71 , 72 ] # MIDI note number
48
69
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 ()
57
97
58
- track = 0
59
98
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
63
99
default_volume = 127 # 0-127, as per the MIDI standard
64
100
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)
68
105
69
- midi_file = MIDIFile (midi_tracks_count )
70
- midi_file .addTempo (track = 0 , time = 0 , tempo = self .tempo_bpm )
106
+ track = 0
71
107
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
76
111
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 ]} " )
78
120
79
121
for track in self .pattern .tracks :
80
122
@@ -119,10 +161,7 @@ def generate_midi(self) -> MIDIFile:
119
161
120
162
duration = (note_end_position - step_number ) * PatternToMidiExporter .MIDI_16TH_NOTE_TIME_VALUE
121
163
122
-
123
- # TODO: add support for chord fx
124
- # TODO: add support for arp fx
125
-
164
+ # add actual notes to midi file data
126
165
if step .get_arp ():
127
166
# arpeggio
128
167
arp = step .get_arp ()
@@ -169,7 +208,7 @@ def generate_midi(self) -> MIDIFile:
169
208
midi_file .addNote (track = instrument_to_midi_track_map [step .instrument_number ],
170
209
channel = channel ,
171
210
pitch = PatternToMidiExporter .get_midi_note_value (note ),
172
- time = arp_note_start_time ,
211
+ time = start_time_offset + arp_note_start_time ,
173
212
duration = arp_note_duration ,
174
213
# TODO: write velocity fx value if set (needs to be converted to 0...127!!!)
175
214
volume = default_volume ,
@@ -187,7 +226,7 @@ def generate_midi(self) -> MIDIFile:
187
226
midi_file .addNote (track = instrument_to_midi_track_map [step .instrument_number ],
188
227
channel = channel ,
189
228
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 ,
191
230
duration = duration ,
192
231
# TODO: write velocity fx value if set (needs to be converted to 0...127!!!)
193
232
volume = default_volume ,
@@ -198,20 +237,86 @@ def generate_midi(self) -> MIDIFile:
198
237
midi_file .addNote (track = instrument_to_midi_track_map [step .instrument_number ],
199
238
channel = channel ,
200
239
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 ,
202
241
duration = duration ,
203
242
# TODO: write velocity fx value if set (needs to be converted to 0...127!!!)
204
243
volume = default_volume ,
205
244
)
206
245
207
246
return midi_file
208
247
209
- def write_midi_file (self , path : str ):
210
248
211
- midi_file = self . generate_midi ()
249
+ class SongToMidiExporter ( BaseMidiExporter ):
212
250
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 = {}
215
283
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 )
216
287
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 ]} " )
217
292
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
0 commit comments