├── LICENSE ├── README.md ├── SuperMario.txt ├── keyboard_synth.py ├── noteslist.txt ├── python synth.png ├── replay_synth.py └── tracks_synth.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 FinFET 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 | # Python_Synth 2 | A simple synthesizer made with Pygame and Numpy in Python. 3 | 4 | ![Screenshot](python%20synth.png) 5 | 6 | ## A simple keyboard synthesizer 7 | ### What is a soundwave 8 | Soundwaves are pressure fluctuations which travel through the air (or other physical medium) and hit your eardrums. We can generate these waves with a speaker, which usually consists of a diaphragm with an electrical coil attached and a permanent magnet. When an electrical signal passes through the coil, it vibrates the diaphragm, which in turn moves the air around it creating soundwaves. 9 | 10 | The electrical signal consists of an alternating current, usually created by a DAC - Digital Analog Converter and amplified by a amplifier. Before that, the signal is digital, consisting of ones and zeros in your computer. 11 | 12 | And what does this digital signal look like? Basically, it is a long list of numbers. 13 | 14 | ### Generating a digital signal 15 | The first thing we should consider when generating a digital signal is the sampling rate, that is, how many values we need to define for a second of sound. The default value for the Pygame mixer is 44100 samples per second, so that's what I'll be using. 16 | 17 | The second thing is the form of the wave, responsible for the quality of the sound, or timbre, the reason why different instruments sound so dissimilar for the same frequency or pitch. The most pure waveform is the sine, also one of the easiest to generate in numpy, but the are innumerous other types, like square, triangular and sawtooth. 18 | 19 | We will start with the sine wave, which will actually be generated by the cosine function (makes things easier for triangular waves later). Cosine is equal to the sine but with a phase shift, which is not relevant in this context, it sounds exactly the same. 20 | 21 | To generate the array of values of a sine wave we need the sampling rate, 44100, the frequency, which can be any value lower than 22.5 kHz by the [Nyquist frequency](https://en.wikipedia.org/wiki/Nyquist_frequency) (most people can't hear anything above 16 or 17 kHz anyway) and the duration for the sound sample. 22 | 23 | With the duration and the sampling rate we can calculate the number of frames that the sample will have. With the number of frames and the duration we can generate an array with the timings of each frame, which in turn is fed into the cosine function multiplied by 2π and the frequency, this results in an array with all values of the sound signal. 24 | 25 | To hear it, first we have to turn it into a pygame sound array, which first has to be multiplied by the value of 32767, duplicated (for stereo mixer), transposed and turned into the int16 type. Then we can use the function `make_sound` from pygame sndarray, the `.copy()` is necessary to make the array contiguous in memory. After that we can finally play the sound, careful with the volume, it will be at the maximum! After that we simply wait for the duration of the sample and exit pygame. 26 | 27 |
28 | Generating the first sound sample 29 | 30 | ```python 31 | import pygame as pg 32 | import numpy as np 33 | 34 | pg.init() 35 | pg.mixer.init() 36 | 37 | sampling_rate = 44100 # default value for the pygame mixer 38 | frequency = 440 # [Hz] 39 | duration = 1.5 # [s] 40 | frames = int(duration*sampling_rate) 41 | arr = np.cos(2*np.pi*frequency*np.linspace(0,duration, frames)) 42 | sound = np.asarray([32767*arr,32767*arr]).T.astype(np.int16) 43 | sound = pg.sndarray.make_sound(sound.copy()) 44 | sound.play() 45 | ``` 46 |
47 | 48 | Great! Now we can do the same for all the notes on a piano keyboard. 49 | 50 | ### Generating samples for every key in a piano 51 | 52 | But wait, what are notes anyway? Simply put, notes are selected frequencies which often sound nice when played together. This may sound a bit weird, but the exact frequencies aren't that important, what matters most are the ratios between them. The most used ratio in western music is the [Twelfth root of two](https://en.wikipedia.org/wiki/Twelfth_root_of_two). 53 | 54 | So, to generate samples for all the keys in a piano we just need a [list of all the notes](https://en.wikipedia.org/wiki/Piano_key_frequencies), conveniently I have listed them all in a text file: [noteslist.txt](https://github.com/FinFetChannel/Python_Synth/blob/main/noteslist.txt). Then we just need the frequency of the first note (16.35160 Hz) and the remaining frequencies can be calculated from it. 55 | 56 | So, we can easily store a sample for each note in a dictionary. For the keys, we are going to use the characters in a regular keyboard, after all, that's what we have to play here. The 108 keys can be subdivided into three groups of 36, since your keyboard probably does not have enough keys for all of them at once. 57 | 58 |
59 | Generating a sample for each piano key 60 | 61 | ```python 62 | import pygame as pg 63 | import numpy as np 64 | 65 | pg.init() 66 | pg.mixer.init() 67 | 68 | def synth(frequency, duration=1.5, sampling_rate=44100): 69 | frames = int(duration*sampling_rate) 70 | arr = np.cos(2*np.pi*frequency*np.linspace(0,duration, frames)) 71 | sound = np.asarray([32767*arr,32767*arr]).T.astype(np.int16) 72 | sound = pg.sndarray.make_sound(sound.copy()) 73 | 74 | return sound 75 | 76 | 77 | keylist = '123456789qwertyuioasdfghjklzxcvbnm,.' 78 | notes_file = open("noteslist.txt") 79 | file_contents = notes_file.read() 80 | notes_file.close() 81 | noteslist = file_contents.splitlines() 82 | 83 | keymod = '0-=' 84 | notes = {} # dict to store samples 85 | freq = 16.3516 # start frequency 86 | 87 | for i in range(len(noteslist)): 88 | mod = int(i/36) 89 | key = keylist[i-mod*36]+str(mod) 90 | sample = synth(freq) 91 | notes[key] = [sample, noteslist[i], freq] 92 | notes[key][0].set_volume(0.33) 93 | notes[key][0].play() 94 | notes[key][0].fadeout(100) 95 | pg.time.wait(100) 96 | freq = freq * 2 ** (1/12) 97 | 98 | pg.mixer.quit() 99 | pg.quit() 100 | 101 | ``` 102 |
103 | 104 | ### Playing with the keyboard 105 | 106 | Now that we have all the samples we can start playing and try to make some music. For that we need to create a pygame window, so we can capture the keystrokes and play the corresponding samples. A note starts playing when a keydown event is registered and stops after the duration of the sample or when a keyup event is registered. The range of notes can be changed with the keys 0 - = 107 | 108 |
109 | Keyboard inputs 110 | 111 | ```python 112 | 113 | ... 114 | 115 | screen = pg.display.set_mode((1280, 720)) 116 | running = 1 117 | 118 | while running: 119 | for event in pg.event.get(): 120 | if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE): 121 | running = False 122 | if event.type == pg.KEYDOWN: 123 | key = str(event.unicode) 124 | if key in keymod: 125 | mod = keymod.index(str(event.unicode)) 126 | elif key in keylist: 127 | key = key+str(mod) 128 | notes[key][0].play() 129 | if event.type == pg.KEYUP and str(event.unicode) != '' and str(event.unicode) in keylist: 130 | key = str(event.unicode)+str(mod) 131 | notes[key][0].fadeout(100) 132 | 133 | pg.mixer.quit() 134 | pg.quit() 135 | 136 | ``` 137 |
138 | 139 | Ok, this works, but this black screen is boring, why not put it to good use? 140 | 141 | ### Displaying notes 142 | 143 | To display the notes on screen, first I'm going to define a position and a color for each one. For the positions I've simply arranged all the notes in a grid of 12 by 9, neatly spaced on the screen. For the colors I tried to mimic a rainbow, where lower sound frequencies are reddish, the middle ones are greenish and the higher ones are blueish. The positions and colors are also stored in the notes dictionary. The notes are then blit into the screen. When playing, the current note gets highlighted with a white color, after the keyup event it returns to its original color. After some adjustments we have a very basic and somewhat pretty sound synthesizer. 144 | 145 |
146 | Notes display 147 | 148 | ```python 149 | 150 | ... 151 | 152 | import pygame as pg 153 | import numpy as np 154 | 155 | pg.init() 156 | pg.mixer.init() 157 | screen = pg.display.set_mode((1280, 720)) 158 | font = pg.font.SysFont("Impact", 48) 159 | 160 | def synth(frequency, duration=1.5, sampling_rate=44100): 161 | frames = int(duration*sampling_rate) 162 | arr = np.cos(2*np.pi*frequency*np.linspace(0,duration, frames)) 163 | sound = np.asarray([32767*arr,32767*arr]).T.astype(np.int16) 164 | sound = pg.sndarray.make_sound(sound.copy()) 165 | 166 | return sound 167 | 168 | 169 | keylist = '123456789qwertyuioasdfghjklzxcvbnm,.' 170 | notes_file = open("noteslist.txt") 171 | file_contents = notes_file.read() 172 | notes_file.close() 173 | noteslist = file_contents.splitlines() 174 | 175 | keymod = '0-=' 176 | notes = {} # dict to store samples 177 | freq = 16.3516 # start frequency 178 | posx, posy = 25, 25 #start position 179 | 180 | 181 | for i in range(len(noteslist)): 182 | mod = int(i/36) 183 | key = keylist[i-mod*36]+str(mod) 184 | sample = synth(freq) 185 | color = np.array([np.sin(i/25+1.7)*130+125,np.sin(i/30-0.21)*215+40, np.sin(i/25+3.7)*130+125]) 186 | color = np.clip(color, 0, 255) 187 | notes[key] = [sample, noteslist[i], freq, (posx, posy), 255*color/max(color)] 188 | notes[key][0].set_volume(0.33) 189 | notes[key][0].play() 190 | notes[key][0].fadeout(100) 191 | freq = freq * 2 ** (1/12) 192 | posx = posx + 140 193 | if posx > 1220: 194 | posx, posy = 25, posy+56 195 | 196 | screen.blit(font.render(notes[key][1], 0, notes[key][4]), notes[key][3]) 197 | pg.display.update() 198 | 199 | 200 | running = 1 201 | mod = 1 202 | pg.display.set_caption("FinFET Synth - Change range: 0 - = // Play with keys: "+keylist ) 203 | 204 | while running: 205 | for event in pg.event.get(): 206 | if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE): 207 | running = False 208 | if event.type == pg.KEYDOWN: 209 | key = str(event.unicode) 210 | if key in keymod: 211 | mod = keymod.index(str(event.unicode)) 212 | elif key in keylist: 213 | key = key+str(mod) 214 | notes[key][0].play() 215 | screen.blit(font.render(notes[key][1], 0, (255,255,255)), notes[key][3]) 216 | if event.type == pg.KEYUP and str(event.unicode) != '' and str(event.unicode) in keylist: 217 | key = str(event.unicode)+str(mod) 218 | notes[key][0].fadeout(100) 219 | screen.blit(font.render(notes[key][1], 0, notes[key][4]), notes[key][3]) 220 | 221 | pg.display.update() 222 | 223 | pg.mixer.quit() 224 | pg.quit() 225 | 226 | 227 | ``` 228 |
229 | 230 | Cool, now with this simple keyboard synthesizer we can start making some music (or at least try), but the sine wave sound is a bit dull. We can explore other waveforms for different timbres. 231 | 232 | ### Making "square" and "triangular" waves 233 | 234 | There are [proper ways](https://docs.scipy.org/doc/scipy/reference/signal.html#waveforms) to generate square and triangular waves but, for this project, I came up with some simple hacks to obtain (approximate) these types of waveforms. 235 | 236 | The square wave can be approximated easily by multiplying the sine wave by a "big" factor, 10 already looks squarish to me, and clipping the result to the -1 to 1 range. I know, it's more like a trapezoidal wave, but it is close. 237 | Triangular waves can be built on top of the square waves with integration, this is done with the `cumsum` function in Numpy, after that we only need to scale it back to the -1 to 1 range. This method works more less fine for short samples but cumulative errors may creep in for longer ones. 238 | 239 |
240 | Squarish and triangularish waves 241 | 242 | ```python 243 | ... 244 | def synth(frequency, duration=1.5, sampling_rate=44100): 245 | frames = int(duration*sampling_rate) 246 | arr = np.cos(2*np.pi*frequency*np.linspace(0,duration, frames)) 247 | ## arr = np.clip(arr*10, -1, 1) # squarish waves 248 | arr = np.cumsum(np.clip(arr*10, -1, 1)) # triangularish waves pt1 249 | arr = arr/max(np.abs(arr)) # triangularish waves pt2 250 | sound = np.asarray([32767*arr,32767*arr]).T.astype(np.int16) 251 | sound = pg.sndarray.make_sound(sound.copy()) 252 | 253 | return sound 254 | ... 255 | ``` 256 |
257 | 258 | We could go wild and try to come up with more interesting wave forms, for example, adding up multiples of the base frequency, we can have interesting results and theoretically mimic the timbre of any instrument. 259 | 260 | ## Replay a sound sequence 261 | 262 | Now we are able to play any music, not me, I don't have this talent, but maybe after some practice, who knows. But what if we managed to play an epyc sequence, how can we save it for eternity? 263 | 264 | Well, we can always use a recording app like audacity, using the PC speaker as its input source, but this only preserves the resulting sound, not exactly the notes which were played. 265 | 266 | ### Exporting to a text file 267 | 268 | There is a better way: store all the relevant keydown and keyup events in a list and later save them to a text file. But music is not just a sequence of notes, timing is arguably as important as the notes being played, this is why we also store timestamps for each event. Before saving them to a text file, it's interesting to turn the timestamps into time intervals, which are easier to handle in playback. The type of the event is also stored as a binary value. 269 | 270 |
271 | Export text file 272 | 273 | ```python 274 | ... 275 | keypresses = [] 276 | while running: 277 | for event in pg.event.get(): 278 | if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE): 279 | running = False 280 | if event.type == pg.KEYDOWN: 281 | key = str(event.unicode) 282 | if key in keymod: 283 | mod = keymod.index(str(event.unicode)) 284 | elif key in keylist: 285 | key = key+str(mod) 286 | notes[key][0].play() 287 | keypresses.append([1, notes[key][1], pg.time.get_ticks()]) 288 | screen.blit(font.render(notes[key][1], 0, (255,255,255)), notes[key][3]) 289 | if event.type == pg.KEYUP and str(event.unicode) != '' and str(event.unicode) in keylist: 290 | key = str(event.unicode)+str(mod) 291 | notes[key][0].fadeout(100) 292 | keypresses.append([0, notes[key][1], pg.time.get_ticks()]) 293 | screen.blit(font.render(notes[key][1], 0, notes[key][4]), notes[key][3]) 294 | 295 | pg.display.update() 296 | 297 | pg.display.set_caption("Exporting sound sequence to txt") 298 | if len(keypresses) > 1: 299 | for i in range(len(keypresses)-1): 300 | keypresses[-i-1][2] = keypresses[-i-1][2] - keypresses[-i-2][2] 301 | keypresses[0][2] = 0 # first at zero 302 | 303 | with open("soundsequence.txt", "w") as file: 304 | for i in range(len(keypresses)): 305 | file.write(str(keypresses[i])+'\n') # separate lines for readability 306 | file.close() 307 | 308 | pg.mixer.quit() 309 | pg.quit() 310 | ``` 311 |
312 | 313 | The final result is a text file with all notes that were played, when they start and when they end, this can also be modified in a text editor for adjustments and corrections. 314 | 315 | ### Replay a txt sound sequence 316 | 317 | To replay the sound sequence I think it's better to start a new script, but we can borrow most of it from the previous one. The main difference here is that there are no real keypresses, instead the program waits until the time for the new note to be played, so the keys in the notes dictionary are the notes themselves. This makes more sense for creating music, the actual keyboard keys are not relevant. 318 | 319 |
320 | Replay text file 321 | 322 | ```python 323 | import pygame as pg 324 | import numpy as np 325 | 326 | def synth(frequency, duration=1.5, sampling_rate=44100): 327 | frames = int(duration*sampling_rate) 328 | arr = np.cos(2*np.pi*frequency*np.linspace(0,duration, frames)) 329 | arr = arr + np.cos(4*np.pi*frequency*np.linspace(0,duration, frames)) # organ like 330 | arr = arr + np.cos(6*np.pi*frequency*np.linspace(0,duration, frames)) # organ like 331 | ## arr = np.clip(arr*10, -1, 1) # squarish waves 332 | ## arr = np.cumsum(np.clip(arr*10, -1, 1)) # triangularish waves pt1 333 | ## arr = arr+np.sin(2*np.pi*frequency*np.linspace(0,duration, frames)) # triangularish waves pt1 334 | arr = arr/max(np.abs(arr)) # triangularish waves pt1 335 | sound = np.asarray([32767*arr,32767*arr]).T.astype(np.int16) 336 | sound = pg.sndarray.make_sound(sound.copy()) 337 | 338 | return sound 339 | 340 | pg.init() 341 | pg.mixer.init() 342 | font2 = pg.font.SysFont("Impact", 48) 343 | screen = pg.display.set_mode((1280, 720)) 344 | pg.display.set_caption("FinFET Synth - replay txt" ) 345 | 346 | a_file = open("noteslist.txt") 347 | file_contents = a_file.read(); a_file.close() 348 | noteslist = file_contents.splitlines() 349 | keymod = '0-=' 350 | notes = {} 351 | posx, posy = 25, 25 #start position 352 | freq = 16.3516 #starting frequency 353 | 354 | for i in range(len(noteslist)): 355 | mod = int(i/36) 356 | key = noteslist[i] 357 | sample = synth(freq) 358 | color = np.array([np.sin(i/25+1.7)*130+125,np.sin(i/30-0.21)*215+40, np.sin(i/25+3.7)*130+125]) 359 | color = np.clip(color, 0, 255) 360 | notes[key] = [freq, sample, (posx, posy), 255*color/max(color), noteslist[i]] 361 | notes[key][1].set_volume(0.33) 362 | freq = freq * 2 ** (1/12) 363 | posx = posx + 140 364 | if posx > 1220: 365 | posx, posy = 25, posy+56 366 | 367 | screen.blit(font2.render(notes[key][4], 0, notes[key][3]), notes[key][2]) 368 | pg.display.update() 369 | 370 | with open("SuperMario.txt", "r") as file: 371 | keypresses = [eval(line.rstrip()) for line in file] 372 | file.close() 373 | 374 | running = 1 375 | for i in range(len(keypresses)): 376 | if not running: 377 | break 378 | for event in pg.event.get(): 379 | if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE): 380 | running = False 381 | 382 | key = keypresses[i][1] 383 | pg.time.wait(keypresses[i][2]) 384 | if keypresses[i][0]: 385 | notes[key][1].play() 386 | screen.blit(font2.render(notes[key][4], 0, (255,255,255)), notes[key][2]) 387 | else: 388 | notes[key][1].fadeout(100) 389 | screen.blit(font2.render(notes[key][4], 0, notes[key][3]), notes[key][2]) 390 | 391 | pg.display.update() 392 | 393 | pg.time.wait(500) 394 | pg.quit() 395 | ``` 396 |
397 | 398 | This script allows us to create and fine tune sound sequences manually in a text editor, I've tried out something like that with part of the [Super Mario theme song](https://github.com/FinFetChannel/Python_Synth/blob/main/SuperMario.txt). 399 | 400 | ### Creating actual sound tracks 401 | 402 | The previous script allows us to replay a sound sequence, but with all those waits it is not practical to integrate it into something like a game. Instead of playing note by note, we can generate an array with all the samples we need and play it at once or even save it to a wav file. 403 | 404 | Before we start, we need to make some adjustments to the synth function. First, we add a fade to each sample (to replicate the fadeout used on keyup events) with a length of 0.1 s, so no notes shorter than that are allowed here. Also, we don't need to turn the array into a sound but to a list, because it is easier to add a new sample to the end of the track. 405 | 406 |
407 | Track synth 408 | 409 | ```python 410 | import pygame as pg 411 | import numpy as np 412 | 413 | def synth2(frequency, duration=1.5, sampling_rate=44100): 414 | frames = int(duration*sampling_rate) 415 | arr = np.cos(2*np.pi*frequency*np.linspace(0,duration, frames)) 416 | ## arr = arr + np.cos(4*np.pi*frequency*np.linspace(0,duration, frames)) 417 | ## arr = arr + np.cos(6*np.pi*frequency*np.linspace(0,duration, frames)) 418 | arr = np.clip(arr*10, -1, 1) # squarish waves 419 | ## arr = np.cumsum(np.clip(arr*10, -1, 1)) # triangularish waves pt1 420 | ## arr = arr+np.sin(2*np.pi*frequency*np.linspace(0,duration, frames)) # triangularish waves pt1 421 | ## arr = arr/max(np.abs(arr)) # adjust to -1, 1 range 422 | fade = list(np.ones(frames-4410))+list(np.linspace(1, 0, 4410)) 423 | arr = np.multiply(arr, np.asarray(fade)) 424 | return list(arr) 425 | ``` 426 |
427 | 428 | The notes dictionary is much simpler this time, as we are only concerned about the frequency of each note. 429 | 430 |
431 | Notes dictionary for tracks 432 | 433 | ```python 434 | pg.init() 435 | pg.mixer.init() 436 | 437 | a_file = open("noteslist.txt") 438 | file_contents = a_file.read(); a_file.close() 439 | noteslist = file_contents.splitlines() 440 | freq = 16.3516 #starting frequency 441 | freqs = {} 442 | 443 | for note in noteslist: 444 | freqs[note]= freq 445 | freq = freq * 2 ** (1/12) 446 | ... 447 | ``` 448 |
449 | 450 | After that we go through all notes in the sound sequence and generate a sample for each one of them to add to a list. The samples are extended while the breaks are reduced by 0.1 seconds to account for the fadeouts. At the end the list is turned back into an array and into a pygame sound. A wait of the length of the sound is added to ensure it is played to the end. 451 | 452 |
453 | Create track from txt 454 | 455 | ```python 456 | ... 457 | with open("SuperMario.txt", "r") as file: 458 | notes = [eval(line.rstrip()) for line in file] 459 | file.close() 460 | 461 | track = [] 462 | for i in range(int(len(notes)/2)): 463 | track = track + list(np.zeros(max(0, int(44.1*(notes[i*2][2]-100))))) 464 | track = track + synth(freqs[notes[i*2][1]], 1e-3*(notes[i*2+1][2]+100)) 465 | 466 | arr = 32767*np.asarray(track)*0.5 # reduce volume by half 467 | sound = np.asarray([arr,arr]).T.astype(np.int16) 468 | sound = pg.sndarray.make_sound(sound.copy()) 469 | 470 | sound.play() 471 | pg.time.wait(int(len(arr)/44.1)) 472 | ... 473 | ``` 474 |
475 | 476 | Finally, we can export the track as a wav file, using the wave library. 477 | 478 |
479 | Export wav file 480 | 481 | ```python 482 | ... 483 | import wave 484 | 485 | sfile = wave.open('mario.wav', 'w') 486 | sfile.setframerate(44100) 487 | sfile.setnchannels(2) 488 | sfile.setsampwidth(2) 489 | sfile.writeframesraw(sound) 490 | sfile.close() 491 | 492 | pg.mixer.quit() 493 | pg.quit() 494 | ``` 495 |
496 | 497 | And we are done! The only limitation in this approach is that there are no overlapping notes in a single track (we could, but it is messy), but we can always generate multiple tracks and combine them (to add two arrays they need to have the same length though). 498 | -------------------------------------------------------------------------------- /SuperMario.txt: -------------------------------------------------------------------------------- 1 | [1, 'E5', 100] 2 | [0, 'E5', 70] 3 | [1, 'E5', 100] 4 | [0, 'E5', 70] 5 | [1, 'E5', 230] 6 | [0, 'E5', 70] 7 | [1, 'C5', 200] 8 | [0, 'C5', 70] 9 | [1, 'E5', 100] 10 | [0, 'E5', 70] 11 | [1, 'G5', 230] 12 | [0, 'G5', 70] 13 | [1, 'G4', 540] 14 | [0, 'G4', 70] 15 | [1, 'C5', 540, '2part'] 16 | [0, 'C5', 70] 17 | [1, 'G4', 410] 18 | [0, 'G4', 70] 19 | [1, 'E4', 410] 20 | [0, 'E4', 70] 21 | [1, 'A4', 410] 22 | [0, 'A4', 70] 23 | [1, 'B4', 200] 24 | [0, 'B4', 70] 25 | [1, 'A#4', 260] 26 | [0, 'A#4', 70] 27 | [1, 'A4', 90] 28 | [0, 'A4', 70] 29 | [1, 'G4', 220] 30 | [0, 'G4', 70] 31 | [1, 'E5', 130] 32 | [0, 'E5', 70] 33 | [1, 'G5', 130] 34 | [0, 'G5', 70] 35 | [1, 'A5', 130] 36 | [0, 'A5', 70] 37 | [1, 'F5', 270] 38 | [0, 'F5', 70] 39 | [1, 'G5', 100] 40 | [0, 'G5', 70] 41 | [1, 'E5', 250] 42 | [0, 'E5', 70] 43 | [1, 'C5', 250] 44 | [0, 'C5', 70] 45 | [1, 'D5', 100] 46 | [0, 'D5', 70] 47 | [1, 'B4', 100] 48 | [0, 'B4', 70] 49 | [1, 'C5', 540, '3part'] 50 | [0, 'C5', 70] 51 | [1, 'G4', 410] 52 | [0, 'G4', 70] 53 | [1, 'E4', 410] 54 | [0, 'E4', 70] 55 | [1, 'A4', 410] 56 | [0, 'A4', 70] 57 | [1, 'B4', 200] 58 | [0, 'B4', 70] 59 | [1, 'A#4', 260] 60 | [0, 'A#4', 70] 61 | [1, 'A4', 90] 62 | [0, 'A4', 70] 63 | [1, 'G4', 220] 64 | [0, 'G4', 70] 65 | [1, 'E5', 130] 66 | [0, 'E5', 70] 67 | [1, 'G5', 130] 68 | [0, 'G5', 70] 69 | [1, 'A5', 130] 70 | [0, 'A5', 70] 71 | [1, 'F5', 270] 72 | [0, 'F5', 70] 73 | [1, 'G5', 100] 74 | [0, 'G5', 70] 75 | [1, 'E5', 250] 76 | [0, 'E5', 70] 77 | [1, 'C5', 250] 78 | [0, 'C5', 70] 79 | [1, 'D5', 100] 80 | [0, 'D5', 70] 81 | [1, 'B4', 100] 82 | [0, 'B4', 70] 83 | [1, 'G5', 660, '4part'] 84 | [0, 'G5', 70] 85 | [1, 'F#5', 80] 86 | [0, 'F#5', 70] 87 | [1, 'F5', 80] 88 | [0, 'F5', 70] 89 | [1, 'D5', 80] 90 | [0, 'D5', 70] 91 | [1, 'E5', 80] 92 | [0, 'E5', 70] 93 | [1, 'G4', 105] 94 | [0, 'G4', 50] 95 | [1, 'A4', 70] 96 | [0, 'A4', 50] 97 | [1, 'C5', 140] 98 | [0, 'C5', 70] 99 | [1, 'A4', 80] 100 | [0, 'A4', 70] 101 | [1, 'C5', 80] 102 | [0, 'C5', 70] 103 | [1, 'D5', 80] 104 | [0, 'D5', 70] 105 | [1, 'G5', 105] 106 | [0, 'G5', 70] 107 | [1, 'F#5', 90] 108 | [0, 'F#5', 70] 109 | [1, 'F5', 90] 110 | [0, 'F5', 70] 111 | [1, 'D5', 80] 112 | [0, 'D5', 70] 113 | [1, 'G5', 240] 114 | [0, 'G5', 70] 115 | [1, 'F#5', 80] 116 | [0, 'F#5', 70] 117 | [1, 'F5', 80] 118 | [0, 'F5', 70] 119 | [1, 'D5', 80] 120 | [0, 'D5', 70] 121 | [1, 'E5', 80] 122 | [0, 'E5', 50] 123 | [1, 'C5', 105] 124 | [0, 'C5', 70] 125 | [1, 'C6', 220] 126 | [0, 'C6', 70] 127 | [1, 'C6', 240] 128 | [0, 'C6', 70] 129 | [1, 'C6', 80] 130 | [0, 'C6', 70] 131 | [1, 'G5', 880, '5part'] 132 | [0, 'G5', 70] 133 | [1, 'F#5', 80] 134 | [0, 'F#5', 70] 135 | [1, 'F5', 80] 136 | [0, 'F5', 70] 137 | [1, 'D5', 80] 138 | [0, 'D5', 70] 139 | [1, 'E5', 80] 140 | [0, 'E5', 70] 141 | [1, 'G4', 105] 142 | [0, 'G4', 50] 143 | [1, 'A4', 70] 144 | [0, 'A4', 50] 145 | [1, 'C5', 140] 146 | [0, 'C5', 70] 147 | [1, 'A4', 80] 148 | [0, 'A4', 70] 149 | [1, 'C5', 80] 150 | [0, 'C5', 70] 151 | [1, 'D5', 80] 152 | [0, 'D5', 70] 153 | [1, 'A4', 105] 154 | [0, 'A4', 70] 155 | [1, 'C5', 80] 156 | [0, 'C5', 70] 157 | [1, 'D5', 80] 158 | [0, 'D5', 70] 159 | [1, 'C3', 80] 160 | [0, 'C3', 70] 161 | [1, 'D#5', 280] 162 | [0, 'D#5', 70] 163 | [1, 'D5', 400] 164 | [0, 'D5', 70] 165 | [1, 'C5', 400] 166 | [0, 'C5', 70] 167 | [1, 'G5', 660, '4part'] 168 | [0, 'G5', 70] 169 | [1, 'F#5', 80] 170 | [0, 'F#5', 70] 171 | [1, 'F5', 80] 172 | [0, 'F5', 70] 173 | [1, 'D5', 80] 174 | [0, 'D5', 70] 175 | [1, 'E5', 80] 176 | [0, 'E5', 70] 177 | [1, 'G4', 105] 178 | [0, 'G4', 50] 179 | [1, 'A4', 70] 180 | [0, 'A4', 50] 181 | [1, 'C5', 140] 182 | [0, 'C5', 70] 183 | [1, 'A4', 80] 184 | [0, 'A4', 70] 185 | [1, 'C5', 80] 186 | [0, 'C5', 70] 187 | [1, 'D5', 80] 188 | [0, 'D5', 70] 189 | [1, 'G5', 105] 190 | [0, 'G5', 70] 191 | [1, 'F#5', 90] 192 | [0, 'F#5', 70] 193 | [1, 'F5', 90] 194 | [0, 'F5', 70] 195 | [1, 'D5', 80] 196 | [0, 'D5', 70] 197 | [1, 'G5', 240] 198 | [0, 'G5', 70] 199 | [1, 'F#5', 80] 200 | [0, 'F#5', 70] 201 | [1, 'F5', 80] 202 | [0, 'F5', 70] 203 | [1, 'D5', 80] 204 | [0, 'D5', 70] 205 | [1, 'E5', 80] 206 | [0, 'E5', 50] 207 | [1, 'C5', 105] 208 | [0, 'C5', 70] 209 | [1, 'C6', 220] 210 | [0, 'C6', 70] 211 | [1, 'C6', 240] 212 | [0, 'C6', 70] 213 | [1, 'C6', 80] 214 | [0, 'C6', 70] 215 | [1, 'G5', 880, '5part'] 216 | [0, 'G5', 70] 217 | [1, 'F#5', 80] 218 | [0, 'F#5', 70] 219 | [1, 'F5', 80] 220 | [0, 'F5', 70] 221 | [1, 'D5', 80] 222 | [0, 'D5', 70] 223 | [1, 'E5', 80] 224 | [0, 'E5', 70] 225 | [1, 'G4', 105] 226 | [0, 'G4', 50] 227 | [1, 'A4', 70] 228 | [0, 'A4', 50] 229 | [1, 'C5', 140] 230 | [0, 'C5', 70] 231 | [1, 'A4', 80] 232 | [0, 'A4', 70] 233 | [1, 'C5', 80] 234 | [0, 'C5', 70] 235 | [1, 'D5', 80] 236 | [0, 'D5', 70] 237 | [1, 'A4', 105] 238 | [0, 'A4', 70] 239 | [1, 'C5', 80] 240 | [0, 'C5', 70] 241 | [1, 'D5', 80] 242 | [0, 'D5', 70] 243 | [1, 'C3', 80] 244 | [0, 'C3', 70] 245 | [1, 'D#5', 280] 246 | [0, 'D#5', 70] 247 | [1, 'D5', 400] 248 | [0, 'D5', 70] 249 | [1, 'C5', 400] 250 | [0, 'C5', 70] -------------------------------------------------------------------------------- /keyboard_synth.py: -------------------------------------------------------------------------------- 1 | import pygame as pg 2 | import numpy as np 3 | 4 | pg.init() 5 | pg.mixer.init() 6 | screen = pg.display.set_mode((1280, 720)) 7 | font = pg.font.SysFont("Impact", 48) 8 | 9 | def synth(frequency, duration=1.5, sampling_rate=44100): 10 | frames = int(duration*sampling_rate) 11 | arr = np.cos(2*np.pi*frequency*np.linspace(0,duration, frames)) 12 | arr = arr + np.cos(4*np.pi*frequency*np.linspace(0,duration, frames)) 13 | arr = arr - np.cos(6*np.pi*frequency*np.linspace(0,duration, frames)) 14 | ## arr = np.clip(arr*10, -1, 1) # squarish waves 15 | ## arr = np.cumsum(np.clip(arr*10, -1, 1)) # triangularish waves pt1 16 | ## arr = arr+np.sin(2*np.pi*frequency*np.linspace(0,duration, frames)) # triangularish waves pt1 17 | arr = arr/max(np.abs(arr)) # triangularish waves pt1 18 | sound = np.asarray([32767*arr,32767*arr]).T.astype(np.int16) 19 | sound = pg.sndarray.make_sound(sound.copy()) 20 | 21 | return sound 22 | 23 | 24 | keylist = '123456789qwertyuioasdfghjklzxcvbnm,.' 25 | notes_file = open("noteslist.txt") 26 | file_contents = notes_file.read() 27 | notes_file.close() 28 | noteslist = file_contents.splitlines() 29 | 30 | keymod = '0-=' 31 | notes = {} # dict to store samples 32 | freq = 16.3516 # start frequency 33 | posx, posy = 25, 25 #start position 34 | 35 | 36 | for i in range(len(noteslist)): 37 | mod = int(i/36) 38 | key = keylist[i-mod*36]+str(mod) 39 | sample = synth(freq) 40 | color = np.array([np.sin(i/25+1.7)*130+125,np.sin(i/30-0.21)*215+40, np.sin(i/25+3.7)*130+125]) 41 | color = np.clip(color, 0, 255) 42 | notes[key] = [sample, noteslist[i], freq, (posx, posy), 255*color/max(color)] 43 | notes[key][0].set_volume(0.33) 44 | notes[key][0].play() 45 | notes[key][0].fadeout(100) 46 | freq = freq * 2 ** (1/12) 47 | posx = posx + 140 48 | if posx > 1220: 49 | posx, posy = 25, posy+56 50 | 51 | screen.blit(font.render(notes[key][1], 0, notes[key][4]), notes[key][3]) 52 | pg.display.update() 53 | 54 | 55 | running = 1 56 | mod = 1 57 | pg.display.set_caption("FinFET Synth - Change range: 0 - = // Play with keys: "+keylist ) 58 | 59 | keypresses = [] 60 | while running: 61 | for event in pg.event.get(): 62 | if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE): 63 | running = False 64 | if event.type == pg.KEYDOWN: 65 | key = str(event.unicode) 66 | if key in keymod: 67 | mod = keymod.index(str(event.unicode)) 68 | elif key in keylist: 69 | key = key+str(mod) 70 | notes[key][0].play() 71 | keypresses.append([1, notes[key][1], pg.time.get_ticks()]) 72 | screen.blit(font.render(notes[key][1], 0, (255,255,255)), notes[key][3]) 73 | if event.type == pg.KEYUP and str(event.unicode) != '' and str(event.unicode) in keylist: 74 | key = str(event.unicode)+str(mod) 75 | notes[key][0].fadeout(100) 76 | keypresses.append([0, notes[key][1], pg.time.get_ticks()]) 77 | screen.blit(font.render(notes[key][1], 0, notes[key][4]), notes[key][3]) 78 | 79 | pg.display.update() 80 | 81 | pg.display.set_caption("Exporting sound sequence") 82 | if len(keypresses) > 1: 83 | for i in range(len(keypresses)-1): 84 | keypresses[-i-1][2] = keypresses[-i-1][2] - keypresses[-i-2][2] 85 | keypresses[0][2] = 0 # first at zero 86 | 87 | with open("test.txt", "w") as file: 88 | for i in range(len(keypresses)): 89 | file.write(str(keypresses[i])+'\n') # separate lines for readability 90 | file.close() 91 | 92 | pg.mixer.quit() 93 | pg.quit() 94 | -------------------------------------------------------------------------------- /noteslist.txt: -------------------------------------------------------------------------------- 1 | C0 2 | C#0 3 | D0 4 | D#0 5 | E0 6 | F0 7 | F#0 8 | G0 9 | G#0 10 | A0 11 | A#0 12 | B0 13 | C1 14 | C#1 15 | D1 16 | D#1 17 | E1 18 | F1 19 | F#1 20 | G1 21 | G#1 22 | A1 23 | A#1 24 | B1 25 | C2 26 | C#2 27 | D2 28 | D#2 29 | E2 30 | F2 31 | F#2 32 | G2 33 | G#2 34 | A2 35 | A#2 36 | B2 37 | C3 38 | C#3 39 | D3 40 | D#3 41 | E3 42 | F3 43 | F#3 44 | G3 45 | G#3 46 | A3 47 | A#3 48 | B3 49 | C4 50 | C#4 51 | D4 52 | D#4 53 | E4 54 | F4 55 | F#4 56 | G4 57 | G#4 58 | A4 59 | A#4 60 | B4 61 | C5 62 | C#5 63 | D5 64 | D#5 65 | E5 66 | F5 67 | F#5 68 | G5 69 | G#5 70 | A5 71 | A#5 72 | B5 73 | C6 74 | C#6 75 | D6 76 | D#6 77 | E6 78 | F6 79 | F#6 80 | G6 81 | G#6 82 | A6 83 | A#6 84 | B6 85 | C7 86 | C#7 87 | D7 88 | D#7 89 | E7 90 | F7 91 | F#7 92 | G7 93 | G#7 94 | A7 95 | A#7 96 | B7 97 | C8 98 | C#8 99 | D8 100 | D#8 101 | E8 102 | F8 103 | F#8 104 | G8 105 | G#8 106 | A8 107 | A#8 108 | B8 109 | -------------------------------------------------------------------------------- /python synth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FinFetChannel/Python_Synth/f2371461eb0187aaa9d9c68a8c3b6badab150647/python synth.png -------------------------------------------------------------------------------- /replay_synth.py: -------------------------------------------------------------------------------- 1 | import pygame as pg 2 | import numpy as np 3 | 4 | def synth(frequency, duration=1.5, sampling_rate=44100): 5 | frames = int(duration*sampling_rate) 6 | arr = np.cos(2*np.pi*frequency*np.linspace(0,duration, frames)) 7 | arr = arr + np.cos(4*np.pi*frequency*np.linspace(0,duration, frames)) # organ like 8 | arr = arr + np.cos(6*np.pi*frequency*np.linspace(0,duration, frames)) # organ like 9 | ## arr = np.clip(arr*10, -1, 1) # squarish waves 10 | ## arr = np.cumsum(np.clip(arr*10, -1, 1)) # triangularish waves pt1 11 | ## arr = arr+np.sin(2*np.pi*frequency*np.linspace(0,duration, frames)) # triangularish waves pt1 12 | arr = arr/max(np.abs(arr)) # triangularish waves pt1 13 | sound = np.asarray([32767*arr,32767*arr]).T.astype(np.int16) 14 | sound = pg.sndarray.make_sound(sound.copy()) 15 | 16 | return sound 17 | 18 | pg.init() 19 | pg.mixer.init() 20 | font2 = pg.font.SysFont("Impact", 48) 21 | screen = pg.display.set_mode((1280, 720)) 22 | pg.display.set_caption("FinFET Synth - replay txt" ) 23 | 24 | a_file = open("noteslist.txt") 25 | file_contents = a_file.read(); a_file.close() 26 | noteslist = file_contents.splitlines() 27 | keymod = '0-=' 28 | notes = {} 29 | posx, posy = 25, 25 #start position 30 | freq = 16.3516 #starting frequency 31 | 32 | for i in range(len(noteslist)): 33 | mod = int(i/36) 34 | key = noteslist[i] 35 | sample = synth(freq) 36 | color = np.array([np.sin(i/25+1.7)*130+125,np.sin(i/30-0.21)*215+40, np.sin(i/25+3.7)*130+125]) 37 | color = np.clip(color, 0, 255) 38 | notes[key] = [freq, sample, (posx, posy), 255*color/max(color), noteslist[i]] 39 | notes[key][1].set_volume(0.33) 40 | freq = freq * 2 ** (1/12) 41 | posx = posx + 140 42 | if posx > 1220: 43 | posx, posy = 25, posy+56 44 | 45 | screen.blit(font2.render(notes[key][4], 0, notes[key][3]), notes[key][2]) 46 | pg.display.update() 47 | 48 | with open("SuperMario.txt", "r") as file: 49 | keypresses = [eval(line.rstrip()) for line in file] 50 | file.close() 51 | 52 | running = 1 53 | for i in range(len(keypresses)): 54 | if not running: 55 | break 56 | for event in pg.event.get(): 57 | if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE): 58 | running = False 59 | 60 | key = keypresses[i][1] 61 | pg.time.wait(keypresses[i][2]) 62 | if keypresses[i][0]: 63 | notes[key][1].play() 64 | screen.blit(font2.render(notes[key][4], 0, (255,255,255)), notes[key][2]) 65 | else: 66 | notes[key][1].fadeout(100) 67 | screen.blit(font2.render(notes[key][4], 0, notes[key][3]), notes[key][2]) 68 | 69 | pg.display.update() 70 | 71 | pg.time.wait(500) 72 | pg.quit() 73 | -------------------------------------------------------------------------------- /tracks_synth.py: -------------------------------------------------------------------------------- 1 | import pygame as pg 2 | import numpy as np 3 | 4 | def synth(frequency, duration=1.5, sampling_rate=44100): 5 | frames = int(duration*sampling_rate) 6 | arr = np.cos(2*np.pi*frequency*np.linspace(0,duration, frames)) 7 | ## arr = arr + np.cos(4*np.pi*frequency*np.linspace(0,duration, frames)) # organ like 8 | ## arr = arr + np.cos(6*np.pi*frequency*np.linspace(0,duration, frames)) # organ like 9 | arr = np.clip(arr*10, -1, 1) # squarish waves 10 | ## arr = np.cumsum(np.clip(arr*10, -1, 1)) # triangularish waves pt1 11 | ## arr = arr+np.sin(2*np.pi*frequency*np.linspace(0,duration, frames)) # triangularish waves pt1 12 | ## arr = arr/max(np.abs(arr)) # adjust to -1, 1 range 13 | fade = list(np.ones(frames-4410))+list(np.linspace(1, 0, 4410)) 14 | arr = np.multiply(arr, np.asarray(fade)) 15 | return list(arr) 16 | 17 | pg.init() 18 | pg.mixer.init() 19 | 20 | a_file = open("noteslist.txt") 21 | file_contents = a_file.read(); a_file.close() 22 | noteslist = file_contents.splitlines() 23 | freq = 16.3516 #starting frequency 24 | freqs = {} 25 | 26 | for i in range(len(noteslist)): 27 | freqs[noteslist[i]]= freq 28 | freq = freq * 2 ** (1/12) 29 | 30 | with open("SuperMario.txt", "r") as file: 31 | notes = [eval(line.rstrip()) for line in file] 32 | file.close() 33 | 34 | track = [] 35 | for i in range(int(len(notes)/2)): 36 | track = track + list(np.zeros(max(0, int(44.1*(notes[i*2][2]-100))))) 37 | track = track + synth(freqs[notes[i*2][1]], 1e-3*(notes[i*2+1][2]+100)) 38 | 39 | arr = 32767*np.asarray(track)*0.5 40 | sound = np.asarray([arr,arr]).T.astype(np.int16) 41 | sound = pg.sndarray.make_sound(sound.copy()) 42 | 43 | sound.play() 44 | pg.time.wait(int(len(arr)/44.1)) 45 | 46 | import wave 47 | 48 | sfile = wave.open('mario.wav', 'w') 49 | sfile.setframerate(44100) 50 | sfile.setnchannels(2) 51 | sfile.setsampwidth(2) 52 | sfile.writeframesraw(sound) 53 | sfile.close() 54 | 55 | pg.mixer.quit() 56 | pg.quit() 57 | --------------------------------------------------------------------------------