synfx_dsp_jit/
lib.rs

1// Copyright (c) 2022 Weird Constructor <weirdconstructor@gmail.com>
2// This file is a part of synfx-dsp-jit. Released under GPL-3.0-or-later.
3// See README.md and COPYING for details.
4
5/*! synfx-dsp-jit is a specialized JIT compiler for digital (audio) signal processing for Rust.
6
7This library allows you to compile an simplified abstract syntax tree (AST) down to machine code.
8This crate uses the [Cranelift JIT compiler](https://github.com/bytecodealliance/wasmtime/tree/main/cranelift)
9for this task. For called Rust functions from the JIT code, either in form
10of stateful DSP nodes or stateless DSP functions, It removes any dynamic dispatch overhead.
11
12The result is packaged conveniently for you in a [DSPFunction] structure.
13
14One primary feature that is covered by this library is the state management of stateful
15nodes/components that can be called from the AST. By attaching a unique ID to your
16AST nodes that call stateful components (aka nodes), this library tracks already initialized
17nodes. It does this to allow you to re-compile the [DSPFunction] and make changes without
18the audio being interrupted (unless your changes interrupt it).
19
20Aside from the compiling process and state management this library also offers
21a (growing) standard library of common DSP algorithms.
22
23All this means this library is primarily directed towards the use case within a real time
24synthesis environment.
25
26You can practically build your own JIT compiled [Pure Data](https://puredata.info/) or
27[SuperCollider](https://supercollider.github.io/) clone with this. In case you
28put in the work of implementing all the DSP nodes and put a compiler on top of
29this JIT of course. Other notable projects in this direction are:
30
31- BitWig's "The Grid", which seems to use LLVM under the hood, either to AOT compiler the devices
32or even JIT compiling the Grid itself (I'm not sure about that).
33- [Gammou - polyphonic modular sound synthesizer](https://github.com/aliefhooghe/Gammou)
34
35This library is used for instance by [HexoDSP](https://github.com/WeirdConstructor/HexoDSP),
36which is a comprehensive DSP graph and synthesis library for developing a modular
37synthesizer in Rust, such as [HexoSynth](https://github.com/WeirdConstructor/HexoSynth).
38It is not the core of HexoDSP, but only provides a small optional part though.
39
40## Available DSP Nodes/Functions And Language Definition
41
42For this please consult the crate documentation in the standard library module: [crate::stdlib].
43
44## Quick Start API
45
46To get you started quickly and learn how to use the API I recommend the [instant_compile_ast]
47function. But be aware that this function recreates the whole [DSPNodeContext]
48on each compilation, so there is no state tracking for you.
49
50```
51 use synfx_dsp_jit::build::*;
52 use synfx_dsp_jit::instant_compile_ast;
53
54 let (ctx, mut fun) = instant_compile_ast(
55     op_add(literal(11.0), var("in1"))
56 ).expect("No compile error");
57
58 fun.init(44100.0, None); // Sample rate and optional previous DSPFunction
59
60 let (sig1, sig2, res) = fun.exec_2in_2out(31.0, 10.0);
61
62 // The result should be 11.0 + 31.0 == 42.0
63 assert!((res - 42.0).abs() < 0.0001);
64
65 // Yes, unfortunately you need to explicitly free this.
66 // Because DSPFunction might be handed around to other threads.
67 ctx.borrow_mut().free();
68```
69
70## DSP JIT API Example
71
72Here is a more detailed example how the API can be used with state tracking.
73
74```
75use synfx_dsp_jit::*;
76use synfx_dsp_jit::build::*;
77
78
79// First we need to get a standard library with callable primitives/nodes:
80let lib = get_standard_library();
81
82// Then we create a DSPNodeContext to track newly created stateful nodes.
83// You need to preserve this context across multiple calls to JIT::new() and JIT::compile().
84let ctx = DSPNodeContext::new_ref();
85
86// Create a new JIT compiler instance for compiling. Yes, you need to create a new one
87// for each time you compile a DSPFunction.
88let jit = JIT::new(lib.clone(), ctx.clone());
89
90// This example shows how to use persistent variables (starting with '*')
91// to build a simple phase increment oscillator
92let ast = stmts(&[
93    assign("*phase", op_add(var("*phase"), op_mul(literal(440.0), var("$israte")))),
94    _if(
95        op_gt(var("*phase"), literal(1.0)),
96        assign("*phase", op_sub(var("*phase"), literal(1.0))),
97        None,
98    ),
99    var("*phase"),
100]);
101
102let mut dsp_fun = jit.compile(ASTFun::new(ast)).expect("No compile error");
103
104// Initialize the function after compiling. For proper state tracking
105// you will need to provide the previous DSPFunction as second argument to `init` here:
106dsp_fun.init(44100.0, None);
107
108// Create some audio samples:
109let mut out = vec![];
110for i in 0..200 {
111    let (_, _, ret) = dsp_fun.exec_2in_2out(0.0, 0.0);
112    if i % 49 == 0 {
113        out.push(ret);
114    }
115}
116
117// Just to show that this phase clock works:
118assert!((out[0] - 0.0099).abs() < 0.0001);
119assert!((out[1] - 0.4988).abs() < 0.0001);
120assert!((out[2] - 0.9877).abs() < 0.0001);
121assert!((out[3] - 0.4766).abs() < 0.0001);
122
123ctx.borrow_mut().free();
124```
125
126## DSP Engine API
127
128The [crate::engine::CodeEngine] API is a convenience API for dealing with
129an audio/real time thread. When you want to compile the function on some non real time thread
130like a GUI or worker thread, and use the resulting DSP function in an audio thread to produce
131audio samples.
132
133```
134use synfx_dsp_jit::engine::CodeEngine;
135use synfx_dsp_jit::build::*;
136
137// Create an engine:
138let mut engine = CodeEngine::new_stdlib();
139
140// Retrieve the backend:
141let mut backend = engine.get_backend();
142
143// This should actually be in some audio thread:
144std::thread::spawn(move || {
145    backend.set_sample_rate(44100.0);
146
147    loop {
148        backend.process_updates(); // Receive updates from the frontend
149
150        // Generate some audio samples here:
151        for frame in 0..64 {
152            let (s1, s2, ret) = backend.process(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
153        }
154    }
155});
156
157// Upload a new piece of code whenever you see fit:
158engine.upload(call("sin", 1, &[literal(1.0)])).unwrap();
159
160let mut not_done = true;
161while not_done {
162    // Call this regularily!!!!
163    engine.query_returns();
164
165    // Just for ending this example:
166    not_done = false;
167}
168```
169
170*/
171
172mod ast;
173#[allow(rustdoc::private_intra_doc_links)]
174mod context;
175mod jit;
176pub mod locked;
177pub mod engine;
178pub mod stdlib;
179
180pub use ast::{build, ASTBinOp, ASTFun, ASTNode};
181pub use context::{
182    DSPFunction, DSPNodeContext, DSPNodeSigBit, DSPNodeType, DSPNodeTypeLibrary, DSPState,
183};
184pub use jit::{get_nop_function, JITCompileError, JIT};
185pub use stdlib::get_standard_library;
186
187use std::cell::RefCell;
188use std::rc::Rc;
189
190/// This is a little helper function to help you getting started with this library.
191///
192/// If you plan to re-compile your functions and properly track state, I suggest you
193/// to explicitly create a [DSPNodeTypeLibrary] with [get_standard_library] and a
194/// [DSPNodeContext] for state tracking.
195///
196///```
197/// use synfx_dsp_jit::build::*;
198/// use synfx_dsp_jit::instant_compile_ast;
199///
200/// let (ctx, mut fun) = instant_compile_ast(
201///     op_add(literal(11.0), var("in1"))
202/// ).expect("No compile error");
203///
204/// fun.init(44100.0, None); // Sample rate and optional previous DSPFunction
205///
206/// let (sig1, sig2, res) = fun.exec_2in_2out(31.0, 10.0);
207///
208/// // The result should be 11.0 + 31.0 == 42.0
209/// assert!((res - 42.0).abs() < 0.0001);
210///
211/// // Yes, unfortunately you need to explicitly free this.
212/// // Because DSPFunction might be handed around to other threads.
213/// ctx.borrow_mut().free();
214///```
215pub fn instant_compile_ast(
216    ast: Box<ASTNode>,
217) -> Result<(Rc<RefCell<DSPNodeContext>>, Box<DSPFunction>), JITCompileError> {
218    let lib = get_standard_library();
219    let ctx = DSPNodeContext::new_ref();
220    let jit = JIT::new(lib, ctx.clone());
221    Ok((ctx, jit.compile(ASTFun::new(ast))?))
222}