Uda Block Designer
One of the topics I hoped to blog about was my design process for software. This post will dive into some speculative planning I've been doing for some open source software I've been thinking about.
Lately I've been working on a couple of DSP libraries in Rust. One is Yagi, which will provide core DSP functions (filters, FFT, modulation, and other math). The other is Uda, which will provide a GNU radio-like block graph with integration to egui
for charting. Yagi will provide the math while Uda will provide an execution framework.
Recently I've been working on Uda. I started with a proof-of-concept implementation that plots some samples with the Soapy SDR library. It loads samples from the Soapy device, feeds into an FFT from my Yagi library, and then hands them off to an egui chart. This works, but all of the running of blocks is implemented specifically for this pipeline. In other words, it is not flexible in a way that could run other blocks, but it's a good way to help piece together what a generic runner should look like.
The next part is the hard part -- I need to actually now design a system that can take a generic graph design from the library user and then shuffle samples around between blocks. I've decided not to reference other libraries that do this. Partly maybe this is just pride or stubbornness, but I find this also helps me make sure that I build something for my purposes, whatever they may be. Coming up with the design flexes some important muscles and I don't want to deprive myself of that.
I ended up working backwards a bit on the initial implementation of Uda. I decided to split the problem into two phases. The first phase is just purely a design phase where the user configures blocks and establishes connections from block to block. Once this is completed, a Runner
is created and we enter the second phase. The Runner
's task is to actually realize the moving of samples and invoking of blocks.
Looking ahead, I expect these blocks will run on various threads. I settled on running one thread for all of the DSP and compute, a thread for each device or audio endpoint, and the GUI thread itself (main thread).
One of the block designer's goals will be to decide which thread to place each block onto and to automatically insert extra blocks for cross-thread communication. Using the previous example, this suggests a pipeline that looks something like Soapy device -> mpsc sender 0 -> mpsc receiver 0 -> FFT -> mpsc sender 1 -> mpsc receiver 1 -> egui chart
. This is a lot of channel blocks for this little toy example, but I suspect this will make sense as the workload on each thread grows. It should also hopefully keep everything unblocked and running smoothly on the GUI.
Because this will be handled by the designer, the threading should hopefully be largely invisible to the library user. I've been trying to come up with the user-facing API. As an example, I'm currently thinking something like
// let mut graph = Graph::new();
// let mut chart = SpectrumPlot::new();
// let source = graph.create_block(BlockTypeEnum::SoapySdrSource{sample_rate: 1000000})?;
// let fft = graph.create_block(BlockTypeEnum::FftBlock{window_size: 1024})?;
// let chart_block = graph.create_block(BlockTypeEnum::ChartBlock{chart: &chart})?;
// graph.connect(source.output(0), fft.input(0))?;
// graph.connect(fft.output(0), chart.input(0))?;
// let runner = graph.create_runner();
// runner.run();
There's still lots more to do, and I'm sure this example will continue to evolve. It's been a fun project so far and I'm looking forward to having an actual library to publish soon.