├── requirements.txt ├── FurElise.mid ├── .gitignore ├── README.md ├── LICENSE ├── misc └── IDE files │ └── python_synthesizer.wpr └── synthesizer.py /requirements.txt: -------------------------------------------------------------------------------- 1 | sounddevice 2 | numpy 3 | mido -------------------------------------------------------------------------------- /FurElise.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cool-RR/python_synthesizer/HEAD/FurElise.mid -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | __pycache__/ 3 | 4 | .tox/ 5 | .pytest_cache/ 6 | .mypy_cache/ 7 | 8 | dist/ 9 | build/ 10 | *.egg-info/ 11 | 12 | *.bak 13 | 14 | *.wpu 15 | 16 | .coverage 17 | htmlcov 18 | 19 | *.wav 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Develop a music synthesizer in Python # 2 | 3 | This repo is used in my talk, "Live-coding a music synthesizer". 4 | 5 | It shows step-by-step how to develop a Python program that synthesizes music and plays a midi file. Follow the commits from the very first commit to the last to see the different steps. 6 | 7 | # License # 8 | 9 | Copyright (c) 2019 Ram Rachum, released under the MIT license. 10 | 11 | I give [Python workshops](http://pythonworkshops.co/) to teach people 12 | Python and related topics. 13 | 14 | 15 | Copyright information 16 | --------------------- 17 | 18 | FurElise.mid, copyright Yassiiiiine, license CC-ASA 4 International, URL: https://commons.wikimedia.org/wiki/File:Für_Elise_(1810),_composed_by_Ludwig_van_Beethoven_-_piano_music.mid -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ram Rachum 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 | -------------------------------------------------------------------------------- /misc/IDE files/python_synthesizer.wpr: -------------------------------------------------------------------------------- 1 | #!wing 2 | #!version=7.0 3 | ################################################################## 4 | # Wing project file # 5 | ################################################################## 6 | [project attributes] 7 | debug.launch-configs = (2, 8 | {'launch-NKQAnPWZ0XaMQEYg': ({}, 9 | {'buildcmd': ('project', 10 | None), 11 | 'env': ('project', 12 | [u'']), 13 | 'name': 'foo', 14 | 'pyexec': ('custom', 15 | u'c:\\Users\\Administrator\\Dropbox\\Desktop\\local\\foo\\Scripts\\python.exe'), 16 | 'pypath': ('project', 17 | []), 18 | 'pyrunargs': ('project', 19 | '-u'), 20 | 'runargs': u'install', 21 | 'rundir': ('project', 22 | u'')})}) 23 | proj.directory-list = [{'dirloc': loc('../..'), 24 | 'excludes': [u'synthesizer.egg-info', 25 | u'dist', 26 | u'build'], 27 | 'filter': '*', 28 | 'include_hidden': False, 29 | 'recursive': True, 30 | 'watch_for_changes': True}] 31 | proj.file-type = 'shared' 32 | proj.home-dir = loc('../..') 33 | proj.launch-config = {loc('../../../../../../../Program Files/Python37/Scripts/pasteurize-script.py'): ('p'\ 34 | 'roject', 35 | (u'"c:\\Users\\Administrator\\Documents\\Python Projects\\PySnooper\\pysnooper" "c:\\Users\\Administrator\\Documents\\Python Projects\\PySnooper\\tests"', 36 | '')), 37 | loc('../../../../../Dropbox/Desktop/local/foo/tits/numpy-1.17.0/setup.py'): ('c'\ 38 | 'ustom', 39 | (u'install', 40 | 'launch-NKQAnPWZ0XaMQEYg')), 41 | loc('../../../../../Dropbox/Scripts and shortcuts/_simplify3d_add_m600.py'): ('p'\ 42 | 'roject', 43 | (u'"C:\\Users\\Administrator\\Dropbox\\Desktop\\foo.gcode"', 44 | ''))} 45 | proj.main-file = loc('../../synthesizer.py') 46 | testing.auto-test-file-specs = (('regex', 47 | 'pysnooper/tests.*/test[^./]*.py.?$'),) 48 | testing.test-framework = {None: ':internal pytest'} 49 | -------------------------------------------------------------------------------- /synthesizer.py: -------------------------------------------------------------------------------- 1 | #!pypy3 2 | import math 3 | import random 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | import sounddevice 8 | import mido 9 | 10 | 11 | MASTER_VOLUME = 0.01 12 | HALF_LIFE = 0.3 13 | INITIAL_DELAY = 0.2 14 | 15 | overtones = {i: 1 / (i ** 1.5) for i in range(1, 8)} 16 | 17 | class Audio: 18 | def play(self, samplerate=8000): 19 | n_delay_samples = int(INITIAL_DELAY * samplerate) 20 | time_array = np.arange(0, self.length, 1 / samplerate) 21 | pressure_array = np.zeros(n_delay_samples + len(time_array)) 22 | sounddevice.play(pressure_array, samplerate=samplerate) 23 | for i, t in enumerate(time_array, start=n_delay_samples): 24 | pressure_array[i] = self.get_pressure(t.item()) 25 | sounddevice.wait() 26 | 27 | class Note(Audio): 28 | def __init__(self, frequency, volume=1): 29 | self.frequency = frequency 30 | self.volume = volume 31 | self.length = 1.5 32 | 33 | def get_pressure(self, t): 34 | if 0 <= t <= self.length: 35 | result = 0 36 | for overtone, overtone_volume in overtones.items(): 37 | result += overtone_volume * math.sin( 38 | math.tau * t * self.frequency * overtone 39 | ) 40 | volume = MASTER_VOLUME * self.volume * 2 ** (-t / HALF_LIFE) 41 | return result * volume 42 | else: 43 | return 0 44 | 45 | class Sequence(Audio): 46 | def __init__(self, offsets_and_notes): 47 | self.offsets_and_notes = tuple(offsets_and_notes) 48 | self.length = max(offset + note.length for offset, note 49 | in self.offsets_and_notes) 50 | 51 | def get_pressure(self, t): 52 | return sum(note.get_pressure(t - offset) for offset, note in 53 | self.offsets_and_notes) 54 | 55 | 56 | class MidiSequence(Sequence): 57 | def __init__(self, path): 58 | offsets_and_notes = [] 59 | 60 | current_time = 0 61 | for message in mido.MidiFile(path): 62 | current_time += message.time 63 | if message.type != 'note_on': 64 | continue 65 | offsets_and_notes.append(( 66 | current_time, 67 | Note( 68 | 440 * 2 ** ((message.note - 69) / 12), 69 | message.velocity / 127 70 | ) 71 | )) 72 | 73 | Sequence.__init__(self, offsets_and_notes) 74 | 75 | 76 | if __name__ == '__main__': 77 | midi_sequence = MidiSequence('FurElise.mid') 78 | midi_sequence.play() 79 | --------------------------------------------------------------------------------