├── README.md ├── bin └── placeholder └── processor.py /README.md: -------------------------------------------------------------------------------- 1 | # Midi Neural Processor 2 | 3 | * Repository for midi-based machine learning model's {pre/post} processing. 4 | * You can use this processor in any machine learnig library like tensorflow, pytorch, etc... 5 | 6 | * This processor's algorithm is based on [PerformanceRNN](https://magenta.tensorflow.org/performance-rnn) & [Music Transformer (Polyphonic Music)](https://arxiv.org/abs/1809.04281) Model's preprocessing algorithm suggested by Google Magenta. 7 | 8 | 9 | 10 | ## Simple Useage 11 | 12 | ### Download 13 | 14 | ```bash 15 | $ git clone https://github.com/jason9693/midi-processor.git 16 | ``` 17 | 18 | ### Encoding & Load midi file 19 | 20 | * You can load & encode your midi file just one line 21 | * encode_midi() is a role of pre-processing. 22 | 23 | ```python 24 | from processor import encode_midi 25 | encoded = encode_midi('bin/ADIG04.mid') ## 'bin/AIDG04.mid' is midi file path. 26 | ## output: [int, int, int, int, ... ]. 27 | ## int range is range(0,388). 388 = NOTE_ON + NOTE_OFF + TIME_SHIFT + VELOCITY 28 | ``` 29 | 30 | ### Decoding 31 | 32 | * decode_midi is convert integer array to midi form. 33 | * you can gave method to ***file_path*** as a second args in that if you want to save midi as .mid file. 34 | * all elements in integer array should be range(0,388). 35 | 36 | ```python 37 | from processor import decode_midi 38 | decode_midi(encoded, 'bin/test.mid') ## 'bin/test.mid' is midi file path. 39 | ``` 40 | 41 | 42 | 43 | ## Comming Soon 44 | 45 | 1. Pedal Control 46 | 2. Midi Converter to .tfrecords 47 | 48 | 49 | 50 | ## License 51 | 52 | Project is published under the MIT licence. Feel free to clone and modify repo as you want, but don't forget to add reference to authors :) 53 | 54 | -------------------------------------------------------------------------------- /bin/placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason9693/midi-neural-processor/3875298892adafbc5cebeeb32050732c5ae5aee1/bin/placeholder -------------------------------------------------------------------------------- /processor.py: -------------------------------------------------------------------------------- 1 | import pretty_midi 2 | 3 | 4 | RANGE_NOTE_ON = 128 5 | RANGE_NOTE_OFF = 128 6 | RANGE_VEL = 32 7 | RANGE_TIME_SHIFT = 100 8 | 9 | START_IDX = { 10 | 'note_on': 0, 11 | 'note_off': RANGE_NOTE_ON, 12 | 'time_shift': RANGE_NOTE_ON + RANGE_NOTE_OFF, 13 | 'velocity': RANGE_NOTE_ON + RANGE_NOTE_OFF + RANGE_TIME_SHIFT 14 | } 15 | 16 | 17 | class SustainAdapter: 18 | def __init__(self, time, type): 19 | self.start = time 20 | self.type = type 21 | 22 | 23 | class SustainDownManager: 24 | def __init__(self, start, end): 25 | self.start = start 26 | self.end = end 27 | self.managed_notes = [] 28 | self._note_dict = {} # key: pitch, value: note.start 29 | 30 | def add_managed_note(self, note: pretty_midi.Note): 31 | self.managed_notes.append(note) 32 | 33 | def transposition_notes(self): 34 | for note in reversed(self.managed_notes): 35 | try: 36 | note.end = self._note_dict[note.pitch] 37 | except KeyError: 38 | note.end = max(self.end, note.end) 39 | self._note_dict[note.pitch] = note.start 40 | 41 | 42 | # Divided note by note_on, note_off 43 | class SplitNote: 44 | def __init__(self, type, time, value, velocity): 45 | ## type: note_on, note_off 46 | self.type = type 47 | self.time = time 48 | self.velocity = velocity 49 | self.value = value 50 | 51 | def __repr__(self): 52 | return '<[SNote] time: {} type: {}, value: {}, velocity: {}>'\ 53 | .format(self.time, self.type, self.value, self.velocity) 54 | 55 | 56 | class Event: 57 | def __init__(self, event_type, value): 58 | self.type = event_type 59 | self.value = value 60 | 61 | def __repr__(self): 62 | return ''.format(self.type, self.value) 63 | 64 | def to_int(self): 65 | return START_IDX[self.type] + self.value 66 | 67 | @staticmethod 68 | def from_int(int_value): 69 | info = Event._type_check(int_value) 70 | return Event(info['type'], info['value']) 71 | 72 | @staticmethod 73 | def _type_check(int_value): 74 | range_note_on = range(0, RANGE_NOTE_ON) 75 | range_note_off = range(RANGE_NOTE_ON, RANGE_NOTE_ON+RANGE_NOTE_OFF) 76 | range_time_shift = range(RANGE_NOTE_ON+RANGE_NOTE_OFF,RANGE_NOTE_ON+RANGE_NOTE_OFF+RANGE_TIME_SHIFT) 77 | 78 | valid_value = int_value 79 | 80 | if int_value in range_note_on: 81 | return {'type': 'note_on', 'value': valid_value} 82 | elif int_value in range_note_off: 83 | valid_value -= RANGE_NOTE_ON 84 | return {'type': 'note_off', 'value': valid_value} 85 | elif int_value in range_time_shift: 86 | valid_value -= (RANGE_NOTE_ON + RANGE_NOTE_OFF) 87 | return {'type': 'time_shift', 'value': valid_value} 88 | else: 89 | valid_value -= (RANGE_NOTE_ON + RANGE_NOTE_OFF + RANGE_TIME_SHIFT) 90 | return {'type': 'velocity', 'value': valid_value} 91 | 92 | 93 | def _divide_note(notes): 94 | result_array = [] 95 | notes.sort(key=lambda x: x.start) 96 | 97 | for note in notes: 98 | on = SplitNote('note_on', note.start, note.pitch, note.velocity) 99 | off = SplitNote('note_off', note.end, note.pitch, None) 100 | result_array += [on, off] 101 | return result_array 102 | 103 | 104 | def _merge_note(snote_sequence): 105 | note_on_dict = {} 106 | result_array = [] 107 | 108 | for snote in snote_sequence: 109 | # print(note_on_dict) 110 | if snote.type == 'note_on': 111 | note_on_dict[snote.value] = snote 112 | elif snote.type == 'note_off': 113 | try: 114 | on = note_on_dict[snote.value] 115 | off = snote 116 | if off.time - on.time == 0: 117 | continue 118 | result = pretty_midi.Note(on.velocity, snote.value, on.time, off.time) 119 | result_array.append(result) 120 | except: 121 | print('info removed pitch: {}'.format(snote.value)) 122 | return result_array 123 | 124 | 125 | def _snote2events(snote: SplitNote, prev_vel: int): 126 | result = [] 127 | if snote.velocity is not None: 128 | modified_velocity = snote.velocity // 4 129 | if prev_vel != modified_velocity: 130 | result.append(Event(event_type='velocity', value=modified_velocity)) 131 | result.append(Event(event_type=snote.type, value=snote.value)) 132 | return result 133 | 134 | 135 | def _event_seq2snote_seq(event_sequence): 136 | timeline = 0 137 | velocity = 0 138 | snote_seq = [] 139 | 140 | for event in event_sequence: 141 | if event.type == 'time_shift': 142 | timeline += ((event.value+1) / 100) 143 | if event.type == 'velocity': 144 | velocity = event.value * 4 145 | else: 146 | snote = SplitNote(event.type, timeline, event.value, velocity) 147 | snote_seq.append(snote) 148 | return snote_seq 149 | 150 | 151 | def _make_time_sift_events(prev_time, post_time): 152 | time_interval = int(round((post_time - prev_time) * 100)) 153 | results = [] 154 | while time_interval >= RANGE_TIME_SHIFT: 155 | results.append(Event(event_type='time_shift', value=RANGE_TIME_SHIFT-1)) 156 | time_interval -= RANGE_TIME_SHIFT 157 | if time_interval == 0: 158 | return results 159 | else: 160 | return results + [Event(event_type='time_shift', value=time_interval-1)] 161 | 162 | 163 | def _control_preprocess(ctrl_changes): 164 | sustains = [] 165 | 166 | manager = None 167 | for ctrl in ctrl_changes: 168 | if ctrl.value >= 64 and manager is None: 169 | # sustain down 170 | manager = SustainDownManager(start=ctrl.time, end=None) 171 | elif ctrl.value < 64 and manager is not None: 172 | # sustain up 173 | manager.end = ctrl.time 174 | sustains.append(manager) 175 | manager = None 176 | elif ctrl.value < 64 and len(sustains) > 0: 177 | sustains[-1].end = ctrl.time 178 | return sustains 179 | 180 | 181 | def _note_preprocess(susteins, notes): 182 | note_stream = [] 183 | 184 | if susteins: # if the midi file has sustain controls 185 | for sustain in susteins: 186 | for note_idx, note in enumerate(notes): 187 | if note.start < sustain.start: 188 | note_stream.append(note) 189 | elif note.start > sustain.end: 190 | notes = notes[note_idx:] 191 | sustain.transposition_notes() 192 | break 193 | else: 194 | sustain.add_managed_note(note) 195 | 196 | for sustain in susteins: 197 | note_stream += sustain.managed_notes 198 | 199 | else: # else, just push everything into note stream 200 | for note_idx, note in enumerate(notes): 201 | note_stream.append(note) 202 | 203 | note_stream.sort(key= lambda x: x.start) 204 | return note_stream 205 | 206 | 207 | def encode_midi(file_path): 208 | events = [] 209 | notes = [] 210 | mid = pretty_midi.PrettyMIDI(midi_file=file_path) 211 | 212 | for inst in mid.instruments: 213 | inst_notes = inst.notes 214 | # ctrl.number is the number of sustain control. If you want to know abour the number type of control, 215 | # see https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2 216 | ctrls = _control_preprocess([ctrl for ctrl in inst.control_changes if ctrl.number == 64]) 217 | notes += _note_preprocess(ctrls, inst_notes) 218 | 219 | dnotes = _divide_note(notes) 220 | 221 | # print(dnotes) 222 | dnotes.sort(key=lambda x: x.time) 223 | # print('sorted:') 224 | # print(dnotes) 225 | cur_time = 0 226 | cur_vel = 0 227 | for snote in dnotes: 228 | events += _make_time_sift_events(prev_time=cur_time, post_time=snote.time) 229 | events += _snote2events(snote=snote, prev_vel=cur_vel) 230 | # events += _make_time_sift_events(prev_time=cur_time, post_time=snote.time) 231 | 232 | cur_time = snote.time 233 | cur_vel = snote.velocity 234 | 235 | return [e.to_int() for e in events] 236 | 237 | 238 | def decode_midi(idx_array, file_path=None): 239 | event_sequence = [Event.from_int(idx) for idx in idx_array] 240 | # print(event_sequence) 241 | snote_seq = _event_seq2snote_seq(event_sequence) 242 | note_seq = _merge_note(snote_seq) 243 | note_seq.sort(key=lambda x:x.start) 244 | 245 | mid = pretty_midi.PrettyMIDI() 246 | # if want to change instument, see https://www.midi.org/specifications/item/gm-level-1-sound-set 247 | instument = pretty_midi.Instrument(1, False, "Developed By Yang-Kichang") 248 | instument.notes = note_seq 249 | 250 | mid.instruments.append(instument) 251 | if file_path is not None: 252 | mid.write(file_path) 253 | return mid 254 | 255 | 256 | if __name__ == '__main__': 257 | encoded = encode_midi('bin/ADIG04.mid') 258 | print(encoded) 259 | decided = decode_midi(encoded,file_path='bin/test.mid') 260 | 261 | ins = pretty_midi.PrettyMIDI('bin/ADIG04.mid') 262 | print(ins) 263 | print(ins.instruments[0]) 264 | for i in ins.instruments: 265 | print(i.control_changes) 266 | print(i.notes) 267 | 268 | --------------------------------------------------------------------------------