Faust × BespokeSynth
My Google Summer of Code 2026 project. Repository.
Project overview
The goal of the project was to provide an interface in BespokeSynth for editing and running Faust programs in real time, with the explicit goal of providing a workable development environment for people writing Faust code.
Faust is a programming language for audio programming. It allows expressing signal graphs that can make almost any sound you can make with a computer.
BespokeSynth is a software modular synthesizer built with JUCE and OpenFrameworks. It has a wide array of UI widgets, modules, and capabilities that allow for making and performing music.
Development process
Beginnings
The first steps of the project were to create basic glue code for connecting Faust code compiled to a C header file (.h) to the rest of the plumbing in BespokeSynth. This let me get as far as hearing the first sounds ever generated by Faust code inside of BespokeSynth, and even generating a basic UI. This was all done in the first two weeks of the project, which serves as a testament to just how easy it is to integrate Faust into your project!
So that’s it, right? Wrap it up folks, we’re done!
… psych! We’re doing live recompilation, baby! Who wants to sit around for a whole C++ project to compile while you’re trying to iterate on that sick sound? I sure don’t!
The rest of this is about just that: not only how the sauce is made, but how it works and why! Hold on to your hats folks, here we go.
Thread synchronization (PoliteDoubleBuffer)
Once live compilation was implemented, it was possible to start working on the live editing experience.
This is where the (arguably) most important part of the project began. I needed a way to manage the DSP code across both the audio thread and the UI thread. Importantly, I needed to be able to:
- compile while the audio thread is running
- not block the audio thread (real-time safety)
- not change to the new Faust code while the audio thread is executing the old code (thread safety)
- fail gracefully without stopping the sound if there is a compilation error
These requirements led to the creation of a thread synchronization primitive that I call the Polite Double Buffer, which I’ll abbreviate to PDB. The PDB works as a one-way pipe for sending data to the audio thread. Importantly, it checks all of the boxes for thread safety and real-time safety.
The high-level look at how the PDB works is this:
- The PDB holds two things: one is the “front” (for the audio thread) and one is the “back” (for the UI thread), and it also allows swapping which one is the front buffer and which one is the back buffer.
- The UI thread can call these methods:
GetBackBuffer()- get the thing on the back bufferSwitchWhenReady()- asks the audio thread to switch buffers
- The audio thread can call these methods:
GetFrontBuffer()- get the thing on the front bufferSwitchIfReady()- switches buffers if it was asked to
See? It’s polite because it asks before switching!
This interface is enough to provide all the properties I’ve listed above and more! It works out very nicely for what we’re doing.
Versioned UI updates
The next key problem was maintaining connections with other parts of BespokeSynth when recompiling the Faust program. BespokeSynth offers sliders that can be connected to modulators like LFO’s. When the Faust program gets compiled, it asks BespokeSynth to create all the controls from scratch. This is a bit of a problem for us because it assumes we don’t care about the previous controls, where in this case, we do.
The solution to this is to track the UI elements as we create them and reuse old elements that have the same name. An important part of this is saving each control with a “generation” number that changes each time the Faust code is updated. Then, when we go to render the UI, we only render controls that match the current generation number. This means that your modules will always stay connected when you recompile your program.
Dependency management: keeping it easy to compile
LLVM is an incredible work of engineering. It produces incredibly fast binaries and does optimizations to code that are hard to comprehend. In our case, using it takes our CPU usage down by an order of magnitude (40% -> 2%), and in some cases more (when comparing against Faust’s in-house interpreter/bytecode backend).
That said, LLVM is also a pain in the butt sometimes because it’s really huge (up to 1 GB for the version we’re using) and takes forever to build from source. Because of this, we are left with no choice but to use the binary distribution of LLVM. This adds complications to the build process because we can’t just add it to our git repository like all the other dependencies. We also don’t want to depend on users having LLVM installed at the system level because that places an undue burden on them to make sure they have the right version installed.
Instead, we need to do the heavy lifting to ensure the user has LLVM on their machine if they want to build with it. To make this as simple as possible, we provide a build option for the user to describe how we should get it. Users can pick between downloading LLVM, their system LLVM, or using no LLVM and falling back to the Interpreter backend. There’s also a bonus option that tries to use the system backend and falls back to downloading if it can’t find a workable LLVM version.
To keep it simple though, we always default to the Interpreter backend. This keeps the barrier to entry of compiling Bespoke from source as low as possible and keeps our default dependencies minimal!
Save/load
Surprisingly, one of the hardest parts of the project to get right was save/load. Bespoke’s save/load system is mostly automated, which is nice in most cases, but it contains a few assumptions about how modules work that proved a bit challenging to mitigate.
Or at least they would have been challenging, if it hadn’t been for BespokeSynth lead developer (and mentor on this project) Ryan Challinor! It just so happened that he had recently encountered similar problems while making another module that has a dynamic UI layout.
The module is called the interface module, and it allows creating custom UI’s from the controls on other modules. These are serialized as JSON objects in the save file and loaded at startup during a load phase dubbed the layout phase.
Fortunately, I was able to use this to compile the DSP code and build the user interface before the automatic save system tries to load the sliders. Saving was a little more complicated because the Polite Double Buffer primitive only allows you to access the DSP code from the audio thread. This was mitigated by always serializing the DSP code before updating the audio thread. This way, we’re always prepared to save.
Polite Double Buffer part 2: the double compile
One of the requirements of the Polite Double Buffer (PDB) primitive is that it never allows the UI thread to access the data that the audio thread has access to. This is good because it keeps you from accidentally doing things you shouldn’t and causing errors, but it also complicates things in some cases.
One of the assumptions in the PDB’s design is that the audio thread will always execute between compilations. If it doesn’t, the thread doing the compilation will hang until the audio thread unblocks it. This seems fine, since the audio thread is always running, right? So if the other thread is blocked, it should get unblocked pretty soon … right? Wrong! There is actually a button that users can press to pause the audio thread from BespokeSynth. But it’s a catch-22! The UI thread needs to be running for the UI to work (duh), so if the user pauses and then compiles twice, it will wait for the audio thread to run before continuing, which puts us in an infinite deadlock!
This is really bad, and it means we need to introduce some way to allow the UI thread to act on behalf of the audio thread if we know it isn’t running.
The simple answer here is: mutex. Use a mutex to allow the UI thread to safely perform the audio thread’s work.
But I can hear you saying, “Wait, but isn’t locking the audio thread what we were trying to avoid? Wouldn’t it defeat the whole purpose of the Polite Double Buffer if it used a mutex?” That’s a fair question! Although introducing a mutex does add significantly more code to our primitive, it doesn’t compromise the design or the original purpose of the PDB.
The key is that the mutex is only used if two conditions are met:
- the audio thread is paused
- AND we need to switch to the new DSP code
… that’s it! The audio thread isn’t usually paused in Bespoke, so that isn’t much of a problem. And on top of that, what does it matter if we lock the audio thread while it’s paused? It’ll never know what hit it! So, that’s what we do. The mutex prevents us from ever running into a situation where we would need to overlap with the audio thread, which solves the problem!
Lifetimes
Not Rust lifetimes, but C++ lifetimes! There are quite a few parts to this project, and one of the key parts of designing it was finding when there was something with a shorter lifetime than the thing it contains. To fix this, I would create a new class for the object so it could be initialized separately from the parent, and things that live the same length as it could be constructed and destructed at the correct times.
In all, these were the key things that I ended up separating:
FaustDSP- lives less time than
FaustConnector - manages the DSP code, provides passthrough functions for FaustConnector to do things like run the DSP. Also, stores whether the DSP is in an error state, tracking whether or not
Processcan be run.
- lives less time than
FaustIR- lives as long as
FaustConnector - stores a serializable copy of the Faust code
- lives as long as
DspContainer- lives less time than
FaustDSP - lives the same length as
FaustDSP - manages anything that touches both LLVM and Interpreter backend, including:
- the compiled DSP and its corresponding factory from Faust
- saving and loading the DSP with the Faust IR
- lives less time than
FaustUI- lives inside
FaustConnector, longer thanFaustDSP - manages anything to do with the UI
- lives inside
If I were to do a similar project again, this architecture is definitely something I would copy.
What I learned
Overall, I learned a lot through this project. I learned to use atomics for making threading primitives, practiced designing within an existing architecture, and spent a lot of time thinking about hard problems!
I also learned that it’s really motivating to write code if you have someone to show it off to! Thanks to my mentors for meeting with me every week and providing feedback during this whole process.
What’s next?
Even though GSoC 2026 is over now, there are still quite a few things this project could benefit from!
Here are some I can think of:
- Add Faust demo programs as entries in the Bespoke module system.
- MIDI support.
- Syntax highlighting in DSP editor.
- Allow setting Faust compiler flags inside Faust programs for each program.
- Integer sliders.
- Convert some existing Bespoke modules to be Faust modules so they run faster and are editable.
… and much more. The beautiful thing about this type of project is that there are so many directions it can be taken! Stay tuned for related future works!
If you made it this far, give yourself a pat on the back! If you didn’t make it through, or found some parts confusing, please send me a message! I’m happy to answer questions! My contact information is on my about page.
Attribution
This project wouldn’t have been possible without the lovely folks from Grame and the BespokeSynth team who were my mentors throughout this project:
Stéphane Letz (@sletz) - Faust lead developer
Thank you for all your dedication to the Faust project and for mentoring all of us Google Summer of Code contributors over the years, whose projects you have helped bring over the finish line for all these years. Your passion speaks for itself!Ryan Challinor (@awwbees) - BespokeSynth lead developer
Thanks for answering all my questions about Bespoke, no matter how silly, and thanks for tolerating all the tickets I’ve filed over the years. It has been really fun getting to work with you on this project!Benjamin Quiedeville (@FDN Seeker) - Experienced Faust user
Thank you for helping guide the project’s architecture and specifically for helping to name the PoliteDoubleBuffer! Thanks for letting me bounce ideas off you and for all your feedback. Best of luck to you on your PhD!
Thank you all for helping me through demos, code review, and planning things out week by week. Your support has been invaluable!
Also, I want to offer a special thanks to Ryan for making BespokeSynth in the first place and for agreeing to let us do this crazy project with it, as well as to Grame for selecting this project and allowing it to happen!
Thanks to Google and the Google Summer of Code organizers for providing this amazing opportunity for open-source organizations and contributors alike.
Finally, thanks to you for reading! And to all the French speakers out there - salut!