A few weeks ago I started working on s-rack, a modular synthesizer written in Rust. An earlier post describes my first week of development.
The big news since then is that this project now runs inside a web browser through WASM as a compilation target.
I followed the example at eframe_template.
I found that Trunk is a builder for
WASM apps. During development it will watch your code, recompile it when it
changes, inject a link to the WASM binary into an HTML template you
provide, and cause the page to reload. It will also generate a production
deployable build in a dist/
folder when you are ready.
I refactored out the audio setup code into its own struct and associated
functions so that the code in main
-- which is different on native than
in WASM -- doesn't have to be concerned with it. After making this change
and making it so the audio engine only starts after a user interaction on web,
the WASM version of s-rack was building without any issue.
Input and output handles
When I wrote about this project a week ago, I anticipated that I would need to create widgets with drag and drop behaviors to act as the input and output "plugs" next to modules.
I found that with egui it was very easy to create widget like objects that
could take an egui Ui
, call its .allocate_space()
, and test for interactions
with the pointer including with drag and drop.
In fact, it was extremely easy to implement dragging an output into an input or
vice versa, as the Response.dnd_*
functions allow payloads of any type. For
example, I used Response.dnd_set_drag_payload
on each port widget to make
the information about the port (a pointer to the module and which port it is)
draggable to other ports that can receive it with Response.dnd_release_payload
.
Oscillator aliasing
As I predicted, naively generating waveforms with fast transitions like a square wave or sawtooth wave results in undesired artifacts -- extra tones which seem unrelated to the tone being generated.
Luckily, adopting a PolyBLEP function to generate corrections for each hard transition turned out to be really easy and worked like magic.
Warning! Annoying noises!
Execution planning
Though after the user has built a patch a module may have more than one (or no) connections to other modules, when the audio driver asks for audio, we want to run each module's audio processing code once and only once. Additionally, we want to run the modules in an optimal order.
Consider the graph above, made up of modules. (Direction of arrows signify a dependency, not the direction of signal flow, which is reverse.) Modules take input buffers of a certain size, process them together with their internal state, and produce one or more output buffers. A produces an output buffer consumed by B.
Periodically the sound library asks for a new buffer of audio to send to the sound driver, which triggers the execution of all modules in the …