ZigRadio is a framework for building signal processing flow graphs. A flow graph is a directed graph of blocks that processes data samples. Samples in a flow graph originate at source blocks, are manipulated through intermediate processing blocks, and terminate at sink blocks. This paradigm — also called dataflow programming — is useful for software-defined radio, because it allows you to model the architectures of conventional hardware radios.
In a software-defined radio flow graph, source and sink blocks tend to implement some kind of I/O, like reading samples from an SDR dongle, or writing samples to an audio device, while processing blocks tend to be computational, like filters and multipliers.
ZigRadio blocks have data types associated with their input and output ports.
For example, the
ComplexMagnitudeBlock has a
complex-valued input port of type std.math.Complex(f32) and real-valued
output port of type f32. Block ports can use any Zig type, including custom
types. Many generic processing blocks, such as the
MultiplyBlock, support compile-time
type parameterization of their ports, and can be instantiated for a variety of
types.
ZigRadio blocks have a sample rate associated with them. This is the rate at which the discrete samples are spaced in time, relative to one another, but not the rate that they are computationally produced, processed, or consumed by the framework. Source blocks define their sample rates, while downstream blocks inherit — and possibly modify — the sample rate of their upstream block. For example, upsampler and downsampler blocks will multiply and divide the sample rate, respectively.
ZigRadio propagates sample rates between blocks for you, and ensures that sample rates match across multiple inputs. Blocks can access their runtime sample rate to perform sample rate dependent initialization and processing. Since blocks know their sample rate, blocks can accept parameters in terms of actual frequencies, e.g. cut-off frequencies in hertz for a filter.
While some flow graphs implement a continuously running system (e.g. an FM broadcast radio receiver), ZigRadio flow graphs are not required to run forever. When a source terminates, the framework will gracefully collapse the flow graph as the final samples propagate their way though the graph. This allows you to build utilities that process finite inputs, like files, to completion.
In this example, we will create a simple flow graph to double the frequency of a tone. It will demonstrate the basic mechanics of instantiating blocks, connecting blocks, and running a flow graph.
This flow graph mixes a 440 Hz (A4) tone with itself to create the sum and difference frequency tones at 0 Hz and 880 Hz, respectively. This output is then filtered with a highpass filter to leave the 880 Hz (A5) tone, which is played out the speakers with a PulseAudio sink.
const std = @import("std");
const radio = @import("radio");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
var source = radio.blocks.SignalSource.init(radio.blocks.SignalSource.WaveformFunction.Cosine, 440, 44100, .{});
var mixer = radio.blocks.MultiplyBlock(f32).init();
var filter = radio.blocks.SinglepoleHighpassFilterBlock(f32).init(100);
var sink = radio.blocks.PulseAudioSink(1).init();
var top = radio.Flowgraph.init(gpa.allocator(), .{ .debug = true });
defer top.deinit();
try top.connectPort(&source.block, "out1", &mixer.block, "in1");
try top.connectPort(&source.block, "out1", &mixer.block, "in2");
try top.connect(&mixer.block, &filter.block);
try top.connect(&filter.block, &sink.block);
try top.start();
radio.platform.waitForInterrupt();
_ = try top.stop();
}
Run the example within the ZigRadio source tree with:
$ zig run -lc --dep radio -Mroot=frequency_doubler.zig -Mradio=src/radio.zig
The flow graph can be terminated with SIGINT (e.g. Ctrl-C).
const std = @import("std");
The first line of the example imports the Zig standard library.
const radio = @import("radio");
The second line imports the radio package containing ZigRadio. This package
exposes all ZigRadio blocks, as well the facilities to create flow graphs,
blocks, and types.
var source = radio.blocks.SignalSource.init(radio.blocks.SignalSource.WaveformFunction.Cosine, 440, 44100, .{});
var mixer = radio.blocks.MultiplyBlock(f32).init();
var filter = radio.blocks.SinglepoleHighpassFilterBlock(f32).init(100);
var sink = radio.blocks.PulseAudioSink(1).init();
These lines instantiate each block of the flow graph. Note that the sample rate is only required for the source block; all other blocks inherit their sample rate through the connections in the flow graph.
var top = radio.Flowgraph.init(gpa.allocator(), .{ .debug = true });
defer top.deinit();
try top.connectPort(&source.block, "out1", &mixer.block, "in1");
try top.connectPort(&source.block, "out1", &mixer.block, "in2");
try top.connect(&mixer.block, &filter.block);
try top.connect(&filter.block, &sink.block);
The next lines instantiate a flow graph with a default general purpose allocator, and connect the blocks within the flow graph.
The first two connections demonstrate the explicit connection syntax. For
example, try top.connectPort(&source.block, "out1", &mixer.block, "in1);,
where source's output port named out1 is connected the mixer's input port
named in1. In this case, the mixer has two inputs, in1 and in2, so we use
the explicit connection syntax to connect the source's one output to both of
the mixer's inputs.
The third and fourth connections demonstrate the linear block connection syntax, which is used to connect the first output to the first input of two blocks. This syntax is convenient for connecting blocks that only have one input and output, which is most blocks.
try top.start();
radio.platform.waitForInterrupt();
_ = try top.stop();
The last lines of the example start the flow graph, wait for the user's
SIGINT signal, and then stop the flow graph. The flow graph will run
indefinitely until the user raises SIGINT (e.g. Ctrl-C).
Building flow graphs with ZigRadio is a matter of choosing the right blocks and connecting them. The ZigRadio Reference Manual documents all packaged blocks, including a description of their operation, their arguments, and their input/output port names and data types.
ZigRadio blocks use native Zig types for their input and output ports. Common data types include:
std.math.Complex(f32), for complex-valued samplesf32, for real-valued samplesu8, for byte samplesu1, for bit samplesIn addition, users can create custom data types to represent complex structures like digital protocol frames.
ZigRadio blocks can also leverage the C APIs of external libraries for custom processing, acceleration, or I/O.
The Creating Flow Graphs guide describes the connecting, running, and modifying flow graphs.
The Creating Blocks guide describes creating blocks and data types.