Weird Constructor / Posts / HexoSynth 2022 - Devlog #7 - The DSP JIT Compiler

HexoSynth 2022 - Devlog #7 - The DSP JIT Compiler

The HexoSynth modular synthesizer (programmed in Rust) was extended with a proof of concept JIT compiler for DSP.

If you don't want to dig through the detailed log:

TLDR: Skip to the Devlog 7 Conclusion Section

Detailed Log

This is the day by day log of my progress. If this is too verbose for your interest, please skip down to the Devlog 7 Conclusion Section.

2022-07-27

On this day I published the Devlog #6 article and refined the oscilloscope more.

HexoSynth: I finished work on the Scope today. The offset and gain parameters are all wired up now too:

HexoSynth Oscilloscope with Offset and Gain Parameters

Also the trigger threshold is now displayed in the oscilloscope:

HexoSynth Oscilloscope Trigger Threshold
HexoSynth Oscilloscope Trigger Threshold

HexoDSP: At the end of the day the Scope node was officially finished and fully covered by automated tests in the test suite. This meant for me, that I could start focusing on the next big feature.

As I got a few days of vacation ahead and a few quiet evenings for myself, I decided to look into WBlockDSP integration into HexoSynth. After seeing another DSP JIT project with

2022-07-28

WBlockDSP: I've invested this day into WBlockDSP (which I later partially refactored into synfx-dsp-jit ) and made a lot of progress in the DSP node JIT API. The API allows constructing an AST (Abstract Syntax Tree) as intermediate DSP code representation. That AST can then be JIT compiled into one function that can be executed at runtime. The API also allows specifying a library of DSP node types that the AST can refer to and construct. The API also takes care of persisting state properly across multiple recompilations of code.

Those features will all allow you to edit the DSP function at runtime in HexoSynth and immediately see and hear the result.

2022-07-29

WBlockDSP: Finished the JIT API today. Still some automated tests to implement and some macro magic for defining DSP nodes. And lots of little things like documentation to do. But the core is implemented now.

WBlockDSP JIT core is working
WBlockDSP JIT core is working

HexoSynth: A few hours later the integration of WBlockDSP with HexoSynth/HexoDSP was done:

2022-07-30

HexoSynth: I've integrated the block code editor veeeeery rudimentary. That is not where it will stay, it will be toggle-able. And the context menus don't have a proper layout/styling yet.

But that is roughly the direction it is going to right now.

I'll keep the feature set minimal for now, and also put primary focus back to the hexagonal DSP nodes. This little JIT DSP thing is really just a little feature in the whole thing. The point is to combine low level DSP elements for little experiments for now. Of course there are tons of features you could add. like more feedback of values in the DSP code, and all that is not impossible to add. also more convenient editing or making "subroutines" are all nice features, but will not be included for now. eventually they might, depends on user interest (eg. mine).

I'll however factor out the whole JIT stuff into the synfx-dsp-jit crate, and I will factor out some of the generic DSP utilities I've accumulated in HexoDSP into synfx-dsp.

HexoSynth BlockCode user workflow proof of concept

For now I really just want to establish this feature. Maybe I extend it in future, maybe someone else wants to extend it. Or maybe takes the building blocks in synfx-dsp-jit and builds an alternative tool with it.

2022-07-31

synfx-dsp-jit: I've spent some time on factoring out the JIT into it's own crate. I've then documented that crate and published it: synfx-dsp-jit on crates.io. Here is an example from it's documentation. You can formulate simple little DSP code (mostly arithmetics) and call out to functions defined in Rust (not shown in the example though).

 use synfx_dsp_jit::build::*;
 use synfx_dsp_jit::instant_compile_ast;

 let (ctx, mut fun) = instant_compile_ast(
     op_add(literal(11.0), var("in1"))
 ).expect("No compile error");

 fun.init(44100.0, None); // Sample rate and optional previous DSPFunction

 let (sig1, sig2, res) = fun.exec_2in_2out(31.0, 10.0);

 // The result should be 11.0 + 31.0 == 42.0
 assert!((res - 42.0).abs() < 0.0001);

 // Yes, unfortunately you need to explicitly free this.
 // Because DSPFunction might be handed around to other threads.
 ctx.borrow_mut().free();

I hope someone else finds it useful. May it be at least as example or starting point for their own projects.

synfx-dsp-jit: I've published a new version of the synfx-dsp-jit crate (version 0.5.2), I discovered that I need to handle multiple return values somehow. And this is what I came up with:

synfx-dsp-jit multiple return values
synfx-dsp-jit multiple return values

HexoDSP: After the work on synfx-dsp-jit I've started to work on the block language compiler. There is not much to show, I'm still right in the middle, but the first lowering step from the block language towards the JIT AST has been done. Next up I'm trying to write the last compilation step and output JIT code finally. Afterwards I'll start and refine the language itself and nail down the syntax and semantics more precisely.

HexoDSP Block Language Compiler Development
HexoDSP Block Language Compiler Development

2022-08-01

HexoDSP: Today I started the final translation step of the BlockCode to JIT compiler. Already got the first values being generated in the DSP backend of HexoDSP. Unfortunately there is still a bug somewhere in the test itself.

synfx-dsp-jit: Worked a bit on the DSPNodeType trait. Because I prioritize documentation of things quite high, I added a few methods to that trait to help building a documented standard library of DSP nodes. The port names are also very helpful for any compiler that wants to target synfx-dsp-jit, because referring to ports by their index is a maintenance horror. If you have names, you can grep the source code easier.

Before it was this:

stateful_dsp_node_type! {
    AccumNodeType, AccumNodeState => process_accum_nod "accum" "vvSr"
}

It will not tell you anything about how exactly to use it. Compare it with this, which should be immediately more clear what to do with the "accum" node:

stateful_dsp_node_type! {
    AccumNodeType, AccumNodeState => process_accum_nod "accum" "vvSr"
    doc
    "This is a simple accumulator. It sums up it's input and returns it. \
     You can reset it's state if you pass a value >= 0.5 into 'reset'."
    inputs
    0 "input"
    1 "reset"
    outputs
    0 "sum"
}

2022-08-02

HexoDSP/synfx-dsp-jit: More work on the BlockCode to JIT compiler in HexoDSP. Also added a few minor features to synfx-dsp-jit. Such as access to the persistent variables in the DSPFunction.

HexoSynth: Finished the very bare minimum of the JIT compiler now, and wanted to finally see that HexoSynth can generate audio data from the JIT compiled function. I still had to finish the text entry widget WLambda API for HexoSynth. Which was quickly done. Not soon after I was able to finally go through the complete technology stack and generate audio samples from the BlockCode visual programming language in the HexoSynth frontend. It does not look spectacular, but if you look closely, you can see the signals in the oscilloscope change.

HexoSynth JIT compiled code runs in audio thread, generating samples displayed by the oscilloscope

YouTube Sound Demos

More playing around with the sample player and the new oscilloscope:

Devlog 7 Conclusion

The oscilloscope was finished and I love it. I'm glad I invested time into developing it. I will also benefit from that a lot once I get back to developing more DSP nodes/modules for HexoDSP/HexoSynth.

HexoSynth Oscilloscope finished
HexoSynth Oscilloscope finished

With the displayed time range shortened:

HexoSynth Oscilloscope zoomed in signal
HexoSynth Oscilloscope zoomed in signal

And in it's minimized form:

HexoSynth Oscilloscope minimized
HexoSynth Oscilloscope minimized

I could not resist the temptation that I've been pondering over the last week: The visual JIT compiled DSP programming language I named WBlockDSP. The feature set will be limited. I don't want to develop a full blown Pure Data or Super Collider alternative. I limit the scope to be a supplemental programmable DSP node for HexoSynth. A little escape hatch to implement your own formulas, to write interesting control signal modulation for instance. Or try out some simple wave shaping algorithms.

I put up that scope limitation purely because I don't have the time or resources to further explore the possibility of a more general purpose (visual) audio programming language developed in Rust. Feel free to contact me if you feel inspired. If you are interested and want to contribute or try to develop this idea in a different direction (maybe a more classical visual programming language with wires? Maybe a text based audio programming language like SuperCollider or Faust?).

The inspiration for the WBlockDSP programmable DSP nodes for HexoSynth came from the DROID - Universal CV Processor Eurorack synthesizer module. Those are basically programmable Eurorack modules. You could customize their functions using files on an SD card in some INI file format.

DROID Eurorack Modules - Inspiration for HexoSynth WBlockDSP
DROID Eurorack Modules - Inspiration for HexoSynth WBlockDSP

My estimations are, that it will take me another 2-3 weeks to stabilize WBlockDSP this feature so far that I can leave it alone for a while. It probably won't be finished anytime, but I hope this lays a foundation for further experimentation. Maybe it's a dead end, or maybe I will expand on that. In any case, it lead to the release of a new crate: synfx-dsp-jit - a specialized JIT compiler for digital (audio) signal processing for Rust. Maybe it helps, maybe at least as example on how to use the Cranelift JIT compiler.

All that means that the roadmap of last week is still mostly unchanged:

  • Stabilize WBlockDSP.
  • Add more automated test cases for the UI workflow.
  • Rewrite the online help.
  • Add inserting DSP chains that are to be pre-defined. And also inserting random DSP chains for a more explorative/experimental workflow.
  • Finish the nih-plug integration. That means a better integration of HexoSynth as VST3/CLAP plugin into your favorite DAW.
  • Add back editing CV widgets.

Contact

In case you find this project interesting or have questions, you can reach me via Discord these days, check out the #hexosynth channel in the Rust Audio Discord. Optionally I'm also online on IRC (via Matrix) in the Linux Audio Developer channel #lad (nickname 'wct') on Libera.Chat: irc:#lad@irc.libera.chat.

Links

Weird Constructor Avatar
Weird Constructor

Nice end 30 y/o guy, geek and F(L)OSS developer that messes with: Linux, Windows, Networking, Interpreters/Compilers, Games, Audio, Music, GUIs, C/C++, Rust, Scheme/Lisp, 3D printing, electronics and more.


For updates follow me on Mastodon

All the F(L)OSS development on my projects happens in my spare time. If you find what I do useful, entertaining or just want to support me, you can do this via Liberapay:

Donate using Liberapay