Show HN: VOOG – Moog-style polyphonic synthesizer in Python with tkinter GUI
github.comBody: I built a polyphonic synthesizer in Python with a tkinter GUI styled after the Moog Subsequent 37.
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.
Needs audio examples! A YouTube link!
There is a reason that most people do not use interpreted languages, or languages with garbage collection, for audio synthesis and DSP.
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!
Got a friend who is in the high frequency trading industry and uses both Java and C#. I asked about GC. Turns out you just write code that doesn’t need to GC. Object pools, off-heap memory etc.
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.
> Turns out you just write code that doesn’t need to GC. Object pools, off-heap memory etc.
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.
That's what we do in games too. If you know the scope of your project and how to avoid dynamic allocation it's fine.
This seems to conflate different things.
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.
I was speaking in broad generalities (and did mention Lua as a counter-example).
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.
There's a lot of soft-realtime (=audio/video, gaming etc) apps using interpreted languages. Besides Python and Lua, also Erlang.
Oh and of course SuperCollider.
Generating audio is far from being an "intensive" operation these days.
Oddly enough, there's another recent popular Show HN on the topic of fixing that (https://news.ycombinator.com/item?id=46972392).
Not being a dev writing code running in realtime nor an audio type with experience of things not running in realtime, what happens when GC kicks in? Does the entire audio stack go silent? Does it only effect the one filter so it sounds like a drop, or is it a pause so not it is no longer in sync? In theory, I get why it is bad, but I'm curious of what it sounds like when it does go bad.
Python mainly uses reference counting for garbage collction, and the reference cycle breaking full-program gc can be manually controlled.
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.
The audio interface hardware expects to get N samples every M msecs, and stops for no man (or program). So, anything that stops or flows the flow enough that less than N samples are delivered every M msecs causes a click or pop in the output. How bad the pop actually sounds depends on a lot of different things, so its hard to predict.
I mean, you can literally try OP's code to see for yourself.
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).
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?
Even if you avoid GC, you need the memory used by the realtime code to be pinned to physical RAM to avoid paging.
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.
Now I'm curious-- what happens if the author adds a manual garbage collection call at the end of _audio_callback? Can it still Moog, or will that cause it to eternally miss deadlines?
> Lua excepted, with conditions
Where can I read more about this? Is Lua's garbage collector tuned for real-time operations?
You can build Lua to not do GC.
You don't have to build Lua to not do GC, you can just tell it how its GC heuristics are supposed to behave at runtime by setting the pause and multiplier variables - good treatise on how to do this at runtime is available here:
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...
I sort of managed to get it working under 3.10 (and it would probably work considerably further back) but the output was a bit wonky, especially when trying to play multiple notes quickly or simultaneously. I had to patch a couple of things related to type annotations in synth/gui/app.py to make it run without MIDI support.
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.
Congrats on releasing. This is what I consider love of the game.
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.
Doesn't work at all on my system (kubuntu stable, whatever the stock audio subsystem is now). keys stick down when activated with keyboard, labels on keys disappear once played, vu meter moves but no sound comes out except sporadic beeps.
Very cool! I will be playing with this.
The only thing that jumped out to me is a lack of a panic button that stops all sound.
This is fantastic work! I love how you stuck with pure Python libraries - tkinter, numpy, and sounddevice. The Moog ladder filter implementation is particularly impressive. Have you considered adding export functionality for the generated audio? Being able to save presets or record to WAV files would make this even more practical for music production. Great job on the GUI design too - those rotary knobs look perfect for this application.
Honestly, this is the best thing that's come along on HN in a while. I was feeling kinda down, but this made my day. And it instantly recognized my m-audio keystation, a feat that Logic could not perform.
I have recently come to really like tkinter. It has many good concepts. And I too am using it from Python. That said ...
Oh no ... Not another Python project, that doesn't pin its versions with hashes.
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.
> 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.
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.
Damn, that is really, really cool.
Thanks for building this and thanks for sharing.
Really ambitious and really cool, congrats on finishing and 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.
This is clean, well-architected Python. The library ecosystem has matured significantly in the last few years - having solid abstractions makes complex applications much more maintainable.