├── .gitignore ├── README.md ├── main.py ├── masterpiece.py ├── randomnote.py ├── rules.json └── song_settings.json /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 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 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # output files 107 | output/ 108 | 109 | # PyCharm 110 | .idea/ 111 | 112 | # macOS General 113 | .DS_Store 114 | .AppleDouble 115 | .LSOverride 116 | 117 | # Thumbnails 118 | ._* 119 | 120 | # Files that might appear in the root of a volume 121 | .DocumentRevisions-V100 122 | .fseventsd 123 | .Spotlight-V100 124 | .TemporaryItems 125 | .Trashes 126 | .VolumeIcon.icns 127 | .com.apple.timemachine.donotpresent 128 | 129 | # Directories potentially created on remote AFP share 130 | .AppleDB 131 | .AppleDesktop 132 | Network Trash Folder 133 | Temporary Items 134 | .apdisk 135 | 136 | # Windows thumbnail cache files 137 | Thumbs.db 138 | ehthumbs.db 139 | ehthumbs_vista.db 140 | 141 | # Folder config file 142 | [Dd]esktop.ini 143 | 144 | # Recycle Bin used on file shares 145 | $RECYCLE.BIN/ 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Random Melody Generator 2 | 3 | Generate random melody under specific rules. 4 | 5 | ## Getting started 6 | ### Prerequisites 7 | This project is written in [Python](https://www.python.org/) 3.6. I haven't tested in other versions. 8 | 9 | [MIDIUtil](https://github.com/MarkCWirt/MIDIUtil) library is used in my project. You can install it simply by using `pip`: 10 | 11 | ```console 12 | pip install MIDIUtil 13 | ``` 14 | 15 | ### How to use? 16 | Simply run `main.py`. A standard MIDI file will be generated in `output` folder. The filename is `midi_{timestamp}.mid`. 17 | 18 | You can play it using a media player (e.g. Windows Media Player) or import it into a Digital Audio Workstation (DAW). 19 | 20 | ## Customize 21 | By default, the melody is in major pentatonic scale, chord progression is C-Am-F-G (1-6-4-5), and percussion pattern is fixed. Also, the interval of notes within a phrase is constrained. 22 | 23 | The rules can be modified in `rules.json`. 24 | 25 | You can also adjust the length of song and tempo in `song_settings.json`. 26 | 27 | ### rules.json 28 | 29 | | Parameter | Description | 30 | | ------------- | ------------- | 31 | | `notes` | Defines the notes used for composition. | 32 | | `interval_upper` | Defines the upper bound of interval of notes. The values in this list will be randomly chosen. | 33 | | `interval_lower` | Defines the lower bound of interval of notes. The values in this list will be randomly chosen. | 34 | | `rhythm` | Defines rhythm pattern for melody. The numbers are in beats (quarter notes). | 35 | | `seq_chord` | Defines the chord sequence. Notes in each sub-array will be played simultaneously to form a chord. | 36 | | `seq_perc` | Defines the percussion sequence. The first element in the sub-array denotes the drum sound, and the second element denotes time value in beats. | 37 | | `velocity` | Defines the velocity of strong, intermediate and weak beats. | 38 | 39 | ### song_settings.json 40 | 41 | | Parameter | Description | 42 | | ------------- | ------------- | 43 | | `length` | Defines the length of song. | 44 | | `tempo` | Defines the tempo of song, measured in Beats per Minute (BPM). | 45 | 46 | ## Credits 47 | - The whole project is written in Python ([python.org](https://www.python.org/)). 48 | - The project uses [MIDIUtil](https://github.com/MarkCWirt/MIDIUtil) library by [MarkCWirt](https://github.com/MarkCWirt). 49 | - I would appreciate [ScoreDraft](https://github.com/fynv/ScoreDraft) by [fynv](https://github.com/fynv). My project is partially inspired by ScoreDraft. Since ScoreDraft uses scripts to compose music, I found its potential to automatically generate music. My algorithm of randomly generating melody was initially tested on ScoreDraft. 50 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import json 6 | import time 7 | from datetime import datetime 8 | 9 | from masterpiece import Masterpiece 10 | 11 | 12 | if __name__ == "__main__": 13 | dtime = datetime.now() 14 | ans_time = time.mktime(dtime.timetuple()) 15 | params_file = open("song_settings.json", "r") 16 | params = json.load(params_file) 17 | params_file.close() 18 | my_masterpiece = Masterpiece( 19 | rules_path="rules.json", 20 | length=params["length"], 21 | tempo=params["tempo"]) 22 | subfolder = "output" 23 | if not os.path.isdir(subfolder): 24 | os.mkdir(subfolder) 25 | my_masterpiece.create_midi_file("{folder}/midi_{suffix}.mid".format( 26 | folder=subfolder, 27 | suffix=ans_time)) 28 | -------------------------------------------------------------------------------- /masterpiece.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from midiutil.MidiFile import MIDIFile 6 | 7 | from randomnote import RandomNote 8 | 9 | 10 | class Masterpiece(object): 11 | def __init__(self, rules_path="rules.json", length=4, tempo=90): 12 | self.rules_path = rules_path 13 | self.length = length 14 | self.tempo = tempo 15 | 16 | rules_file = open(rules_path, "r") 17 | rules = json.load(rules_file) 18 | rules_file.close() 19 | self.rhythm = rules["rhythm"] 20 | self.seq_chord = rules["seq_chord"] 21 | self.seq_perc = rules["seq_perc"] 22 | self.velocity = rules["velocity"] 23 | self.rn = RandomNote(rules["notes"], rules["interval_upper"], rules["interval_lower"]) 24 | 25 | self.MyMIDI = MIDIFile(3) 26 | self.current_track_number = 0 27 | 28 | def create_melody_sequence(self): 29 | seq_melody = [] 30 | for i in range(self.length): 31 | for phrase in self.rhythm: 32 | self.rn.reset() 33 | for duration in phrase: 34 | seq_melody.append((self.rn.random_note(), duration)) 35 | return seq_melody 36 | 37 | def create_melody_track(self): 38 | seq_melody = self.create_melody_sequence() 39 | 40 | self.MyMIDI.addTrackName( 41 | track=self.current_track_number, 42 | time=0, trackName="piano") 43 | self.MyMIDI.addTempo( 44 | track=self.current_track_number, 45 | time=0, tempo=self.tempo) 46 | self.MyMIDI.addProgramChange( 47 | tracknum=self.current_track_number, 48 | channel=0, time=0, program=0) 49 | 50 | pos = 0 51 | for pitch, duration in seq_melody: 52 | relative_pos = pos - int(pos / 4) * 4 53 | if 0 <= relative_pos < 1: 54 | vol = self.velocity["strong"] 55 | elif 2 <= relative_pos < 3: 56 | vol = self.velocity["intermediate"] 57 | else: 58 | vol = self.velocity["weak"] 59 | self.MyMIDI.addNote( 60 | track=self.current_track_number, 61 | channel=0, pitch=pitch, time=pos, duration=duration, volume=vol) 62 | if relative_pos in [0, 2]: 63 | self.MyMIDI.addControllerEvent( 64 | track=self.current_track_number, 65 | channel=0, time=pos, controller_number=64, parameter=127) 66 | self.MyMIDI.addControllerEvent( 67 | track=self.current_track_number, 68 | channel=0, time=pos + 1.96875, controller_number=64, parameter=0) 69 | pos += duration 70 | self.current_track_number += 1 71 | 72 | def create_chord_track(self): 73 | self.MyMIDI.addTrackName( 74 | track=self.current_track_number, 75 | time=0, trackName="chords") 76 | self.MyMIDI.addTempo( 77 | track=self.current_track_number, 78 | time=0, tempo=self.tempo) 79 | self.MyMIDI.addProgramChange( 80 | tracknum=self.current_track_number, 81 | channel=0, time=0, program=0) 82 | 83 | # C D E F G A B | C D E F G A B | C 84 | # 48 50 52 53 55 57 59 | 60 62 64 65 67 69 71 | 72 85 | 86 | pos = 0 87 | while pos < self.length * 16: 88 | for item in self.seq_chord: 89 | for pitch in item: 90 | self.MyMIDI.addControllerEvent( 91 | track=self.current_track_number, 92 | channel=0, time=pos, controller_number=64, parameter=127) 93 | self.MyMIDI.addControllerEvent( 94 | track=self.current_track_number, 95 | channel=0, time=pos + 1.96875, controller_number=64, parameter=0) 96 | self.MyMIDI.addNote( 97 | track=self.current_track_number, 98 | channel=0, pitch=pitch, time=pos, duration=2, volume=76) 99 | self.MyMIDI.addControllerEvent( 100 | track=self.current_track_number, 101 | channel=0, time=pos + 2, controller_number=64, parameter=127) 102 | self.MyMIDI.addControllerEvent( 103 | track=self.current_track_number, 104 | channel=0, time=pos + 3.96875, controller_number=64, parameter=0) 105 | self.MyMIDI.addNote( 106 | track=self.current_track_number, 107 | channel=0, pitch=pitch, time=pos + 2, duration=2, volume=68) 108 | pos += 4 109 | self.current_track_number += 1 110 | 111 | def create_perc_track(self): 112 | self.MyMIDI.addTrackName( 113 | track=self.current_track_number, 114 | time=0, trackName="perc") 115 | self.MyMIDI.addTempo( 116 | track=self.current_track_number, 117 | time=0, tempo=self.tempo) 118 | self.MyMIDI.addProgramChange( 119 | tracknum=self.current_track_number, 120 | channel=9, time=0, program=0) 121 | 122 | pos = 0 123 | while pos < self.length * 16: 124 | if pos != 0: 125 | self.MyMIDI.addNote( 126 | track=self.current_track_number, 127 | channel=9, pitch=49, time=pos, duration=0.5, volume=102) 128 | for pitch, duration in self.seq_perc: 129 | relative_pos = pos - int(pos / 4) * 4 130 | if 0 <= relative_pos < 1: 131 | vol = 102 132 | elif 2 <= relative_pos < 3: 133 | vol = 96 134 | else: 135 | vol = 92 136 | self.MyMIDI.addNote( 137 | track=self.current_track_number, 138 | channel=9, pitch=pitch, time=pos, duration=duration, volume=vol) 139 | pos += duration 140 | self.current_track_number += 1 141 | 142 | def create_midi_file(self, filename, melody=True, chord=True, perc=True): 143 | if melody: 144 | self.create_melody_track() 145 | if chord: 146 | self.create_chord_track() 147 | if perc: 148 | self.create_perc_track() 149 | with open(filename, "wb") as midi_file: 150 | self.MyMIDI.writeFile(midi_file) 151 | -------------------------------------------------------------------------------- /randomnote.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import random 4 | 5 | 6 | class RandomNote(object): 7 | def __init__(self, choose_from, interval_upper, interval_lower): 8 | self.last_played = 0 9 | self.notes = choose_from 10 | self.interval_upper = interval_upper 11 | self.interval_lower = interval_lower 12 | 13 | def random_note(self): 14 | while True: 15 | note = random.choice(range(1, len(self.notes) + 1)) 16 | if not self.last_played: 17 | break 18 | else: 19 | # 音程限制 20 | if random.choice(self.interval_upper) \ 21 | >= abs(note - self.last_played) \ 22 | >= random.choice(self.interval_lower): 23 | break 24 | else: 25 | continue 26 | self.last_played = note 27 | return self.notes[self.last_played - 1] 28 | 29 | def reset(self): 30 | self.last_played = 0 31 | -------------------------------------------------------------------------------- /rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "notes": [72, 74, 76, 79, 81, 84, 86], 3 | "interval_upper": [1, 1, 1, 1, 1, 1, 1, 1, 2, 2], 4 | "interval_lower": [1, 1, 1, 1, 1, 1, 1, 1, 0], 5 | "rhythm": [ 6 | [0.5, 0.5, 0.5, 0.25, 0.25, 0.5, 0.5, 1.0], 7 | [0.5, 0.5, 1.0, 0.5, 0.5, 1.0], 8 | [0.5, 0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 1.0], 9 | [0.5, 0.5, 1.0, 0.25, 0.25, 0.5, 1.0] 10 | ], 11 | "seq_chord": [ 12 | [52, 55, 60], 13 | [52, 57, 60], 14 | [53, 57, 60], 15 | [55, 59, 62] 16 | ], 17 | "seq_perc": [ 18 | [36, 0.5], [42, 0.5], [38, 0.5], [42, 0.5], [36, 0.5], [42, 0.5], [38, 0.5], [42, 0.5], 19 | [36, 0.5], [42, 0.5], [38, 0.5], [42, 0.5], [36, 0.5], [36, 0.5], [38, 0.5], [42, 0.5], 20 | [36, 0.5], [42, 0.5], [38, 0.5], [42, 0.5], [36, 0.5], [42, 0.5], [38, 0.5], [42, 0.5], 21 | [36, 0.5], [42, 0.5], [38, 0.5], [42, 0.5], [36, 0.5], [36, 0.5], [38, 0.5], [38, 0.25], [38, 0.25] 22 | ], 23 | "velocity": { 24 | "strong": 102, 25 | "intermediate": 96, 26 | "weak": 92 27 | } 28 | } -------------------------------------------------------------------------------- /song_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "length": 8, 3 | "tempo": 90 4 | } --------------------------------------------------------------------------------