Dagn

Node graph based synthesizer

Siebencorgie published on
4 min, 608 words

Dagn

After working exclusively with graphics on Jakar and Tikan I wanted to try something new in the form of Dagn. A audio synthesizer based on a graphical node graph.

I think the idea mostly originated from my work with UnrealEngine and Blender that use node graph to view graph like structures. Till now I didn't get used to the timeline like view usual DAWs use for music. I think they fit piece wise music well, but whenever I try to make sounds my approach is more that of an analogue synthesizer performance. By combining effects and instruments I try to find interesting sounds, not necessarily music in the common sense.

For the synth I first came up with a execution graph library where each node represents a node on the synth. Code wise this looks something like this:

use std::sync::Arc;
use std::sync::Mutex;

use dager::edge::Edge;
use dager::executor::Executor;
use dager::node::AbstAggregator;
use dager::node::Aggregator;
use dager::node::Node;

struct Add;
impl Node for Add{
    type InSig = (f32, f32);
    type OutSig = [f32; 1];
    fn process(&mut self, input: Self::InSig) -> Self::OutSig {
	let (a,  b) = input;
	println!("adding: {}+{}", a, b);
	[a + b]
    }

    fn default(&mut self, aggregator: &mut dyn AbstAggregator){
	println!("Setting default of port 0 to 1");
	aggregator.set_default_value(0, Box::new(1.0 as f32)).expect("Failed to set default");
    }

    fn name<'a>(&'a self) -> &'a str {
	"Adder"
    }
}

struct Printer;
impl Node for Printer{
    type InSig = [f32; 1];
    type OutSig = [f32; 1];
    fn process(&mut self, input: Self::InSig) -> Self::OutSig {
	let [a] = input;
	println!("Got {}", a);
	[0.0]
    }

    fn name<'a>(&'a self) -> &'a str {
	"Printer"
    }
}


//Note graph:        |----|           |----|     |-------|
//     default: 1.0 -|add1|--\   1.0 -|add2| ----|printer|
//   from main: 2.0 -|----|   \-------|----|     |-------|
//
//
fn main(){
    let ex = Executor::new();
    
    let add_node1 = Arc::new(Mutex::new(Aggregator::from_node(Add)));
    let add_node2 = Arc::new(Mutex::new(Aggregator::from_node(Add)));
    let printer = Arc::new(Mutex::new(Aggregator::from_node(Printer)));
    
    Edge::connect(add_node1.clone(), 0, add_node2.clone(), 1).expect("Failed to connect edge");
    Edge::connect(add_node2.clone(), 0, printer.clone(), 0).expect("Failed to connect edge");
    
    //Should execute since the other edge is set by the default value
    add_node1.lock().unwrap().set_in_from_edge(ex.clone(), 1, Box::new(2.0 as f32)).expect("Failed to set input");
}

Apart from the actual sound this was a nice exercise in working with graphs (well DAGs, also known as the easy graphs) in Rust. I tried to use the type system of Rust as much as possible to express requirements of nodes and the graph execution. The final interface looks like this:

UI

It allows you to import a sound driver (in this case CPAL) to communicate with the driver. In audio, scheduling the audio buffer is even more important then scheduling frames for graphics. Therefore it is common to use a dedicated audio thread and let the driver call the program, instead of calling the driver from the program.

Fun fact: This uses my second widget toolkit called Neith. It was a big improvement compared to Widkan (the one used in Tikan).

Since it is a synthesizer audio is more relevant then graphics, therefore, have a look at the video:

While I am currently not working on the Synth anymore I plan on writing a second iteration. I have multiple improvements that I want to try out. Dagn uses a typed node graph, which means not only samples but also other data can be transmitted through the cables. While this opens up interesting non-numeric communication between nodes it is currently not intuitive to use.