# Creating Blocks
This guide describes the basic concepts behind blocks, data types, and vectors,
and demonstrates how to create blocks and data types.
If you're new to Lua, don't worry. Lua is a simple, consistent language that
can be picked up easily. Check out [Learn Lua in Y
Minutes](https://learnxinyminutes.com/docs/lua/) for a quick introduction.
## Table of Contents
* [Hierarchical Blocks](#hierarchical-blocks)
* [Using `CompositeBlock` directly](#using-compositeblock-directly)
* [Deriving from `CompositeBlock`](#deriving-from-compositeblock)
* [Basic Types](#basic-types)
* [Vectors](#vectors)
* [Blocks](#blocks)
* [Methods](#methods)
* [Template](#template)
* [Type Signatures](#type-signatures)
* [Example](#example)
* [Testing](#testing)
* [Benchmarking](#benchmarking)
* [FFI](#ffi)
* [Faster `MultiplyBlock`](#faster-multiplyblock)
* [BFSK Modulator](#bfsk-modulator)
* [Custom Types](#custom-types)
* [`CStruct` types](#cstruct-types)
* [`Object` types](#object-types)
* [Manipulating Bits](#manipulating-bits)
* [Development Tips](#development-tips)
## Hierarchical Blocks
The simplest kind of custom block that can be created in LuaRadio is a
hierarchical block.
A hierarchical block is a composition of several blocks into a single block,
which abstracts the underlying flow graph and provides a simpler input/output
port interface. Creating a hierarchical block is a matter of instantiating
blocks, connecting them, and defining the boundary input and output ports.
Hierarchical blocks have no runtime overhead in LuaRadio; they're simply a
convenience. When running a flow graph containing a hierarchical block,
LuaRadio runs its internal blocks as if they were directly connected into the
top-level flow graph.
There are two ways to create a hierarchical block. The first is to use
[`CompositeBlock`](0.reference-manual.md#compositeblock) directly, and the
second is to derive a block class from `CompositeBlock`.
In the example below, we'll create a hierarchical block both ways for a mono
Wideband FM Demodulator for FM broadcast radio, described by the following flow
graph:
### Using `CompositeBlock` directly
``` lua
local radio = require('radio')
function wbfm_mono_demodulator_factory(tau)
-- Blocks
local fm_demod = radio.FrequencyDiscriminatorBlock(1.25)
local af_filter = radio.LowpassFilterBlock(128, 15e3)
local af_deemphasis = radio.FMDeemphasisFilterBlock(tau)
local demodulator = radio.CompositeBlock()
-- Connections
demodulator:connect(fm_demod, af_filter, af_deemphasis)
-- Type signature
demodulator:add_type_signature({block.Input("in", radio.types.ComplexFloat32)},
{block.Output("out", radio.types.Float32)})
-- Aliases
demodulator:connect(demodulator, "in", fm_demod, "in")
demodulator:connect(demodulator, "out", af_deemphasis, "out")
return demodulator
end
```
In this example, we create a function `wbfm_mono_demodulator_factory()` which
returns a `CompositeBlock` that can be used as an Wideband FM Mono Demodulator
in a flow graph. The argument to the function specifies the time constant of
the FM De-emphasis filter (typically 75 μS in the Americas and South Korea, and
50 μS everywhere else).
``` lua
-- Blocks
local fm_demod = radio.FrequencyDiscriminatorBlock(1.25)
local af_filter = radio.LowpassFilterBlock(128, 15e3)
local af_deemphasis = radio.FMDeemphasisFilterBlock(tau)
local demodulator = radio.CompositeBlock()
-- Connections
demodulator:connect(fm_demod, af_filter, af_deemphasis)
```
The blocks section instantiates the internal blocks of the demodulator, as well
as a `CompositeBlock` that will hold their composition. The connections section
connects the internal blocks. This procedure is identical to building a
top-level flow graph, as seen in the [Getting
Started](2.getting-started.md#example) guide.
``` lua
-- Type signature
demodulator:add_type_signature({block.Input("in", radio.types.ComplexFloat32)},
{block.Output("out", radio.types.Float32)})
```
The type signature section adds a type signature that defines the names and
data types of the boundary input/output ports of the hierarchical block. This
step creates the external interface to the hierarchical block, so we may
connect other blocks to it.
The first argument of
[`add_type_signature()`](0.reference-manual.md#blockadd_type_signatureinputs-outputs-process_funcnil-initialize_funcnil)
is an array of [`radio.block.Input()`](0.reference-manual.md#input)
descriptors, which specifies the name and data type of each input port. The
second argument is the analogous for output ports, with
[`radio.block.Output()`](0.reference-manual.md#output) descriptors. In this
case, there is one input named `in` of type `ComplexFloat32`, and one output
named `out` of type `Float32`. We may call `add_type_signature()` as many times
as we need to register multiple type signatures of different data types as
needed, but they must all share the same input/output port count and names.
``` lua
-- Aliases
demodulator:connect(demodulator, "in", fm_demod, "in")
demodulator:connect(demodulator, "out", af_deemphasis, "out")
```
The aliases section aliases the hierarchical block input/output ports we
defined in `add_type_signature()` to concrete input/output ports of its
internal blocks. The first one aliases the demodulator's `in` port to the
frequency discriminator's `in` port. The second aliases the demodulator's `out`
port to the de-emphasis filter's `out` port. Note that this invocation of
`connect()` differs from previous invocations, in that the `CompositeBlock`
passes itself in as a block for connection.
We can then instantiate and use our Wideband FM Mono Demodulator in a flow
graph:
``` lua
radio.CompositeBlock():connect(
radio.RtlSdrSource(88.5e6 - 250e3, 1102500), -- RTL-SDR source, offset-tuned to 88.5MHz-250kHz
radio.TunerBlock(-250e3, 200e3, 5), -- Translate -250 kHz, filter 200 kHz, decimate by 5
wbfm_mono_demodulator_factory(75e-6), -- Wideband FM Mono Demodulator
radio.DownsamplerBlock(5), -- Downsample by 5
radio.PulseAudioSink(1) -- Play to system audio with PulseAudio
):run()
```
### Deriving from `CompositeBlock`
Instead of building our Wideband FM Mono Demodulator composition in a function,
we can build it in a new block class derived from `CompositeBlock`. The
resulting block class will look and feel like any other LuaRadio block.
``` lua
local radio = require('radio')
local WBFMMonoDemodulator = radio.block.factory('WBFMMonoDemodulator', radio.CompositeBlock)
function WBFMMonoDemodulator:instantiate(tau)
radio.CompositeBlock.instantiate(self)
-- Blocks
local fm_demod = radio.FrequencyDiscriminatorBlock(1.25)
local af_filter = radio.LowpassFilterBlock(128, 15e3)
local af_deemphasis = radio.FMDeemphasisFilterBlock(tau)
-- Connections
self:connect(fm_demod, af_filter, af_deemphasis)
-- Type signature
self:add_type_signature({block.Input("in", radio.types.ComplexFloat32)},
{block.Output("out", radio.types.Float32)})
-- Aliases
self:connect(self, "in", fm_demod, "in")
self:connect(self, "out", af_deemphasis, "out")
end
```
The third line manufactures a new block class with name "WBFMMonoDemodulator"
and parent class `CompositeBlock`. The name string is used for debugging
purposes.
The remaining lines populate the `instantiate()` constructor of the
manufactured block class. This method is called when the block is instantiated,
e.g. `WBFMMonoDemodulator(...)`. The inner workings are basically the same as
the function factory above, except that any reference to `demodulator` is
replaced with `self`, since the class itself is a `CompositeBlock`.
We can use this Wideband FM Mono Demodulator in the same example as before,
after replacing `wbfm_mono_demodulator_factory(...)` with
`WBFMMonoDemodulator(...)`. This Wideband FM Mono Demodulator will behave
identically, but will have the added benefits of a descriptive block name and
being an instance of a well-defined block class.
## Basic Types
Before we dive in to implementing blocks, we'll first need to take a look at
LuaRadio's basic types and how to create and manipulate vectors of them.
LuaRadio has four basic types:
[`ComplexFloat32`](0.reference-manual.md#complexfloat32),
[`Float32`](0.reference-manual.md#float32), [`Bit`](0.reference-manual.md#bit),
[`Byte`](0.reference-manual.md#byte). Each instance of these types represents
one element, so a `radio.types.ComplexFloat32(3, 2)` is a complex number with
real value of 3 and imaginary value of 2, and `radio.types.Float32(123.0)` is a
real number with a value of 123.0. These types are LuaJIT ctypes, distinct from
any of the Lua primitive types.
Each basic type is backed by a C structure type. This is primarily for
implementation reasons, so that LuaRadio can associate a metatable of
operations with it, and so that these instances are distinct from other
occurrences of the underlying C type (e.g. `float`, `uint8_t`).
The C types for the four basic types are:
``` c
/* radio.types.ComplexFloat32 */
typedef struct {
float real;
float imag;
} complex_float32_t;
/* radio.types.Float32 */
typedef struct {
float value;
} float32_t;
/* radio.types.Bit */
typedef struct {
uint8_t value;
} bit_t;
/* radio.types.Byte */
typedef struct {
uint8_t value;
} byte_t;
```
Despite the structure wrapper, these types are compatible with the C types
`float complex`, `float`, `uint8_t`, `uint8_t`, respectively.
These basic types have metatables associated with them, that define basic
operations on the type:
``` lua
local a = radio.types.ComplexFloat32(3, 2)
print(a) --> ComplexFloat32
print(a + a) --> ComplexFloat32
print(a * a) --> ComplexFloat32
print(a:scalar_mul(2)) --> ComplexFloat32
print(a:abs()) --> 3.6055512428284
print(a:arg()) --> 0.58800262212753
```
See the [LuaRadio Reference Manual](0.reference-manual.md#basic-types) for all
operations defined on the basic types.
## Vectors
It would be inefficient for blocks to process one sample at a time, as the
overhead of serializing the sample and calling the block to process it would
exceed the cost of processing it. Instead, blocks operate on a vector of
samples at a time, to amortize the overhead of serialization.
A vector is a simple container class for a dynamically-allocated contiguous C
array, with only a few auxiliary methods for string representation, resizing,
and appending. Each basic type provides two static methods for creating a
vector of itself: `.vector(num)` for a zero-initialized vector, or
`.vector_from_array(arr)` for a vector initialized from a Lua array.
The example below illustrates the important uses of the Vector:
``` lua
-- Create a length 4, zero-initialized Float32 vector
local vec = radio.types.Float32.vector(4)
-- Vector properties
print(vec.length) --> 4, length of vector
print(vec.size) --> 16, size of vector in bytes
print(vec.data_type) --> ctype, C type of vector element
print(vec.data_type == radio.types.Float32) --> true
print(ffi.typeof(vec.data)) --> ctype, C type of vector data
-- Vector string representation
print(vec) --> [Float32, Float32, Float32, Float32]
-- Accessing and modifying vector elements
print(vec.data[0]) --> Float32
vec.data[0].value = 5
print(vec.data[0]) --> Float32
print(vec) --> [Float32, Float32, Float32, Float32]
-- Iterating over a vector's elements
-- (Note: Lua for-loop ranges are inclusive)
for i = 0, vec.length-1 do
print(vec.data[i])
end
--> Float
--> Float
--> Float
--> Float
-- Resizing a vector
vec:resize(6)
print(vec.length) --> 6
print(vec.size) --> 24
-- Appending an element to a vector
vec:append(radio.types.Float32(42))
print(vec.length) --> 7
vec:append(radio.types.Float32(26))
print(vec.length) --> 8
print(vec.data[6]) --> Float32
print(vec.data[7]) --> Float32
```
Much of block processing consists of either iterating over a vector and
modifying its elements, or passing the vector to an external library. Since
`vec.data` is a `float *` (in the example above) and `vec.length` is its
length, we have all of the information we need to pass the vector memory to an
external library for processing.
Resizing a vector only causes a re-allocation of the underlying memory when it
is grown to a larger size. The underlying memory is retained on resizing to a
smaller size; just the bookkeeping is updated.
All vectors are allocated with page alignment. This is to enable their
processing with certain library routines that require, or perform better with,
aligned buffers. This is often because SIMD operations are involved.
##### Caveats
Note that a Vector's `data` is a C pointer with zero-based indexing, where as
Lua uses one-based indexing for its arrays. This inconsistency seems terribly
confusing, but in practice doesn't pose much ambiguity: just think C or
low-level when working with sample data, and Lua or high-level when working
with block arguments and curly bracket arrays.
## Blocks
LuaRadio blocks are written in pure Lua, but may interface with external
libraries via the [LuaJIT foreign function
interface](http://luajit.org/ext_ffi_api.html) (FFI) to offload processing,
when needed.
Each block is run in an independent process, under its own Lua state. Blocks
typically have one hot loop, so they can benefit greatly from LuaJIT's
just-in-time trace compiler, and many pure Lua blocks do achieve good
performance without needing acceleration from an external library.
In other cases — particularly when the block can be accelerated by SIMD
extensions — being able to prototype in pure Lua first, before wrapping an
external library, is a nice convenience in block development and can serve as a
fallback implementation for non-real-time applications.
### Methods
Blocks are classes manufactured by the
[`radio.block.factory(name)`](0.reference-manual.md#radioblockfactoryname-parent_classnil)
factory function. Blocks implement all of their functionality with four
methods:
* `instantiate(...)` — constructor
* `initialize()` — initialization (optional)
* `process(...)` — main work method
* `cleanup()` — clean up (optional)
The `instantiate(...)` method is the block constructor, called when the block
is instantiated. This method should establish basic state and register the
block's type signatures.
The `initialize()` method is called by the framework for additional block
initialization before the block is run. In this method, the block's
differentiated type signature and sample rate are available to the block, and
can be used to perform additional initialization.
The `process()` method is the main work method of the block. It receives
immutable input vectors of samples as arguments, and returns output vectors of
samples. This method is called repeatedly by the framework to process inputs
into outputs.
The `cleanup()` method is called by the framework when the flow graph has
collapsed, right before the block exits. This method can be optionally
implemented to perform additional clean up, if necessary.
Finally, source blocks and blocks that modify the sample rate must implement
the `get_rate()` method, which returns the block's sample rate as a number, in
samples per second.
### Template
The template below provides an outline of the block methods and what can be
done in each one:
``` lua
local radio = require('radio')
local MyBlock = radio.block.factory('MyBlock')
function MyBlock:instantiate(...)
-- Perform instantiation here
-- Call self:add_type_signature(, ) for each type signature
end
function MyBlock:initialize()
-- Perform initialization here
-- Can use self:get_input_type(), self:get_output_type(), self:get_rate()
-- to get differentiated type signature and sample rate
end
function MyBlock:process(...)
-- Process input samples and return output samples here
end
function MyBlock:cleanup()
-- Perform cleanup here
end
return MyBlock
```
### Type Signatures
A type signature defines the names and data types of input and output ports.
Type signatures must be added in the `instantiate(...)` method of a block, so
that the block can be connected into a flow graph after it has been
instantiated. When a flow graph is run, every block — starting from sources,
downstream to sinks — is differentiated into the type signature that is
compatible with its input types.
The syntax for adding a type signature is:
```
self:add_type_signature(, )
```
The first argument of
[`add_type_signature()`](0.reference-manual.md#blockadd_type_signatureinputs-outputs-process_funcnil-initialize_funcnil)
is an array of [`radio.block.Input()`](0.reference-manual.md#input)
descriptors, which specify the name and data type of each input port. The
second argument is analogous, with
[`radio.block.Output()`](0.reference-manual.md#output) descriptors, for output
ports. We may call `add_type_signature()` as many times as needed to register
type signatures of different data types, but they must all share the same
input/output port count and names.
Source and sink type signatures can be described with an empty array for inputs
or outputs, respectively.
Below are some examples of type signatures:
``` lua
-- One ComplexFloat32 input named `in` and one Float32 output named `out`
self:add_type_signature({radio.block.Input('in', radio.types.ComplexFloat32)},
{radio.block.Output('out', radio.types.Float32)})
-- Two Float32 inputs named `in1` and `in2`, and one Float32 output named `out`
self:add_type_signature({radio.block.Input('in1', radio.types.Float32),
radio.block.Input('in2', radio.types.Float32)},
{radio.block.Output('out', radio.types.Float32)})
-- Byte source, one Byte output named `out`
self:add_type_signature({}, {radio.block.Output('out', radio.types.Float32)})
-- ComplexFloat32 sink, one ComplexFloat32 input named `in`
self:add_type_signature({radio.block.Input('in', radio.types.ComplexFloat32)}, {})
```
A type signature may also specify custom `process()` and `initialize()`
methods. When the block is differentiated, its `process()` and `initialize()`
methods are bound to these methods.
``` lua
self:add_type_signature(, [, , ])
```
For example:
``` lua
-- Complex-valued type signature
self:add_type_signature({radio.block.Input('in', radio.types.ComplexFloat32)},
{radio.block.Output('out', radio.types.ComplexFloat32)},
self.process_complex, self.initialize_complex)
-- Real-valued type signature
self:add_type_signature({radio.block.Input('in', radio.types.Float32)},
{radio.block.Output('out', radio.types.Float32)},
self.process_real, self.initialize_real)
```
If a block with the type signatures above is differentiated to the
`ComplexFloat32` type, then `initialize()` will be mapped to
`initialize_complex()`, and `process()` will be mapped to `process_complex()`.
Analogous mapping occurs in the `Float32` type differentiation.
This feature removes redundant differentiation from the critical path of
`process()` and makes it easier to separate type-dependent initialization and
processing code in a block.
A block can look up its differentiated types in `initialize()` or `process()`
with the [`get_input_type()`](0.reference-manual.md#blockget_input_typeindex1)
and [`get_output_type()`](0.reference-manual.md#blockget_output_typeindex1)
methods. See the [LuaRadio Reference Manual](0.reference-manual.md#block-1)
for more information.
### Example
In the example below, we create a multiply block capable of multiplying
complex-valued or real-valued inputs.
``` lua
local radio = require('radio')
local MultiplyBlock = radio.block.factory('MultiplyBlock')
function MultiplyBlock:instantiate()
self:add_type_signature({radio.block.Input("in1", radio.types.ComplexFloat32),
radio.block.Input("in2", radio.types.ComplexFloat32)},
{radio.block.Output("out", radio.types.ComplexFloat32)})
self:add_type_signature({radio.block.Input("in1", radio.types.Float32),
radio.block.Input("in2", radio.types.Float32)},
{radio.block.Output("out", radio.types.Float32)})
end
function MultiplyBlock:initialize()
self.out = self:get_output_type().vector()
end
function MultiplyBlock:process(x, y)
local out = self.out:resize(x.length)
for i = 0, x.length - 1 do
out.data[i] = x.data[i] * y.data[i]
end
return out
end
```
The block constructor `instantiate()` adds two type signatures with two inputs
and one output. The first is for complex-valued inputs, and the second is for
real-valued inputs.
The `initialize()` method creates a persistent vector of its differentiated
output type (either `ComplexFloat32` or `Float32`) in `self.out`, which the
`process()` method can use to resize and store output samples in.
The `process()` method resizes its output vector for the input length, performs
the multiplication on the two input vectors `x` and `y`, storing the results in
the output vector, and returns the output vector. Note that this `process()`
implementation is the same for both data types.
Inputs are provided to the block's `process()` method in the same order that
they are defined in the type signature. In this case, the first argument `x`
corresponds to the input `in1`, and the second argument `y` corresponds to the
input `in2`. The framework guarantees that blocks with multiple inputs will
receive equal length vectors for all inputs, so `x.length` and `y.length` are
the same in `process()`. The framework also guarantees that all inputs to a
block have equal sample rates.
Although we could have created a new output vector in `process()`, instead of
reusing `self.out`, it would lead to unnecessary memory allocations and
deallocations as the vector is created on each call to `process()` and later
garbage collected. If this block were in the path of high sample throughput,
then this can also cause more active memory usage than necessary, as the
garbage collector plays catch-up.
Reusing `self.out` allows the output vector to approach a steady state size, as
it is resized to accommodate the inputs. In turn, this allows the block to
reach constant memory usage and spend most of its time on computation and
serialization.
### Testing
There are two ways to go about testing a block. The first is manually, by
calling the block directly in a test script. This requires differentiating and
initializing the block first, so that the correct type signature is selected
and the block is initialized. For example:
``` lua
-- Instantiate
local multiply = MultiplyBlock()
-- Differentiate to Float32 type
multiply:differentiate({radio.types.Float32, radio.types.Float32})
-- Initialize
multiply:initialize()
-- Now, we can use process()
local a = radio.types.Float32.vector_from_array({1, 3, 5})
local b = radio.types.Float32.vector_from_array({2, 4, 6})
local c = multiply:process(a, b)
print(c) --> [Float32, Float32, Float32]
```
A block is differentiated with
[`differentiate()`](0.reference-manual.md#blockdifferentiateinput_data_types)
by providing an array of its input data types. In this case, an array of two
`Float32` data types was provided, so that the block would be differentiated
into the type signature with two `Float32` inputs and one `Float32` output.
The second way of testing a block is with LuaRadio's unit testing suite. This
suite provides a block testing jig, where only the block arguments, input
vectors, and expected output vectors need to be specified. For example:
``` lua
local radio = require('radio')
local jigs = require('tests.jigs')
jigs.TestBlock(MultiplyBlock, {
{
desc = "simple test",
args = {},
inputs = {radio.types.Float32.vector_from_array({1, 3, 5}),
radio.types.Float32.vector_from_array({2, 4, 6})},
outputs = {radio.types.Float32.vector_from_array({2, 12, 30})},
},
-- More unit tests follow
}, {epsilon = 1e-6})
```
This test can then be run with `busted` from the LuaRadio repository:
```
$ busted multiply_test.lua
●
1 success / 0 failures / 0 errors / 0 pending : 0.013749 seconds
$
```
Many of testing jig specifications in the LuaRadio test suite are code
generated with Python 3. See the LuaRadio [`tests/`](../tests) folder for more
details.
### Benchmarking
LuaRadio includes a [`BenchmarkSink`](0.reference-manual.md#benchmarksink) for
benchmarking blocks. This sink reports the average throughput of samples
delivered to the sink in samples per second. This throughput can be used as a
rough performance benchmark of a block.
For example, to benchmark the `MultiplyBlock` we built above:
``` lua
local radio = require('radio')
local source = radio.ZeroSource(radio.types.ComplexFloat32, 0)
local multiply = MultiplyBlock()
local sink = radio.BenchmarkSink()
local top = radio.CompositeBlock()
top:connect(source, 'out', multiply, 'in1')
top:connect(source, 'out', multiply, 'in2')
top:connect(multiply, sink)
top:run()
```
In this flow graph, the multiply block inputs are fed by a
[`ZeroSource`](0.reference-manual.md#zerosource), which sources zero-valued
samples, and the multiply block output is connected to a `BenchmarkSink`, which
counts received samples over time. The `BenchmarkSink` periodically reports the
average rate of samples consumed.
The `BenchmarkSink` is technically benchmarking the entire flow graph,
including the `ZeroSource` and the framework's overhead of serializing samples
between blocks. However, in this case, the `MultiplyBlock` is the computational
limiting factor by several orders of magnitude, so the overhead of `ZeroSource`
and the framework is relatively insignificant, and the resulting rate
corresponds roughly to that of the `MultiplyBlock`. Other situations may
require a different strategy for benchmarking (see caveats below).
Running this flow graph on my platform:
```
$ luaradio multiply_benchmark.lua
[BenchmarkSink] 204.70 MS/s (1.64 GB/s)
[BenchmarkSink] 204.57 MS/s (1.64 GB/s)
[BenchmarkSink] 205.03 MS/s (1.64 GB/s)
[BenchmarkSink] 203.60 MS/s (1.63 GB/s)
[BenchmarkSink] 202.17 MS/s (1.62 GB/s)
[BenchmarkSink] 205.98 MS/s (1.65 GB/s)
...
```
##### Caveats
`BenchmarkSink` results are specific to the platform, affected by CPU load
caused by external processes, and do not take into account the performance
degradation that occurs after all available processor cores are utilized in a
larger flow graph. Nonetheless, the `BenchmarkSink` gives a good idea of
whether or not a block will be a real-time bottleneck to an upstream block of a
particular rate, e.g. an SDR source at 2 MS/s.
It's also important to point out that while the `ZeroSource` is applicable to
many signal processing blocks, which will perform their computation regardless
of if the samples are all zero-valued, it might not be the appropriate stimulus
for blocks that have value-dependent code paths. In those cases, a random
source or a deliberately constructed source may be more appropriate.
### FFI
#### Faster `MultiplyBlock`
The `MultiplyBlock` we created above is pretty fast, but we can interface with
a external library to achieve better performance. In this incarnation of
`MultiplyBlock`, we will wrap the [VOLK library](http://libvolk.org/), a
library of signal processing routines with SIMD acceleration. VOLK provides
kernels for multiplying both complex-valued vectors and real-valued vectors,
with
[`volk_32fc_x2_multiply_32fc()`](http://libvolk.org/doxygen/volk_32fc_x2_multiply_32fc.html)
and
[`volk_32f_x2_multiply_32f()`](http://libvolk.org/doxygen/volk_32f_x2_multiply_32f.html),
respectively.
The [LuaJIT foreign function interface](http://luajit.org/ext_ffi_api.html)
(FFI) allows LuaJIT scripts to call C functions and access C symbols in
dynamically loaded external libraries. This requires defining the function
prototypes and relevant constants first, like a C header file would, so that
LuaJIT has the necessary information to translate types and operate the C ABI.
In the example below, we create `FasterMultiplyBlock`. This block is similar to
the `MultiplyBlock` above, except that it uses the VOLK library with the FFI in
its implementation of `process()`:
``` lua
local radio = require('radio')
local ffi = require('ffi')
local FasterMultiplyBlock = radio.block.factory('FasterMultiplyBlock')
function FasterMultiplyBlock:instantiate()
self:add_type_signature({radio.block.Input("in1", radio.types.ComplexFloat32),
radio.block.Input("in2", radio.types.ComplexFloat32)},
{radio.block.Output("out", radio.types.ComplexFloat32)},
self.process_complex)
self:add_type_signature({radio.block.Input("in1", radio.types.Float32),
radio.block.Input("in2", radio.types.Float32)},
{radio.block.Output("out", radio.types.Float32)},
self.process_real)
end
function FasterMultiplyBlock:initialize()
self.out = self:get_output_type().vector()
end
ffi.cdef[[
void (*volk_32fc_x2_multiply_32fc)(
complex_float32_t* cVector,
const complex_float32_t* aVector,
const complex_float32_t* bVector,
unsigned int num_points
);
void (*volk_32f_x2_multiply_32f)(
float32_t* cVector,
const float32_t* aVector,
const float32_t* bVector,
unsigned int num_points
);
]]
local libvolk = radio.platform.libs.volk
function FasterMultiplyBlock:process_complex(x, y)
local out = self.out:resize(x.length)
libvolk.volk_32fc_x2_multiply_32fc(out.data, x.data, y.data, x.length)
return out
end
function FasterMultiplyBlock:process_real(x, y)
local out = self.out:resize(x.length)
libvolk.volk_32f_x2_multiply_32f(out.data, x.data, y.data, x.length)
return out
end
```
The example defines the functions for the two VOLK kernels in
[`ffi.cdef()`](http://luajit.org/ext_ffi_api.html#ffi_cdef). The first is for
multiplying complex floats, and the second is for multiplying real floats. The
definitions are function pointers, because VOLK kernels are exposed as function
pointers (despite their prototypes in the VOLK documentation).
These definitions are almost identical to their library prototypes —
[`volk_32fc_x2_multiply_32fc()`](http://libvolk.org/doxygen/volk_32fc_x2_multiply_32fc.html)
and
[`volk_32f_x2_multiply_32f()`](http://libvolk.org/doxygen/volk_32f_x2_multiply_32f.html)
— with the exception of substituting the VOLK `lv_32fc_t` type for
`complex_float32_t`, and the C `float` type for `float32_t`.
The substituted types are binary compatible. The substitution is necessary for
the FFI to accept the use of the LuaRadio `ComplexFloat32` and `Float32` data
types in those argument positions and not raise a type mismatch. Other
alternatives are to use a `typedef` alias or
[`ffi.cast()`](http://luajit.org/ext_ffi_api.html#ffi_cast).
Next, the example looks up the VOLK library from the `radio.platform.libs`
table. This pre-loaded table has a few computational libraries
([VOLK](http://libvolk.org/),
[liquid-dsp](https://github.com/jgaeddert/liquid-dsp),
[FFTW](http://www.fftw.org/)) that are used by several blocks in the LuaRadio
codebase for acceleration. To load a different library, the
[`ffi.load()`](http://luajit.org/ext_ffi_api.html#ffi_load) call can be used
directly.
Rather than sharing one `process()` method, as the previous `MultiplyBlock`
implementation did, this version of `MultiplyBlock` binds a custom process
method with `add_type_signature()` to each type signature, since the library
call depends on the differentiated type signature.
Each `process()` method resizes the output vector, calls the library routine
with the input vector data and output vector data, and returns the output
vector.
If we re-run the benchmark from above with this block, we see a roughly 1.5x
performance improvement:
```
$ luaradio multiply_bench.lua
[BenchmarkSink] 297.38 MS/s (2.38 GB/s)
[BenchmarkSink] 304.44 MS/s (2.44 GB/s)
[BenchmarkSink] 301.10 MS/s (2.41 GB/s)
[BenchmarkSink] 306.13 MS/s (2.45 GB/s)
[BenchmarkSink] 305.72 MS/s (2.45 GB/s)
[BenchmarkSink] 299.13 MS/s (2.39 GB/s)
...
```
This performance improvement is not substantial, given that the block is
already in the relatively high rate territory, but in other cases —
particularly filtering and DFT — the performance improvement from wrapping an
optimized external library can be an order of magnitude or more. Those cases
are worth accelerating for real-time applications.
#### BFSK Modulator
In this FFI example, we create a binary FSK (BFSK/2FSK) modulator block by
wrapping the FSK modulator in the digital signal processing library
[liquid-dsp](https://github.com/jgaeddert/liquid-dsp). This example
demonstrates adding new functionality to LuaRadio by wrapping an external
library with the LuaJIT FFI.
The `fskmod` module is a recent addition to liquid-dsp, so it has not yet made
it into the [online documentation](http://liquidsdr.org/doc/). Nevertheless,
the docstrings are available in the implementation
[here](https://github.com/jgaeddert/liquid-dsp/blob/master/src/modem/src/fskmod.c),
and the module is straightforward to use. Below is the liquid-dsp style
documentation reproduced for `fskmod`:
* `fskmod_create(m, k, bandwidth)` creates and returns a `fskmod` object with
bits per symbol `m`, samples per symbol `k`, and total normalized signal
bandwidth `bandwidth`.
* `fskmod_destroy(q)` destroys an `fskmod` object, freeing all
internally-allocated memory.
* `fskmod_modulate(q, s, y)` modulates the input symbol `s`, storing `k`
samples per symbol number of output samples to `y`.
The `fskmod` module is a M-ary FSK modulator, but since we're only implementing
binary FSK for our block, we use 1 for the bits per symbol parameter `m`. The
other parameters (samples per symbol `m` and normalized signal bandwidth `k`)
are functions of the block's input bit rate, output sample rate, and specified
FSK deviation.
In the example below, we create a block that modulates an input bit stream to a
complex-valued baseband BFSK signal. The constructor accepts the deviation in
Hz (the frequency shift between mark and space frequencies), and the output
sample rate of the modulated samples in samples per second:
``` lua
local radio = require('radio')
local ffi = require('ffi')
local BFSKModulator = radio.block.factory('BFSKModulator')
function BFSKModulator:instantiate(deviation, sample_rate)
self.deviation = deviation
self.sample_rate = sample_rate
self:add_type_signature({radio.block.Input("in", radio.types.Bit)},
{radio.block.Output("out", radio.types.ComplexFloat32)})
end
ffi.cdef[[
typedef struct fskmod_s * fskmod;
fskmod fskmod_create(unsigned int _m, unsigned int _k, float _bandwidth);
void fskmod_destroy(fskmod _q);
void fskmod_modulate(fskmod _q, unsigned int _s, complex_float32_t *_y);
]]
local libliquid = radio.platform.libs.liquid
function BFSKModulator:initialize()
self.samples_per_bit = math.floor(self.sample_rate / radio.block.Block.get_rate(self))
self.modulator = ffi.gc(
libliquid.fskmod_create(1, self.samples_per_bit, (self.deviation / self.sample_rate)/2),
libliquid.fskmod_destroy
)
if self.modulator == nil then
error("Creating liquid fskmod object.")
end
self.out = radio.types.ComplexFloat32.vector()
end
function BFSKModulator:get_rate()
return self.sample_rate
end
function BFSKModulator:process(x)
local out = self.out:resize(x.length * self.samples_per_bit)
for i = 0, x.length-1 do
libliquid.fskmod_modulate(self.modulator, x.data[i].value, out.data[i*self.samples_per_bit])
end
return out
end
```
The `ffi.cdef()` call defines the liquid C API for the `fskmod` module. This
consists of an opaque structure pointer aliased to `fskmod`, create and destroy
functions for the `fskmod` object, and the main `fskmod_modulate()` method,
which takes an input symbol (in our case, a bit) and writes the samples of the
modulated signal to a complex-valued array. These definitions are identical to
those in the `liquid.h` header file, except for substituting the `float complex`
type for the compatible LuaRadio data type, `complex_float32_t`, in
`fskmod_modulate()`.
The block's `initialize()` method computes the relevant parameters for
`fskmod_create()`: the number of samples per bit and the normalized frequency
deviation. The samples per bit parameter is computed by dividing the output
sample rate — specified in the constructor — by the input bit rate. The input
bit rate is looked up by using the Block superclass's
[`get_rate()`](0.reference-manual.md#blockget_rate) implementation, which
returns the rate of the upstream block, since this block redefines `get_rate()`
for the samples it produces. It creates the `fskmod` object with these
parameters, and a `ComplexFloat32` persistent output vector in `self.out`.
The `fskmod` object is wrapped with
[`ffi.gc()`](http://luajit.org/ext_ffi_api.html#ffi_gc), which attaches the
`fskmod_destroy()` finalizer to the object. In effect, `fskmod_destroy()` will
be called on the `fskmod` object when it is garbage collected. This allows us
to delegate the resource management of the `fskmod` to LuaJIT's reference
counting and garbage collection, so we don't have to worry about managing it.
The `BFSKModulator` implements a custom `get_rate()` because its output
samples, the complex-valued BFSK signal, do not share the sample rate of its
input samples, the bit stream.
The `process()` method resizes its output vector length to the input bit length
times the number of samples per bit. It calls `fskmod_modulate()` for each bit
value with the proper offset into the output vector, to modulate each bit into
samples in the output vector, and it returns the output vector.
We can test our BFSK modulator in a simple flow graph, by sourcing it with
random bits from
[`UniformRandomSource`](0.reference-manual.md#uniformrandomsource), and sinking
the modulated signal to a
[`GnuplotSpectrumSink`](0.reference-manual.md#gnuplotspectrumsink) spectrum
plotting sink:
``` lua
local top = radio.CompositeBlock():connect(
radio.UniformRandomSource(radio.types.Bit, 9600), -- Random bit source, 9600 sample rate (bit rate)
BFSKModulator(100e3, 1e6), -- BFSK Modulator, 100 kHz deviation, 1 MHz sample rate
radio.ThrottleBlock(), -- Throttle block to pace plotting
radio.GnuplotSpectrumSink() -- Spectrum plotting sink
):run()
```
The resulting plot shows the expected two lobes of the BFSK modulated signal,
100 kHz apart:
The liquid `fskmod` module actually supports modulating M-ary FSK, by choice of
M in `fskmod_create()`. This example can be extended to a generic M-ary FSK
modulator by propagating this parameter to `fskmod_create()` in `initialize()`,
and by batching input bits into a base-M number that is passed to the input
symbol argument of `fskmod_modulate()` in `process()`.
## Custom Types
LuaRadio's four basic types serve well for most signal processing needs, but
are insufficient for higher-level blocks that need to produce or consume more
complex aggregate types. This need arises quickly in digital systems, where a
bit stream may be framed into a data frame, a data frame decoded into a packet,
and so on.
LuaRadio offers two solutions for custom types. The first, called a [`CStruct`
type](0.reference-manual.md#cstructtype), is a type backed by a constant-size C
structure. This is appropriate for fixed size data types and is efficiently
serialized between blocks. All four basic types are themselves, in fact,
`CStruct` types.
The second, called an [`Object` type](0.reference-manual.md#objecttype), is a
type backed by any Lua object. Instances of this type can be variable size and
contain any Lua primitive data types (tables, numbers, strings, booleans,
etc.). `Object` types are not as efficient as `CStruct` types, as they are
marshalled and unmarshalled with [MessagePack](http://msgpack.org/) when
serialized between blocks. However, for relatively low rate uses, they can be
very effective.
### `CStruct` types
`CStruct` types are manufactured with the
[`radio.types.CStructType.factory(ctype, methods)`](0.reference-manual.md#cstructtypefactoryctype-methods)
factory function. The first parameter is the name of the C structure type, and
the second parameter is an optional table of methods and metamethods to be
associated with the type.
The type will automatically inherit `.vector()` and `.vector_from_array()`
static methods, so that vectors of the type can be created by blocks, as well
as internal serialization methods, so that the framework can serialize the type
between blocks. See the [LuaRadio Reference
Manual](0.reference-manual.md#cstructtype) for more inherited methods of
`CStruct` types.
In this example, we construct a fictional type `FooFrame` with a 16-bit address
field, 8-bit type field, and 4-byte data field. We associate a `__tostring()`
string representation method on the type it, as well as `isbeacon()` and
`isdata()` methods to test the fictional frame type.
``` lua
local radio = require('radio')
local ffi = require('ffi')
ffi.cdef[[
typedef struct {
uint16_t address;
uint8_t type;
uint8_t data[4];
} fooframe_t;
]]
local fooframe_methods = {}
function fooframe_methods:__tostring()
return string.format(
"FooFrame",
self.address, self.type, self.data[0], self.data[1], self.data[2], self.data[3]
)
end
function fooframe_methods:isbeacon()
return self.type == 0x1
end
function fooframe_methods:isdata()
return self.type == 0x2
end
local FooFrame = radio.types.CStructType.factory("fooframe_t", fooframe_methods)
```
Now, we can use this type like any other `CStruct` data type:
``` lua
-- A single FooFrame
local a = FooFrame(0xbeef, 0x1, {0xaa, 0xbb, 0xcc, 0xdd})
print(a) --> FooFrame
print(a:isbeacon(), a:isdata()) --> true false
-- A vector of 10 FooFrame
local vec = FooFrame.vector(10)
print(vec.length) --> 10
print(vec.size) --> 80
print(vec.type) --> ctype
-- Assign first FooFrame in vector
vec.data[0].address = 0x1234
vec.data[0].type = 0x02
vec.data[0].data = {0xaa, 0xbb, 0xcc, 0xdd}
print(vec.data[0]) --> FooFrame
```
Blocks can specify the data type in type signatures to produce or consume it:
``` lua
...
function FooFramerBlock:instantiate()
...
self:add_type_signature({radio.block.Input("in", radio.types.Bit)},
{radio.block.Output("out", FooFrame))
end
function FooFramerBlock:process(x)
local out = FooFrame.vector()
for i = 0, x.length-1 do
...
out:append(FooFrame(...))
end
return out
end
...
```
### `Object` types
`Object` types are manufactured with the
[`radio.types.ObjectType.factory()`](0.reference-manual.md#objecttypefactorymethods)
factory function.
The type will automatically inherit `.vector()` and `.vector_from_array()`
static methods, so that vectors of the type can be created by blocks, as well
as internal serialization methods, so that the framework can serialize the type
between blocks. In addition, the type will inherit the `.to_json()` method to
form a JSON representation, enabling the type to be used with the
[`JSONSource`](0.reference-manual.md#jsonsource) and
[`JSONSink`](0.reference-manual.md#jsonsink). See the
[LuaRadio Reference Manual](0.reference-manual.md#objecttype) for more
inherited methods of `Object` types.
After an `Object` data type is created, its `.new()` constructor must be
implemented. Additional methods can be defined directly on the data type class.
In this example, we construct a fictional type `BarPacket` with string address
and data fields. We define its constructor and a `__tostring()` method.
``` lua
local radio = require('radio')
local BarPacket = radio.types.ObjectType.factory()
function BarPacket.new(address, data)
-- Create a new object with its metatable set to BarPacket
local self = setmetatable({}, BarPacket)
self.address = address
self.data = data
return self
end
function BarPacket:__tostring()
return string.format(
'BarPacket',
tostring(self.address), tostring(self.data)
)
end
```
Now, we can use this type like other data types:
``` lua
local a = BarPacket("foo", "abcd")
print(a) --> BarPacket
local b = BarPacket("waldo", "deadbeef")
print(b) --> BarPacket
-- A vector of BarPacket
local vec = BarPacket.vector()
print(vec.length) --> 0
vec:append(a)
vec:append(b)
print(vec.length) --> 2
print(vec.data[0]) --> BarPacket
print(vec.data[1]) --> BarPacket
print(vec)
--> [BarPacket, BarPacket]
```
As the example shows, `BarPacket` can contain variably sized fields.
Vectors of `Object` types provide a similar interface to `CStruct` type
vectors. Note that their data array is also zero-indexed, for consistency with
`CStruct` type vectors.
Just like `CStruct` based types, blocks can use the `BarPacket` type in type
signatures to produce or consume vectors of the type:
``` lua
...
function BarDecoderBlock:instantiate()
...
self:add_type_signature({radio.block.Input("in", BarFrame)},
{radio.block.Output("out", BarPacket))
end
function BarDecoderBlock:process(x)
local out = BarPacket.vector()
for i = 0, x.length-1 do
...
out:append(BarPacket(...))
end
return out
end
...
```
## Manipulating Bits
Digital blocks that manipulate [`Bit`](0.reference-manual.md#bit) typed vectors
often need to convert bits into numbers and perform bitwise operations on them.
The `Bit` data type provides the static method
[`.tonumber()`](0.reference-manual.md#bittonumbervec-offset0-length0-ordermsb)
that operates on `Bit` vectors to extract numbers from bits. This method takes
a `Bit` vector as its first argument, and optional arguments offset, length,
and bit order to specify most kinds of bit extractions.
Below are a few examples of its use:
``` lua
local vec = radio.types.Bit.vector_from_array({0, 1, 0, 1})
print(vec.length) --> 4
print(radio.types.Bit.tonumber(vec)) --> 5
print(radio.types.Bit.tonumber(vec, 0, 4, 'lsb')) --> 10
print(radio.types.Bit.tonumber(vec, 2, 2, 'msb')) --> 1
print(radio.types.Bit.tonumber(vec, 2, 2, 'lsb')) --> 2
```
The Lua 5.1 language, which LuaJIT implements, does not support bitwise
operations natively, but LuaJIT includes a built-in [`bit`
library](http://bitop.luajit.org/api.html#operations) that can be used to
perform bitwise operations on numbers:
``` lua
bit.band(0x1234abcd, 0xff) --> 0x000000cd
bit.bor(0x1234ab00, 0xcd) --> 0x1234abcd
bit.rshift(0x1234abcd, 1) --> 0x091a55e6
bit.lshift(0x1234abcd, 1) --> 0x2469579a
bit.bnot(0xfffffff0) --> 0x0000000f
```
##### Caveats
There are two important caveats with the `bit` library. First, all operations
with `bit` are limited to 32-bits. For many use cases, this is not a major
issue, and easy to work around. Second, all results of bitwise operations are
returned as **signed 32-bit** numbers. This is generally not an issue for
bitwise operations, which are agnostic to signedness interpretation (with the
exception of arithmetic shifts), but it is an issue when comparing bitwise
operation results to literals. For example:
``` lua
print(bit.band(0xdeadbeef, 0x80000000) == 0x80000000) --> false (?)
print(bit.bor(0x0fffffff, 0xf0000000) == 0xffffffff) --> false (?)
print(bit.lshift(0x7fffffff, 1) == 0xfffffffe) --> false (?)
```
The discrepancy here is that the Lua number type is a double, and the bit
operation results are returned as signed 32-bit numbers in a double. 0x80000000
and -2147483648, 0xffffffff and -1, 0xfffffffe and -2 are all two different
double numbers.
To compare the result of a bitwise operation to a literal that has its 31st bit
set (the sign bit), first normalize the literal to a signed 32-bit number with
`bit.tobit()`:
``` lua
print(bit.band(0xdeadbeef, 0x80000000) == bit.tobit(0x80000000)) --> true
print(bit.bor(0x0fffffff, 0xf0000000) == bit.tobit(0xffffffff)) --> true
print(bit.lshift(0x7fffffff, 1) == bit.tobit(0xfffffffe)) --> true
```
These caveats will disappear and bitwise operations will be a lot more natural
if a future version of LuaJIT implements the Lua 5.3 language, which has a
64-bit integer type and native bitwise operations.
## Development Tips
#### FFI
When manipulating samples with the LuaJIT FFI, use `ffi.copy()` instead of C
`memcpy()` and `ffi.fill()` instead of C `memset()` for better performance.
Avoid calling `ffi.typeof()` or `ffi.new()` repeatedly in the Block's
`process()` critical path, and instead call them once in `initialize()` (e.g.
when initializing C types, allocating memory, or allocating sample vectors).
#### REPL
For prototyping with Lua, the LuaJIT REPL is a bit too minimalist to be
productive. As in the original Lua 5.1 REPL, it requires preceding expressions
with "=" to print their value, and it doesn't support dumping tables. I suggest
using Torch's [trepl](https://github.com/torch/trepl) for prototyping,
available in [LuaRocks](https://luarocks.org/modules/vsergeev/trepl-torchless).
It supports table dumping, pretty printing, tab completion, and line history.
Another alternative is [lua-repl](https://github.com/hoelzro/lua-repl).
##### trepl
Install trepl with:
```
sudo luarocks --lua-version=5.1 --server=https://luarocks.org/dev install trepl-torchless
```
Add a Lua path modification to allow local loading of the `radio` package, and
an alias to run trepl under LuaJIT:
```
export LUA_PATH="./?/init.lua;;"
alias th='luajit /usr/lib/luarocks/rocks-5.1/trepl-torchless/scm-1/bin/th'
```
Start the REPL with:
```
th
```
##### lua-repl
Install linenoise and lua-repl with:
```
sudo luarocks --lua-version=5.1 install linenoise
sudo luarocks --lua-version=5.1 install luarepl
```
Add a Lua path modification to allow local loading of the `radio` package, and
an alias to run lua-repl under LuaJIT:
```
export LUA_PATH="./?/init.lua;;"
alias lua-repl='luajit /usr/lib/luarocks/rocks-5.1/luarepl/0.9-1/bin/rep.lua'
```
Start the REPL with:
```
lua-repl
```
#### Prototyping
For some blocks, it may be more productive to prototype signal processing
routines in Python, with libraries like [numpy](http://www.numpy.org/),
[scipy](http://docs.scipy.org/doc/scipy/reference/index.html), and
[matplotlib](http://matplotlib.org/), which allow for more traditional sample
by sample development and plotting than a flow graph paradigm like LuaRadio.
When the routine is ready, it can be ported to a LuaRadio block for performance
and reusability.
Samples from LuaRadio can be dumped to a file with the
[`RawFileSink`](0.reference-manual.md#rawfilesink),
[`IQFileSink`](0.reference-manual.md#iqfilesink), or
[`RealFileSink`](0.reference-manual.md#realfilesink), and imported into Python
for inspection with the
[`numpy.fromfile()`](https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.fromfile.html)
function. The `numpy.float32` type is compatible with LuaRadio's `Float32`
type, and the `numpy.complex64` type is compatible with LuaRadio's
`ComplexFloat32` type. The reverse is also possible with numpy ndarray's
[`.tofile()`](https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.ndarray.tofile.html#numpy.ndarray.tofile)
method, and the [`RawFileSource`](0.reference-manual.md#rawfilesink),
[`IQFileSource`](0.reference-manual.md#iqfilesource), or
[`RealFileSource`](0.reference-manual.md#realfilesource) blocks.