Features: 3 oscillators, Moog ladder filter (24dB/oct), dual ADSR envelopes, LFO, glide, noise generator, 4 multitimbral channels, 19 presets, rotary
knob GUI, virtual keyboard with mouse + QWERTY input, and MIDI support.
No external GUI frameworks — just tkinter, numpy, and sounddevice.Overall neat concept. I've thought about playing around with sounddevice myself and the code here offers quite a bit of guidance.
Do you plan to put a license on this? Would you be interested in a PR to make a wheel (installable as an application with uv or pipx) from it? Also, I didn't play around with the patches, but it seems to me like they could be refactored to be data-driven.
I sent the link to my buddy who owns a shop that specializes in analog synths. Our shared love of drum machines and electronic music production was how we became friends. Dunno if he's nerdy to the point that he'll install it, but I'm certain he'll also love that it exists.
The only thing that jumped out to me is a lack of a panic button that stops all sound.
Oh no ... Not another Python project, that doesn't pin its versions with hashes.
pip install numpy sounddevice
pip install mido python-rtmidi
This stuff really shouldn't be done in 2026 any longer.I mean it's a hobby project, so you are free to do what you want, of course. Just please never do this in a professional environment. This is one reason Python projects catch so much flak from many people. One day it works, next day it doesn't. And surely not 2 years later, when a random person stumbles upon the repository and wants to try things. Please make your projects reproducible. Use pinned versions and lock files containing hashes, so that other people can get the same setup and it doesn't become an "It ran on my machine." project.
I would be very surprised if a project like this were broken by a Numpy or sounddevice update within the next 2 years. sounddevice is too simple (and the code uses it in a localized and very simple way), and Numpy too stable (they're pretty good about semver, and it was 18 years from 1.0 to 2.0.0). Anyway, people qualified to set up Python code locally in "dev mode" following instructions like this, should also be qualified to notice the last-commit dates and do that kind of investigative work. (We also now have installers that can just automatically disregard packages published after a certain date.)
The flip side of this is that having every project pin an exact version increases the chance that different projects needlessly demand different versions. The same version could be hard-linked into multiple environments (even if you aren't brave enough to try to stuff multiple applications into a common "sandbox"), avoiding bloat. And sure, you don't care about a few megs of disk space. But not everyone has a fast Internet connection. And Fastly presumably cares that total PyPI is now in the exabyte range and probably a very large percentage of that is unnecessary.
Thanks for building this and thanks for sharing.
Getting into the weeds, how are you doing individual voices, ie an an analog synth needs a separate signal path for each note of polyphony with inadvertent and unavoidable interference… which ironically is desirable.
It's great that it works, and it may well work 99% of the time. And it may have been a great learning experience/platform, so congrats for that.
But it's important for people to understand why this is generally the wrong toolset for this sort of software development, even when it can be so much fun.
Python and other interpreted languages (Lua excepted, with conditions), and languages like Swift that have GC, cannot ensure non-blocking behavior in the code that need to runs in realtime. You can paper over this with very large audio buffers (which makes the synth feel sluggish) or with crossed fingers (which work a surprising amount of the time). But ultimately you need a language like C/C++/Rust etc. to ensure that your realtime DSP code is actually realtime.
Despite Apple pushing Swift "for everything", even they still acknowledge that you should not write AudioUnit (or any other plugin formats) using Swift.
Meanwhile, have fun with this, which it looks like you already did!
It won’t do the absolute fastest tasks in the stack quite as well but supposedly the coding speed and memory management benefits are more important, and there’s no GC so it’s reliable.
Some GCd languages make this easier than others. Java and C# allow you to use primitive types. Even just doing some basic arithmetic in Python (at least CPython) is liable to create temporary objects; locals don't get stack-allocated.
Interpreted is not a problem from the predictable behaviour point of view. You may get less absolute performance. Though with Python you can do the heavy lifting in numpy etc which are in native code. And this is what is done here, see eg https://github.com/gpasquero/voog/blob/main/synth/dsp/envelo...
Languages that have garbage collection: not going to rehash the standard back-and-forth here, suffice it to say that the devil is in the details.
If you want realtime safe behavior, your first port of call is rarely going to be an interpreted language, even though, sure, it is true that some of them are or can be made safe.
For a simple audio app like this synth on a modern CPU it's kind of trivial to do it in any language if the buffer is >40 ms. I'm talking about managing the buffers. Running the synth/filter math in pure Python is still probably not doable.
For RC, each "kick in" of the GC is usually small amount of work, triggered by the reference count of an object going to 0. In this program's case I'd guess you don't hear any artifacts.
It’s been a while since I was involved in computer audio, but is there a difficulty I’m not seeing with simply using ring buffers and doing memory allocations upfront so as to avoid GC altogether?
The problem with GC is not (always) what it does, it's when it does it. You often do not have control over that, and if it kicks in the middle of otherwise realtime code ... not good.
Where can I read more about this? Is Lua's garbage collector tuned for real-time operations?
https://www.lua.org/wshop18/Ierusalimschy.pdf
And fine details about how to tune things for the optimal GC behaviour for your app:
https://www.tutorialspoint.com/lua/lua_garbage_collection.ht...