├── .gitignore ├── LICENSE ├── README.md ├── demidi.py └── remidi.py /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | .dmypy.json 112 | dmypy.json 113 | 114 | # Pyre type checker 115 | .pyre/ 116 | 117 | .DS_Store 118 | vocab.json 119 | data/ 120 | midi/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Stephanie Wagner 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 | Scripts used to convert midi files to something that text-based neural networks can understand, and vice versa. 2 | 3 | ### How to use 4 | Install [python-midi](https://github.com/vishnubob/python-midi), then flatten midi files with `demidi.py` 5 | ``` 6 | python3 demidi.py --mididir "/path/to/your/midis" --outdir "/path/to/output" 7 | ``` 8 | 9 | Run `remidi.py` to create a midi file that uses the same syntax 10 | ``` 11 | python3 remidi.py --datafile "/path/to/datafile.txt" --outfile "/path/to/outfile.mid" 12 | ``` 13 | 14 | ### Syntax 15 | Let's say you have a `NoteOnEvent` on Track 1 that looks like this. 16 | ``` 17 | NoteOnEvent(tick=8, channel=0, data=[66, 83]) 18 | ``` 19 | It will be converted to this in text format 20 | ``` 21 | 1NoteOnEvent8t0c66d83d 22 | ``` 23 | 24 | Using `--include-resolution` will append the [resolution](https://en.wikipedia.org/wiki/Pulses_per_quarter_note) to the beginning of the file. This is useful if you plan to train with midis that have different resolutions (if you're not sure, try running it and see if the numbers at the beginning of the text files are different). If you don't use that option (e.g. all resolutions are the same), it's recommended you use `--resolution` with `remidi.py` when converting text back to midis. 25 | 26 | ### Samples 27 | 28 | [Here are some samples](https://soundcloud.com/user-122134918/sets/ai-generated-music) that were generated using [textgenrnn](https://github.com/minimaxir/textgenrnn), using different types of songs as training data. 29 | -------------------------------------------------------------------------------- /demidi.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import midi 3 | from midi import * 4 | import os 5 | import json 6 | import sys 7 | import json 8 | 9 | def gen_vocab(tick_max, include_resolutions = False, resolutions = False): 10 | events = [i for i in dir(midi.events) if i.find('Event') >= 0] 11 | vocab = {} 12 | count = 0 13 | 14 | # tracks 15 | for i in range(16): 16 | for e in events: 17 | vocab[str(i) + e] = count 18 | count += 1 19 | 20 | for i in range(tick_max + 1): 21 | vocab[str(i) + 't'] = count 22 | count += 1 23 | 24 | # channels 25 | for i in range(128): 26 | vocab[str(i) + 'c'] = count 27 | count += 1 28 | 29 | # data array 30 | for i in range(128): 31 | vocab[str(i) + 'd'] = count 32 | count += 1 33 | 34 | # resolutions 35 | if include_resolutions: 36 | for r in resolutions: 37 | vocab[str(r) + 'r'] = count 38 | count += 1 39 | 40 | with open('vocab.json', 'w') as f: 41 | f.write(json.dumps(vocab)) 42 | 43 | print("Generated vocab.json in {}".format(os.path.dirname(os.path.realpath('vocab.json')))) 44 | 45 | def demidi(midi_dir, data_dir, include_resolutions, tick_max, generate_vocab): 46 | midifiles = [] 47 | for root, dirs, filenames in os.walk(midi_dir): 48 | for f in filenames: 49 | if f.endswith(".mid"): 50 | midifiles.append(f) 51 | 52 | # 96 is quarter notes, but if giving music with variing notes, do not set this. 53 | masterresolution = 96 54 | # mother 3 music is 24 (and highest tick is only 2489) 55 | # undertale/deltarune is 96 56 | 57 | biggest_tick = 0 58 | fff = '' 59 | goodticks = [] 60 | badsongs = [] 61 | resolutions = [] 62 | events_blacklist = ['CopyrightMetaEvent', 'AbstractEvent', 'MetaEvent', 'MarkerEvent', 'SomethingEvent', 'CuePointEvent'] 63 | 64 | for midifile in midifiles: 65 | try: 66 | pattern = midi.read_midifile("{}/{}".format(midi_dir, midifile)) 67 | except: 68 | continue 69 | 70 | resolutions.append(pattern.resolution) 71 | count = 0 72 | words = [] 73 | word = "" 74 | write_file = True 75 | for p in pattern: 76 | for e in p: 77 | if e.__class__.__name__ not in events_blacklist: 78 | word = "" 79 | word += str(count) + e.__class__.__name__.lower() 80 | word += str(e.tick) + 't' 81 | 82 | if tick_max >= 0: 83 | if e.tick >= tick_max: 84 | badsongs.append(midifile) 85 | else: 86 | goodticks.append(e.tick) 87 | 88 | if e.tick > biggest_tick: 89 | biggest_tick = e.tick 90 | 91 | if e.__class__.__name__ not in ['InstrumentNameEvent', 'MarkerEvent', 'TrackNameEvent', 'TimeSignatureEvent', 'SetTempoEvent', 'EndOfTrackEvent', 'CopyrightMetaEvent', 'KeySignatureEvent', 'SmpteOffsetEvent']: 92 | word += str(e.channel) + 'c' 93 | else: 94 | print(e) 95 | 96 | for d in e.data: 97 | word += str(d) + 'd' 98 | words.append(word) 99 | count += 1 100 | 101 | if midifile not in badsongs or tick_max < 0: 102 | with open("{}/{}.txt".format(data_dir, midifile), 'w') as f: 103 | if include_resolutions: 104 | f.write("{} {}".format(pattern.resolution, " ".join(words))) 105 | else: 106 | f.write(" ".join(words)) 107 | 108 | resolutions = list(set(resolutions)) 109 | 110 | print("Largest tick found: {}\nResolutions found: {}".format(biggest_tick, resolutions)) 111 | 112 | if generate_vocab: 113 | gen_vocab(biggest_tick, include_resolutions=include_resolutions, resolutions=resolutions) 114 | 115 | def main(args): 116 | if args.midi_dir is None or args.data_dir is None: 117 | parser.print_help() 118 | return 0 119 | 120 | demidi(args.midi_dir, args.data_dir, args.include_resolutions, args.tick_max, args.generate_vocab) 121 | 122 | if __name__ == "__main__": 123 | parser = argparse.ArgumentParser(description='Flatten midi files and generate vocab for training neural networks. More info at https://github.com/stephwag/midi-rnn') 124 | parser.add_argument('--mididir', metavar='-M', dest='midi_dir', default=None, 125 | help='Absolute path to data directory') 126 | parser.add_argument('--outdir', metavar='-O', dest='data_dir', default=None, 127 | help='Absolute path to output directory') 128 | 129 | parser.add_argument('--include-resolution', action='store_true', dest='include_resolutions', help='Midi resolution of the out file (default: 96)') 130 | parser.add_argument('--vocab', dest='generate_vocab', action='store_true', help='Generate vocab (default: false)') 131 | 132 | parser.add_argument('--tickmax', metavar='-T', dest='tick_max', nargs=1, default=-1, type=int, 133 | help='Only process midis that are less or equal to this value. The lower the value, the lower the size of the vocab (default: no max)') 134 | 135 | args = parser.parse_args() 136 | main(args) 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /remidi.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import midi 3 | from midi import * 4 | import os 5 | from random import randint 6 | import json 7 | import sys 8 | 9 | 10 | 11 | def get_pair(text): 12 | number = int(''.join(filter(str.isdigit, text))) 13 | l = len(str(number)) 14 | return (number, text[l:]) 15 | 16 | def remidi(data_file, out_file, resolution=96): 17 | events_list = [i for i in dir(midi.events) if i.find('Event') >= 0] 18 | events_hash = {} 19 | for evt in events_list: 20 | events_hash[evt.lower()] = evt 21 | 22 | with open(data_file, 'r') as f: 23 | textdata = f.read().split(" ") 24 | 25 | pattern = midi.Pattern() 26 | start_index = 0 27 | 28 | try: 29 | pattern.resolution = int(textdata[0]) 30 | start_index = 1 31 | except: 32 | pattern.resolution = resolution 33 | 34 | tracks = {} 35 | 36 | for index in range(start_index, len(textdata)): 37 | 38 | t = textdata[index] 39 | result = t 40 | 41 | params = {} 42 | 43 | tick = None 44 | channel = None 45 | data = [] 46 | 47 | i = t.find('event') + 5 48 | 49 | etext = t[:i] 50 | ev_num, ev = get_pair(etext) 51 | print(ev) 52 | c = getattr(midi, events_hash[ev]) 53 | t = t[i:] 54 | 55 | params["track"] = ev_num 56 | params["event"] = ev 57 | params["class"] = c 58 | 59 | if t.find('t') >= 0 and len(t) > 0: 60 | ttext = t[:t.find('t')] 61 | if len(ttext) > 0: 62 | params['tick'] = get_pair(ttext)[0] 63 | t = t[t.find('t'):] 64 | 65 | if t.find('c') >= 0 and len(t) > 0: 66 | ctext = t[:t.find('c')] 67 | if len(ctext) > 0: 68 | params['channel'] = get_pair(ctext)[0] 69 | t = t[t.find('c'):] 70 | 71 | if t.find('d') >= 0 and len(t) > 0: 72 | params['data'] = [] 73 | ds = t.split('d') 74 | for dd in ds: 75 | if len(dd) > 0: 76 | params['data'].append(get_pair(dd)[0]) 77 | 78 | if ev_num not in tracks: 79 | tracks[ev_num] = [ params ] 80 | else: 81 | tracks[ev_num].append(params) 82 | 83 | keys = sorted(tracks.keys()) 84 | for k in keys: 85 | trk = midi.Track() 86 | tname_param = None 87 | for pm in tracks[k]: 88 | if pm['class'].__name__ == 'TrackNameEvent': 89 | klass = pm['class'] 90 | trk.append(TrackNameEvent(tick=pm["tick"], text='TRACK ' + str(k), data=pm["data"])) 91 | 92 | for pm in tracks[k]: 93 | if pm['class'].__name__ != 'TrackNameEvent' and pm['class'].__name__ != 'CopyrightMetaEvent': 94 | klass = pm['class'] 95 | trk.append(klass(**pm)) 96 | pattern.append(trk) 97 | print(pattern) 98 | 99 | midi.write_midifile(out_file, pattern) 100 | 101 | def main(args): 102 | if args.data_file is None or args.out_file is None: 103 | parser.print_help() 104 | return 0 105 | 106 | remidi(args.data_file, args.out_file, args.resolution) 107 | 108 | 109 | if __name__ == "__main__": 110 | parser = argparse.ArgumentParser(description='Create midi files from data that use demidi.py syntax. More info at https://github.com/stephwag/midi-rnn') 111 | parser.add_argument('--datafile', metavar='-M', dest='data_file', default=None, 112 | help='Path to a single data file (must be an absolute path)') 113 | 114 | parser.add_argument('--outfile', metavar='-O', dest='out_file', default=None, help='Output file path (must be an absolute path)') 115 | parser.add_argument('--resolution', metavar='-R', dest='resolution', default=96, type=int, help='Midi resolution of the out file (default: 96)') 116 | 117 | args = parser.parse_args() 118 | main(args) 119 | 120 | 121 | 122 | 123 | --------------------------------------------------------------------------------