This document describes how to embed LuaRadio into host applications, and how applications can source or sink samples with LuaRadio flow graphs.
LuaRadio can be run directly from its Lua file sources, but when it is
installed as a shared library, it is packaged into one dual-purpose shared
library called libluaradio
.
This library can both be loaded as the radio
module by LuaJIT for use in
LuaRadio scripts, as well as linked into C/C++ applications to embed the
LuaRadio engine. The packaged library contains the entire LuaRadio framework,
bytecode compiled, as well as a small C API for creating LuaRadio contexts,
loading scripts with a top-level flow graph, and controlling the top-level flow
graph.
A LuaRadio packaged shared library installation provides the following:
/usr/bin/luaradio
— LuaRadio runner helper script/usr/lib/libluaradio.so
— Shared library for dynamic linking/usr/lib/libluaradio.a
— Static library for static linking/usr/include/luaradio.h
— C API header file/usr/lib/lua/5.1/radio.so
-symlink-> /usr/lib/libluaradio.so
— Lua moduleDynamically linking libluaradio.so
and libluajit-5.1.so
into an application
enables it to run LuaRadio scripts. Statically linking the static equivalent of
these libraries allows building entirely standalone executables with LuaRadio.
The LuaRadio packaged library includes a small C API
that wraps the details of Lua states and the radio
package. This simplified
interface provides for creating a LuaRadio context, loading a script with a
top-level flow graph, and the basic control functions of the top-level flow
graph (start, status, wait, stop).
/* Create a new LuaRadio context. */
luaradio_t *luaradio_new(void);
/* Load a script that returns a LuaRadio flow graph. */
int luaradio_load(luaradio_t *radio, const char *script);
/* Start a LuaRadio flow graph. */
int luaradio_start(luaradio_t *radio);
/* Get the running status of a LuaRadio flow graph. */
int luaradio_status(luaradio_t *radio, bool *running);
/* Wait for a LuaRadio flow graph to finish. */
int luaradio_wait(luaradio_t *radio);
/* Stop a LuaRadio flow graph. */
int luaradio_stop(luaradio_t *radio);
/* Free a LuaRadio context. */
void luaradio_free(luaradio_t *radio);
/* Get the Lua state of a LuaRadio context. */
lua_State *luaradio_get_state(luaradio_t *radio);
/* Get a human readable error message for the last error that occurred. */
const char *luaradio_strerror(luaradio_t *radio);
/* Get LuaRadio version info */
const char *luaradio_version(void);
unsigned int luaradio_version_number(void);
const luaradio_version_t *luaradio_version_info(void);
The C API also defines the C types corresponding to the four basic data types
radio.types.ComplexFloat32
,
radio.types.Float32
,
radio.types.Bit
, and
radio.types.Byte
, under complex_float32_t
,
float32_t
, bit_t
, byte_t
, respectively.
In basic usage of the API, applications create a LuaRadio context with
luaradio_new()
, load a script that returns a top-level flow graph with
luaradio_load()
, and start the flow graph with luaradio_start()
.
After this, luaradio_status()
, luaradio_wait()
, and luaradio_stop()
can
be used to get the running status of the flow graph, wait on the flow graph to
finish, or stop the flow graph, respectively. These calls correspond to the
CompositeBlock
methods of the same
name.
When the flow graph is terminated and work with the context is complete, the
context can be freed with luaradio_free()
.
In more advanced usage, applications may access the Lua state of the LuaRadio
context with luaradio_get_state()
. Applications can use this with the Lua C
API to prepare a custom environment for scripts prior to loading them.
Most API functions return 0 on success or a negative integer code on failure.
luaradio_new()
returns NULL
on memory allocation failure.
luaradio_strerror()
can be used to get a human-readable error string of the
last failure that occurred.
The API is re-entrant, as all relevant state is maintained in the luaradio_t
context.
An application may source or sink samples in a LuaRadio flow graph through a
pair of connected file descriptors, e.g. from pipe()
or socketpair()
. A
file descriptor can be handed off to LuaRadio as a parameter to a
RawFileSource
, if the application is
sourcing samples, or a RawFileSink
, if
the application is sinking samples, or both.
The application can then use standard file descriptor I/O calls read()
or
write()
on its file descriptor to serialize samples from or to LuaRadio. The
application must know the C type of the samples, as the samples are read and
written raw, in their native memory representation, with no marshalling.
In the case of an application sinking samples of a variable-sized
Object
based data type (e.g. for a
decoded packet type), the JSONSink
can be
used. This sink accepts a file descriptor parameter, like the RawFileSink
,
but serializes individual samples with JSON, separated by a newline delimiter.
The application can split each serialized sample by the newline delimiter, and
deserialize the sample with JSON.
In the examples below, file descriptor hand-off and parameter passing happens through string substitution of a script template. This solution is simple and demonstrates how to embed a LuaRadio script, but is inherently prone to unintended errors and injection.
The recommended approach for applications adding support for user defined
LuaRadio scripts is to use the Lua C
API with the underlying Lua
state of the LuaRadio context (see luaradio_get_state()
) to define a custom
environment. For example, the application might define an environment
containing variables with the source and sink file descriptors, or even
pre-instantiated RawFileSource
and RawFileSink
blocks, that can be used in
the user defined scripts as sources from or sinks to the application.
Embedding LuaRadio means embedding an interpreter and framework that is not native to the application. This is powerful when the goal is to provide the user with the ability to define arbitrary radio scripts, but it’s not as satisfying when the goal is to leverage LuaRadio as a signal processing engine for a pre-defined flow graph, because the flow graph blocks do not feel like first-class citizens to the application. On the other hand, the alternative — binding every block into a language — adds other layers of complexity like code generation and mapping types.
The FM Radio example is a command-line FM broadcast radio receiver, built with
a small flow graph containing an
RtlSdrSource
, a
WBFMMonoDemodulator
, and a
PulseAudioSink
. It accepts the
station frequency from the command-line and substitutes it into the flow graph
script.
This example demonstrates basic use of the C API.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <luaradio.h>
const char *script_template =
"local frequency = %f\n"
"return radio.CompositeBlock():connect("
" radio.RtlSdrSource(frequency - 250e3, 1102500),"
" radio.TunerBlock(-250e3, 200e3, 5),"
" radio.WBFMMonoDemodulator(),"
" radio.DownsamplerBlock(5),"
" radio.PulseAudioSink(1)"
")";
int main(int argc, char *argv[]) {
luaradio_t *radio;
char script[512];
if (argc < 2) {
fprintf(stderr, "Usage: %s <FM station frequency>\n", argv[0]);
return -1;
}
/* Substitute station frequency in script template */
snprintf(script, sizeof(script), script_template, atof(argv[1]));
/* Create context */
if ((radio = luaradio_new()) == NULL) {
perror("Allocating memory");
return -1;
}
/* Load flow graph */
if (luaradio_load(radio, script) < 0) {
fprintf(stderr, "Error loading flow graph: %s\n", luaradio_strerror(radio));
return -1;
}
/* Start flow graph */
if (luaradio_start(radio) < 0) {
fprintf(stderr, "Error starting flow graph: %s\n", luaradio_strerror(radio));
return -1;
}
/* Wait until completion */
if (luaradio_wait(radio) < 0) {
fprintf(stderr, "Error waiting for flow graph: %s\n", luaradio_strerror(radio));
return -1;
}
/* Free context */
luaradio_free(radio);
return 0;
}
This example may be built in the embed directory with make examples
.
luaradio/embed $ make examples
...
luaradio/embed $ export LD_LIBRARY_PATH=build
luaradio/embed $ ./build/examples/fm-radio
Usage: ./build/examples/fm-radio <FM station frequency>
luaradio/embed $
Listen to 91.1 MHz:
luaradio/embed $ ./build/examples/fm-radio 91.1e6
Note: there is no need to set LD_LIBRARY_PATH
if LuaRadio is installed, as
libluaradio.so.0
will be in a library search path like /usr/lib
.
Radio Data Service (RDS) is a digital protocol used by FM broadcast radio stations to transmit metadata about the station and its programming. This protocol is most commonly known for providing station and song text information with “RadioText” messages, but the standard specifies a variety of other message types (see the RDS Standard (PDF)). One message type in particular — type 4A (see pg. 28) — contains time and date information. RDS-enabled FM radio stations transmit it once every minute.
The RDS timesync example is a command-line tool that sets the system time and
date to an RDS-enabled FM radio station. It is built with a small flow graph
containing an RtlSdrSource
, an
RDSReceiver
, and a
RawFileSink
. The frames decoded by the
RDS receiver are serialized to the C application through the RawFileSink
. The
C application scans for the time and date message type, and once it finds one,
decodes it and sets the system time and date to it.
This example demonstrates how to serialize samples from a LuaRadio flow graph to an application.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#include <luaradio.h>
const char *script_template =
"local frequency = %f\n"
"return radio.CompositeBlock():connect("
" radio.RtlSdrSource(frequency - 250e3, 1102500),"
" radio.TunerBlock(-250e3, 200e3, 5),"
" radio.RDSReceiver(),"
" radio.RawFileSink(%d)"
")";
/* radio.RDSFramerBlock.RDSFrameType */
typedef struct {
uint16_t blocks[4];
} rds_frame_t;
static time_t rds_decode_time(const rds_frame_t *time_frame) {
/* See RDS Standard 3.1.5.6, pg. 28 */
/* Extract modified julian date, hour, minute */
uint32_t mjd = ((time_frame->blocks[1] & 0x3) << 15) | ((time_frame->blocks[2] & 0xfffe) >> 1);
uint32_t hour = ((time_frame->blocks[2] & 0x1) << 4) | ((time_frame->blocks[3] & 0xf000) >> 12);
uint32_t minute = ((time_frame->blocks[3] >> 6) & 0x3f);
/* See RDS Standard Annex G, pg. 81 */
/* Convert modified julian date to year, month, day */
uint32_t yp = (uint32_t)(((float)mjd - 15078.2) / 365.25);
uint32_t mp = (uint32_t)(((float)mjd - 14956.1 - ((uint32_t)((float)yp * 365.25))) / 30.6001);
uint32_t k = (mp == 14 || mp == 15) ? 1 : 0;
uint32_t day = mjd - 14956 - ((uint32_t)((float)yp * 365.25))-((uint32_t)((float)mp * 30.6001));
uint32_t month = mp - 1 - k * 12;
uint32_t year = yp + k;
/* Convert hour, minute, year, month, day to time_t */
struct tm tm = {
.tm_sec = 0, .tm_min = minute, .tm_hour = hour,
.tm_mday = day, .tm_mon = month - 1, .tm_year = year,
.tm_isdst = -1,
};
return timegm(&tm);
}
int main(int argc, char *argv[]) {
luaradio_t *radio;
char script[512];
int sink_fds[2];
rds_frame_t frame;
if (argc < 2) {
fprintf(stderr, "Usage: %s <FM station frequency>\n", argv[0]);
return -1;
}
/* Create a pair of connected file descriptors with pipe() */
if (pipe(sink_fds) < 0) {
perror("pipe()");
return -1;
}
/* Substitute station frequency and write fd of pipe in script template */
snprintf(script, sizeof(script), script_template, atof(argv[1]), sink_fds[1]);
/* Create context */
if ((radio = luaradio_new()) == NULL) {
perror("Allocating memory");
return -1;
}
/* Load flow graph */
if (luaradio_load(radio, script) < 0) {
fprintf(stderr, "Error loading flow graph: %s\n", luaradio_strerror(radio));
return -1;
}
/* Start flow graph */
if (luaradio_start(radio) < 0) {
fprintf(stderr, "Error starting flow graph: %s\n", luaradio_strerror(radio));
return -1;
}
for (unsigned int frame_count = 1; ; frame_count++) {
/* Read RDS frame from read fd of pipe */
if (read(sink_fds[0], &frame, sizeof(frame)) != sizeof(frame)) {
perror("read()");
return -1;
}
printf("\rRDS frames received: % 5d", frame_count);
/* Check if it's a time frame (group = 0x4, version = 0x0) */
uint32_t group = (frame.blocks[1] >> 12) & 0xf;
uint32_t version = (frame.blocks[1] >> 11) & 0x1;
if (group == 0x4 && version == 0) {
printf("\nRDS time frame found!\n");
break;
}
}
/* Stop flow graph */
if (luaradio_stop(radio) < 0) {
fprintf(stderr, "Error stopping flow graph: %s\n", luaradio_strerror(radio));
return -1;
}
/* Decode the time */
time_t t = rds_decode_time(&frame);
printf("Setting system time to %s", ctime(&t));
/* Set system time */
struct timeval tv = { .tv_sec = t, .tv_usec = 0 };
if (settimeofday(&tv, NULL) < 0) {
perror("settimeofday()");
return -1;
}
/* Free context */
luaradio_free(radio);
return 0;
}
This example may be built in the embed directory with make examples
.
luaradio/embed $ make examples
...
luaradio/embed $ export LD_LIBRARY_PATH=build
luaradio/embed $ ./build/examples/rds-timesync
Usage: ./build/examples/fm-radio <FM station frequency>
luaradio/embed $
Sync time and date with 88.5 MHz:
luaradio/embed $ ./build/examples/rds-timesync 88.5e6
RDS frames received: 443
RDS time frame found!
Setting system time to Tue May 10 23:11:00 2016
luaradio/embed $
Note: there is no need to set LD_LIBRARY_PATH
if LuaRadio is installed, as
libluaradio.so.0
will be in a library search path like /usr/lib
.
Compile the examples and link them with the libluaradio
and libluajit-5.1
shared libraries:
$ clang fm-radio.c -o fm-radio -lluaradio -lluajit-5.1 -Wl,-E
$ clang rds-timesync.c -o rds-timesync -lluaradio -lluajit-5.1 -Wl,-E
You may need to add header and library paths with -I
and -L
, respectively,
if LuaRadio is not installed to standard paths.
We can check with ldd
that these executables only depend on standard
libraries, libluaradio
, and libluajit-5.1
:
$ ldd fm-radio
linux-vdso.so.1 (0x00007ffc76d3a000)
libluaradio.so.0 => /usr/lib/libluaradio.so.0 (0x00007f566ac4f000)
libluajit-5.1.so.2 => /usr/lib/libluajit-5.1.so.2 (0x00007f566a9df000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f566a63e000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007f566a33a000)
libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f566a136000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f5669f20000)
/lib64/ld-linux-x86-64.so.2 (0x00007f566ae83000)
$ ldd rds-timesync
linux-vdso.so.1 (0x00007fff82f42000)
libluaradio.so.0 => /usr/lib/libluaradio.so.0 (0x00007f648bc19000)
libluajit-5.1.so.2 => /usr/lib/libluajit-5.1.so.2 (0x00007f648b9a9000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f648b608000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007f648b304000)
libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f648b100000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f648aeea000)
/lib64/ld-linux-x86-64.so.2 (0x00007f648be4d000)
$
Compile the examples and link them with the libluaradio
and libluajit
static libraries:
$ clang fm-radio.c -o fm-radio -Wl,--whole-archive,-E libluaradio.a -Wl,--no-whole-archive libluajit.a -ldl -lm
$ clang rds-timesync.c -o rds-timesync -Wl,--whole-archive,-E libluaradio.a -Wl,--no-whole-archive libluajit.a -ldl -lm
The libluajit.a
static library can be obtained from LuaJIT/src/libluajit.a
after building LuaJIT locally.
After stripping symbols, these standalone executables contain the entire LuaJIT + LuaRadio runtime in 640 KB:
$ du -h fm-radio rds-timesync
716K fm-radio
716K rds-timesync
$ strip fm-radio rds-timesync
$ du -h fm-radio rds-timesync
640K fm-radio
640K rds-timesync
$
We can check with ldd
that these executables only depend on standard libraries:
$ ldd fm-radio
linux-vdso.so.1 (0x00007fff08556000)
libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f9095e02000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007f9095afe000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f90958e8000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f9095547000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9096006000)
$ ldd rds-timesync
linux-vdso.so.1 (0x00007ffe7fdfe000)
libdl.so.2 => /usr/lib/libdl.so.2 (0x00007fc708814000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007fc708510000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007fc7082fa000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fc707f59000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc708a18000)
$