├── .gitignore ├── LICENSE ├── README.md ├── roll.py └── test_file ├── 1.mid ├── imagine_dragons-believer-ch4.mp3 ├── imagine_dragons-believer.mid ├── other01.mid ├── test.mid ├── test.mp3 └── visualiztion of midi.PNG /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 exeex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # midi-visualization 2 | A python package for midi-visualization which works with numpy, matplotlib and mido 3 | 4 | ![output image](https://github.com/exeex/midi-visualization/raw/master/test_file/visualiztion%20of%20midi.PNG "output image") 5 | 6 | You must install *numpy*, *matplotlib* and *mido* before you use it. 7 | 8 | If you have a script which parse midi files with mido. 9 | You can just use this: 10 | ``` 11 | from roll import MidiFile 12 | ``` 13 | to replace mido.MidiFile 14 | 15 | 16 | 17 | # How to use it? 18 | Just see the __main__ block in the script 19 | You can just run the script to see how it works 20 | 21 | ``` 22 | if __name__ == "__main__": 23 | mid = MidiFile("test_file/1.mid") 24 | 25 | # get the list of all events 26 | # events = mid.get_events() 27 | 28 | # get the np array of piano roll image 29 | roll = mid.get_roll() 30 | 31 | # draw piano roll by pyplot 32 | mid.draw_roll() 33 | 34 | ``` 35 | -------------------------------------------------------------------------------- /roll.py: -------------------------------------------------------------------------------- 1 | import mido 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import matplotlib as mpl 5 | from matplotlib.colors import colorConverter 6 | 7 | 8 | # inherit the origin mido class 9 | class MidiFile(mido.MidiFile): 10 | 11 | def __init__(self, filename): 12 | 13 | mido.MidiFile.__init__(self, filename) 14 | self.sr = 10 15 | self.meta = {} 16 | self.events = self.get_events() 17 | 18 | def get_events(self): 19 | mid = self 20 | print(mid) 21 | 22 | # There is > 16 channel in midi.tracks. However there is only 16 channel related to "music" events. 23 | # We store music events of 16 channel in the list "events" with form [[ch1],[ch2]....[ch16]] 24 | # Lyrics and meta data used a extra channel which is not include in "events" 25 | 26 | events = [[] for x in range(16)] 27 | 28 | # Iterate all event in the midi and extract to 16 channel form 29 | for track in mid.tracks: 30 | for msg in track: 31 | try: 32 | channel = msg.channel 33 | events[channel].append(msg) 34 | except AttributeError: 35 | try: 36 | if type(msg) != type(mido.UnknownMetaMessage): 37 | self.meta[msg.type] = msg.dict() 38 | else: 39 | pass 40 | except: 41 | print("error",type(msg)) 42 | 43 | return events 44 | 45 | def get_roll(self): 46 | events = self.get_events() 47 | # Identify events, then translate to piano roll 48 | # choose a sample ratio(sr) to down-sample through time axis 49 | sr = self.sr 50 | 51 | # compute total length in tick unit 52 | length = self.get_total_ticks() 53 | 54 | # allocate memory to numpy array 55 | roll = np.zeros((16, 128, length // sr), dtype="int8") 56 | 57 | # use a register array to save the state(no/off) for each key 58 | note_register = [int(-1) for x in range(128)] 59 | 60 | # use a register array to save the state(program_change) for each channel 61 | timbre_register = [1 for x in range(16)] 62 | 63 | 64 | for idx, channel in enumerate(events): 65 | 66 | time_counter = 0 67 | volume = 100 68 | # Volume would change by control change event (cc) cc7 & cc11 69 | # Volume 0-100 is mapped to 0-127 70 | 71 | print("channel", idx, "start") 72 | for msg in channel: 73 | if msg.type == "control_change": 74 | if msg.control == 7: 75 | volume = msg.value 76 | # directly assign volume 77 | if msg.control == 11: 78 | volume = volume * msg.value // 127 79 | # change volume by percentage 80 | # print("cc", msg.control, msg.value, "duration", msg.time) 81 | 82 | if msg.type == "program_change": 83 | timbre_register[idx] = msg.program 84 | print("channel", idx, "pc", msg.program, "time", time_counter, "duration", msg.time) 85 | 86 | 87 | 88 | if msg.type == "note_on": 89 | print("on ", msg.note, "time", time_counter, "duration", msg.time, "velocity", msg.velocity) 90 | note_on_start_time = time_counter // sr 91 | note_on_end_time = (time_counter + msg.time) // sr 92 | intensity = volume * msg.velocity // 127 93 | 94 | 95 | 96 | # When a note_on event *ends* the note start to be play 97 | # Record end time of note_on event if there is no value in register 98 | # When note_off event happens, we fill in the color 99 | if note_register[msg.note] == -1: 100 | note_register[msg.note] = (note_on_end_time,intensity) 101 | else: 102 | # When note_on event happens again, we also fill in the color 103 | old_end_time = note_register[msg.note][0] 104 | old_intensity = note_register[msg.note][1] 105 | roll[idx, msg.note, old_end_time: note_on_end_time] = old_intensity 106 | note_register[msg.note] = (note_on_end_time,intensity) 107 | 108 | 109 | if msg.type == "note_off": 110 | print("off", msg.note, "time", time_counter, "duration", msg.time, "velocity", msg.velocity) 111 | note_off_start_time = time_counter // sr 112 | note_off_end_time = (time_counter + msg.time) // sr 113 | note_on_end_time = note_register[msg.note][0] 114 | intensity = note_register[msg.note][1] 115 | # fill in color 116 | roll[idx, msg.note, note_on_end_time:note_off_end_time] = intensity 117 | 118 | note_register[msg.note] = -1 # reinitialize register 119 | 120 | time_counter += msg.time 121 | 122 | # TODO : velocity -> done, but not verified 123 | # TODO: Pitch wheel 124 | # TODO: Channel - > Program Changed / Timbre catagory 125 | # TODO: real time scale of roll 126 | 127 | # if there is a note not closed at the end of a channel, close it 128 | for key, data in enumerate(note_register): 129 | if data != -1: 130 | note_on_end_time = data[0] 131 | intensity = data[1] 132 | # print(key, note_on_end_time) 133 | note_off_start_time = time_counter // sr 134 | roll[idx, key, note_on_end_time:] = intensity 135 | note_register[idx] = -1 136 | 137 | return roll 138 | 139 | def get_roll_image(self): 140 | roll = self.get_roll() 141 | plt.ioff() 142 | 143 | K = 16 144 | 145 | transparent = colorConverter.to_rgba('black') 146 | colors = [mpl.colors.to_rgba(mpl.colors.hsv_to_rgb((i / K, 1, 1)), alpha=1) for i in range(K)] 147 | cmaps = [mpl.colors.LinearSegmentedColormap.from_list('my_cmap', [transparent, colors[i]], 128) for i in 148 | range(K)] 149 | 150 | for i in range(K): 151 | cmaps[i]._init() # create the _lut array, with rgba values 152 | # create your alpha array and fill the colormap with them. 153 | # here it is progressive, but you can create whathever you want 154 | alphas = np.linspace(0, 1, cmaps[i].N + 3) 155 | cmaps[i]._lut[:, -1] = alphas 156 | 157 | fig = plt.figure(figsize=(4, 3)) 158 | a1 = fig.add_subplot(111) 159 | a1.axis("equal") 160 | a1.set_facecolor("black") 161 | 162 | array = [] 163 | 164 | for i in range(K): 165 | try: 166 | img = a1.imshow(roll[i], interpolation='nearest', cmap=cmaps[i], aspect='auto') 167 | array.append(img.get_array()) 168 | except IndexError: 169 | pass 170 | return array 171 | 172 | def draw_roll(self): 173 | 174 | 175 | roll = self.get_roll() 176 | 177 | # build and set fig obj 178 | plt.ioff() 179 | fig = plt.figure(figsize=(4, 3)) 180 | a1 = fig.add_subplot(111) 181 | a1.axis("equal") 182 | a1.set_facecolor("black") 183 | 184 | # change unit of time axis from tick to second 185 | tick = self.get_total_ticks() 186 | second = mido.tick2second(tick, self.ticks_per_beat, self.get_tempo()) 187 | print(second) 188 | if second > 10: 189 | x_label_period_sec = second // 10 190 | else: 191 | x_label_period_sec = second / 10 # ms 192 | print(x_label_period_sec) 193 | x_label_interval = mido.second2tick(x_label_period_sec, self.ticks_per_beat, self.get_tempo()) / self.sr 194 | print(x_label_interval) 195 | plt.xticks([int(x * x_label_interval) for x in range(20)], [round(x * x_label_period_sec, 2) for x in range(20)]) 196 | 197 | # change scale and label of y axis 198 | plt.yticks([y*16 for y in range(8)], [y*16 for y in range(8)]) 199 | 200 | # build colors 201 | channel_nb = 16 202 | transparent = colorConverter.to_rgba('black') 203 | colors = [mpl.colors.to_rgba(mpl.colors.hsv_to_rgb((i / channel_nb, 1, 1)), alpha=1) for i in range(channel_nb)] 204 | cmaps = [mpl.colors.LinearSegmentedColormap.from_list('my_cmap', [transparent, colors[i]], 128) for i in 205 | range(channel_nb)] 206 | 207 | # build color maps 208 | for i in range(channel_nb): 209 | cmaps[i]._init() 210 | # create your alpha array and fill the colormap with them. 211 | alphas = np.linspace(0, 1, cmaps[i].N + 3) 212 | # create the _lut array, with rgba values 213 | cmaps[i]._lut[:, -1] = alphas 214 | 215 | 216 | # draw piano roll and stack image on a1 217 | for i in range(channel_nb): 218 | try: 219 | a1.imshow(roll[i], origin="lower", interpolation='nearest', cmap=cmaps[i], aspect='auto') 220 | except IndexError: 221 | pass 222 | 223 | # draw color bar 224 | 225 | colors = [mpl.colors.hsv_to_rgb((i / channel_nb, 1, 1)) for i in range(channel_nb)] 226 | cmap = mpl.colors.LinearSegmentedColormap.from_list('my_cmap', colors, 16) 227 | a2 = fig.add_axes([0.05, 0.80, 0.9, 0.15]) 228 | cbar = mpl.colorbar.ColorbarBase(a2, cmap=cmap, 229 | orientation='horizontal', 230 | ticks=list(range(16))) 231 | 232 | # show piano roll 233 | plt.draw() 234 | plt.ion() 235 | plt.show(block=True) 236 | 237 | def get_tempo(self): 238 | try: 239 | return self.meta["set_tempo"]["tempo"] 240 | except: 241 | return 500000 242 | 243 | def get_total_ticks(self): 244 | max_ticks = 0 245 | for channel in range(16): 246 | ticks = sum(msg.time for msg in self.events[channel]) 247 | if ticks > max_ticks: 248 | max_ticks = ticks 249 | return max_ticks 250 | 251 | 252 | if __name__ == "__main__": 253 | mid = MidiFile("test_file/1.mid") 254 | 255 | # get the list of all events 256 | # events = mid.get_events() 257 | 258 | # get the np array of piano roll image 259 | roll = mid.get_roll() 260 | 261 | # draw piano roll by pyplot 262 | mid.draw_roll() 263 | 264 | 265 | -------------------------------------------------------------------------------- /test_file/1.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exeex/midi-visualization/673ad5257fc1bd7c1594d86bfe2952875c8589ef/test_file/1.mid -------------------------------------------------------------------------------- /test_file/imagine_dragons-believer-ch4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exeex/midi-visualization/673ad5257fc1bd7c1594d86bfe2952875c8589ef/test_file/imagine_dragons-believer-ch4.mp3 -------------------------------------------------------------------------------- /test_file/imagine_dragons-believer.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exeex/midi-visualization/673ad5257fc1bd7c1594d86bfe2952875c8589ef/test_file/imagine_dragons-believer.mid -------------------------------------------------------------------------------- /test_file/other01.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exeex/midi-visualization/673ad5257fc1bd7c1594d86bfe2952875c8589ef/test_file/other01.mid -------------------------------------------------------------------------------- /test_file/test.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exeex/midi-visualization/673ad5257fc1bd7c1594d86bfe2952875c8589ef/test_file/test.mid -------------------------------------------------------------------------------- /test_file/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exeex/midi-visualization/673ad5257fc1bd7c1594d86bfe2952875c8589ef/test_file/test.mp3 -------------------------------------------------------------------------------- /test_file/visualiztion of midi.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exeex/midi-visualization/673ad5257fc1bd7c1594d86bfe2952875c8589ef/test_file/visualiztion of midi.PNG --------------------------------------------------------------------------------