Deprecating the rsynth
crate
rsynth
was a library for writing audio applications and plugins in the Rust programming language, initially developed by doomy.
I took over the maintenance of the project and made significant changes to the design.
The project grew in features and even reached over 100 stars on GitHub (which is a lot for this project, see below why).
However, it was barely used, couldn't catch-up with the evolution of audio plugin standards and eventually lost my interest.
After checking if doomy had still plans for it (he didn't), I decided to retire the project to make
room for other stuff for myself and to point potential users towards other libraries.
In this blog-post, I discuss why the project was deprecated, its heritage and some lessons learned.
Why deprecate the project?
As it goes, there are several reasons why the project was deprecated.
Changing context
Initially, rsynth
only supported VST 2.4. Myself not being a particular fan of VST 2.4, I added support for jack
, offline rendering and in-memory rendering (for testing).
Fast forwards five years, Steinberg is dancing on VST 2's grave, jack
is being challenged by pipewire
and Steinberg's approach to the VST 2 standard gave rise to yet another new standard: CLAP (not to be confused with the crate for Command Line Argument Parsing that has the same name). Also, I wanted LV2 support.
When Robbert van der Helm, of yabridge
fame initiated nih-plug
as a direct “competitor” of rsynth
, I knew that rsynth
's fate was sealed.
Little to no usage
I searched for it. It looks like only my own crate print_chords
used rsyth
.
Since it only used jack
, I ported it to using the jack
crate directly.
This took me a couple of hours.
Intermezzo: what to do if you happen to be the one user that I didn't know about?
If, by any chance, you are actually using rsynth
, the obvious solution is using nih-plug
instead.
There's another option that I offer for consideration and that I plan to use myself.
Instead of using a crate that tries to abstract away the differences between the plugin standards, you can proceed as follows.
First, write the plugin as a “core” library (a rust crate).
This is anyhow something I'd recommend, also if you use nih-plug
, for instance.
Per plugin standard that you want to support, create a separate crate that depends both on the “core” library and on an a library that is dedicated to that particular plugin standard (such as the lv2
crate, the clack
crate and ... well ... something else for VST 3).
You may be surprised to hear “don't use a crate that tries to abstract away the differences between plugin standards” from somebody who spent a lot of time developing, well, a crate that tries to abstract away the differences between the plugin standards. The reason I recommend considering the other approach is that it gives you the possibility to use all the features of the specific plugin standard, while avoiding code duplication by putting everything that is common in a separate crate. Yes, you may have to learn the details about all the plugin standards, but I think that any abstraction layer will eventually leak details of the underlying plugin standards anyway (unless maybe it tries to stick to only the features that are offered by all common standards). The authors of abstraction layers are not really to blame for this. In my experience, it's just very hard to abstract over multiple standards that are also moving targets on their own. If you then want to keep some level of stability, well, that's just not doable.
From the point of view of an ecosystem, one can provide templates that can be used as a starting point and customized at will.
Heritage
Now for the good news: several crates have been spun off of rsynth
:
vecstorage
: re-use the memory of aVec
to store items of different types (in particular with different lifetimes)midi-consts
: the name says it alltimestamp-stretcher
: convert time-stamps from one format (e.g. midi ticks) to another (e.g. audio frames). It's agnostic about the format of the time stamps, supports a dynamic conversion-rate and is very precise (i.e.: no “drifting”)midi-reader-writer
: reading + writing midi files, currently mostly a wrapper aroundmidly
polyphony
: I moved all the voice-stealing logic from earlier versions ofrsynth
to this crate. I'm happy with the design, but “it can use some love”.
I also split off two other crates which I will not mention by name because you shouldn't use them. They emerged in an attempt to implement a certain design that failed so spectacularly that it deserves a dedicated blog post.
This leads me to the lessons learned.
Lessons learned
I like learning from somebody else's mistakes more than learning from my own mistakes, the hard way. So I offer my “lessons learned” to you in the hopes that it helps you. Of course, your context may be different.
Use a simple design
Before this project, I asked myself “What's the best (most elegant, ...) design that gets the job done?” Now I ask myself: “What's the simplest design that gets the job done?” Usually, not all design requirements are known upfront and adding complexity at the start will only make your life harder. You don't have to believe me, but remember my words when you're fighting with the borrow checker.
Use a modular design
This obviously served me well, otherwise I would not have been able to show off so many cool crates that grew out of rsynth
.
This is a general thing, e.g. the lapce
editor uses the rope data structure from the now-abandoned xi
editor.
Another advantage is that it assists/forces you to have a modular design, which is easier to reason about since you don't need to understand the whole application in order to understand its parts.
Of course, the perfect design doesn't come on day one, so it's only natural when components start as modules, evolving with the rest of the application, to be split off in a separate crate only later when a certain level of stability has been reached.
Don't start your own project from scratch, build on somebody else's project
I don't know how Robbert van der Helm thinks about this since he named his project after the Not Invented Here syndrome, but taking over somebody else's project worked for me in this case. It brings in another view (with many details), it's a starting point and you get a co-developer and some visibility. Don't underestimate the effort it takes to reach the same level on your own.
Don't try to abstract away too much
In an intermediate design, rsynth
tried to abstract away ... too much.
This made the design overly complicated.
If, while designing an API, you struggle to give the API user control over things you are abstracting away, you may want to reconsider if it was a good idea to abstract this away in the first place.
I know this may be a little vague.
I would need another blog post to describe this more in depth.
Don't do as I do, but use ports instead
This lesson learned is specific to audio development.
Rsyth uses &[&[f32]]
and &mut[&mut[f32]]
to represent input and output channels, respectively.
In hindsight, I would have used ports like the lv2
or jack
crate.
This allows a much more flexible set-up.
When using &[&[f32]]
and &mut[&mut[f32]]
, you have to come up with something else to represent midi.
And controls.
And starting/stopping (in case of stand-alone applications).
And delay reporting.
And time/position.
And whatever current or future standard-specific input-output may come on your way.
A set-up using ports can accommodate for that more easily.
Meta-data isn't a compile-time constant
This lesson learned is specific to audio development.
Especially for stand-alone applications, where the meta-data may depend on the command-line arguments, meta-data isn't a compile-time constant.
Acknowledgments
First of, a big thanks to doomy for initiating the project and trusting me to take it over.
Also thanks to
- Matthias Geier for your source code contribution which cleaned up some code
- the authors of the crates that
rsynth
depends on - everybody who boosted my ego by starring
rsynth
on GitHub - the people from the Rust Audio community – I learned so much from you.