Fundamentals
Export Targets
Code Export
Patcher UI
Special Topics
RNBO Raspberry Pi OSCQuery Runner
Working with the Minimal Export
The Minimal Export setting generates just the essential code for a functional RNBO instance. It is meant to run on small, bare metal targets.
The minimal export setting generates code that can run on small, bare-metal targets. It generates just the absolute essentials, sacrificing a few bells and whistles for a minimal code footprint. It also gives you tight control over memory allocation, letting you use a memory pool to avoid real-time memory allocations.
Minimal Export
You'll find the Minimal Export setting in the C++ Export Dialog.
Be sure not to add an extension to the "Export Name". RNBO will automatically add the ".h" extension for a C++ header.
After code export, you'll see the following files in the export directory.
For this export target, the most important items are the rnbo_source.h header, which contains your generated code, and the common directory, which contains the RNBO library files.
Two other settings are particularly important for bare metal targets:
- Fixed Vector Size
- Fixed Sample Rate
These can be especially useful for bare metal targets, which often operate with a fixed signal vector size or a fixed sample rate.
You will find a simple example of how to use the exported code (with some comments) on:
https://github.com/Cycling74/rnbo.example.baremetal
But let's go through it step-by-step.
Includes
To configure your tool chain to use the exported C++ code, add the common folder to as an include directory. If you're using CMake, simply add this folder in the usual way:
cmake_minimum_required(VERSION 3.17) # we only use C++17 for the AudioFile lib, RNBO would be happy with C++11 set(CMAKE_CXX_STANDARD 17) project(baremetal) add_executable(baremetal main.cpp tlsf.c) include_directories(export/common/)
As an alternative, you can also define RNBO_LIB_PREFIX
to the common directory.
#define RNBO_LIB_PREFIX common
This can be quite handy if you don't have easy access to the compiler settings (e.g. Arduino projects).
Then you can include the generated source code simply via the main header file:
#include "export/rnbo_source.h"
Since this is a header only library, there is no need to add any .cpp files to your project.
Preprocessor Switches
A few defines let you further refine the behavior of your minimal export.
#define RNBO_NOSTL
This switches off the use of C++ standard library classes and uses only RNBO built in Arrays and such. Useful if you want to avoid linking to std::lib.
#define RNBO_USE_FLOAT32
Force the use of 32-bit floats instead of 64-bit double values, very handy for platforms that only have native 32-bit float processing.
#define RNBO_NOTHROW
Disables the any exception throwing on out-of-bounds array access.
#define RNBO_FIXEDLISTSIZE 64
List are normally allocated in run-time on the heap. Setting this to a sufficiently large number will avoid these allocations.
Custom Platform
By default, RNBO uses standard libc functions to allocate memory and to print messages. This behavior is defined in the RNBO_Platform.h header.
You can replace these functions and use your custom ones with the help of a few preprocessor flags.
#define RNBO_USECUSTOMPLATFORM
This will bypass the RNBO_Platform.h header entirely, and you must provide all the necessary methods from the Platform
namespace yourself.
#define RNBO_USECUSTOMPLATFORMPRINT
Replace the print methods with you own:
static void printMessage(const char* message) static void printErrorMessage(const char* message)
#define RNBO_USECUSTOMALLOCATOR
Implement your own memory allocation:
void *malloc(size_t size) void free(void * ptr) void *realloc(void * ptr, size_t size) void *calloc(size_t count, size_t size);
You find an example for this on:
https://github.com/Cycling74/rnbo.example.baremetal
Patcher and Engine
When interacting with exported RNBO code, you'll mostly be calling functions in either the Patcher or the Engine. The Patcher consists mainly of your generated code. By default, this will have the class rnbomatic
. You can call functions like setParameterValue
to set parameter values, and process
to process a vector of audio data.
The Engine is used for scheduling events in the future and processing them at the correct time. You can call functions in the engine like scheduleParameterChange
to schedule an event to be processed at a specific time in the future. This functionality is what makes it possible for RNBO to support time-aware objects like metro.
The minimal export provides a class called MinimalEngine
declared in RNBO_MinimalEngine.h.
You might need to provide your own if you want to get messages out of your RNBO patcher—see MIDI and Messages Out for more information.
Instantiate and Initialize
In order to use your RNBO patcher, you will need to have one instance of your rnbomatic class. For example:
RNBO::rnbomatic<> rnbo;
Before you can use it you will have to initialize it:
rnbo.initialize();
This will initialize all objects and allocate all buffer and data objects. At last you will need to call prepareToProcess in order to use all audio related parts of your patcher:
rnbo.prepareToProcess(SAMPLERATE, VECTORSIZE, true);
It's important to note that this is the last time RNBO needs to make any allocations. However, there may be more allocations if you exceed the fixed list size, resize audio buffers, or change sample rate or vector size by calling prepareToProcess
again.
Process
To process audio you will need to call the process
function to handle one vector of audio data. Typically you call this in a tight loop for real-time audio processing.
void process(const SampleValue * const* inputs, Index numInputs, SampleValue * const* outputs, Index numOutputs, Index n )
Audio input and output should be an array of non-interleaved buffers, one for each channel. To get the total number of audio inputs and outputs, the patcher provides the following functions:
Index getNumInputChannels() const Index getNumOutputChannels() const
You can drive a patcher with less inputs and/or outputs than needed but be aware that if you provide too many outputs, the surplus will not be zero-ed.
Your RNBO export might also have signal parameters, for example if you use the param~
object. The patcher also provides functions for getting the number of input and output signal parameters.
ParameterIndex getNumSignalInParameters() const ParameterIndex getNumSignalOutParameters() const
They need to be added as audio input and/or output to the inputs and output array. This means that when calling process
, the length of the arguments inputs
and outputs
will be the number of audio channels plus the number of signal parameters. The index and IO type of each signal parameter can be accessed via their ParameterInfo
.
Before you call process the first time, you must call prepareToProcess
.
void prepareToProcess(number sampleRate, Index maxBlockSize, bool force)
This will not only set the sample rate and vector size, but also allocate any signal buffers that are needed for running audio through the DSP graph, and call all dspsetup
methods of your signal objects.
Be aware that you will even have to call this method when you used a fixed sample rate and vector size (it will just allocate no signals, then).
Execution Model
The general way that events and audio processing is executed is:
- Execute all events whose time matches into the current aka. next audio buffer
- call process to do the audio processing
- back to step 1.
This has a few implications:
- Events can influence audio in the current audio buiffer, even in a sample accurate way if you use for example sig~
- Events can schdule other events, either for this audio buffer or some time in the future
- Events cannot schedule other events for a time in the past
- Audio processing cannot schedule events that influence the current audio buffer, only for the next buffer
Parameters
Parameters are one of the main points of interaction with your RNBO patcher. They have a name (which might be duplicated) and and ID (which will be unique). All of them have a min and max value.
Here are the main functions to interact with the parameter system:
ParameterIndex getNumParameters() const ConstCharPointer getParameterName(ParameterIndex index) const ConstCharPointer getParameterId(ParameterIndex index) const void getParameterInfo(ParameterIndex index, ParameterInfo * info) const void setParameterValue(ParameterIndex index, ParameterValue v, MillisecondTime time) ParameterValue getParameterValue(ParameterIndex index) void setParameterValueNormalized(ParameterIndex index, ParameterValue normalizedValue, MillisecondTime time = RNBOTimeNow) ParameterValue getParameterNormalized(ParameterIndex index) void processParameterBangEvent(ParameterIndex index, MillisecondTime time)
Note that most of the time parameters are identified by their index. You can use the function getParameterInfo
to get info about the parameter at a particular index, including its min and max value.
const auto numParams = rnbo.getNumParameters(); for (RNBO::ParameterIndex i = 0; i < numParams; i++) { RNBO::ParameterInfo info; rnbo.getParameterInfo(i, &info); std::cout << "Param " << i << ": " << rnbo.getParameterName(i) << " min: " << info.min << " max: " << info.max << std::endl; }
And set them either directly or via a scheduled event (be aware that you only have a limited event queue size !):
rnbo.setParameterValue(0, info.min + (info.max - info.min) * 0.5, RNBO::TimeNow); rnbo.setParameterValueNormalized(0, 0.5, RNBO::TimeNow); rnbo.getEngine()->scheduleParameterChange(0, info.min + (info.max - info.min) * 0.2, 200);
The time you provide can be tricky! When calling a patcher function like setParameterValue
, the time
argument should usually be RNBO::TimeNow
, which is an alias for zero. This means to process the event as fast as possible. Providing any other value for time will not schedule the change in the future, but will instead update the patcher time to be this time value.
To schedule an event for a time in the future, instead use a function from the Engine, like scheduleParameterChange
.
There are some situations where you might want to call patcher functions with a time value other than RNBO::TimeNow
. In particular, this would let you drive the whole patcher with these kinds of calls, without ever needing to call process
. If your RNBO export has no audio processing, you could just consecutively call functions like setParameterValue and process you events one by one without ever calling the process
function (more in the chapter Time and Sync).
Messages
Anything that is sent to/from a message port is called a message. This can either be a number, a list or a bang. A message port is identified by a Tag which is by itself the integer hash of a MessageTagInfo
.
For example, if you have the port called "myport" in your patcher, the MessageTagInfo
is "myport" and the Tag is RNBO::TAG("myport")
(RNBO::TAG
is a constexpr
helper to convert the string to its integer hash)
You can get the overall number of messages, get their human readable tag info, and then send to inports via your rnbomatic instance. You can also receive from outports by overloading the MinimalEngine
class (please refer to https://github.com/Cycling74/rnbo.example.baremetal for an example)
MessageIndex getNumMessages() const void processNumMessage(MessageTag tag, MessageTag objectId, MillisecondTime time, number payload) void processListMessage(MessageTag tag, MessageTag objectId, MillisecondTime time, const list& payload) void processBangMessage(MessageTag tag, MessageTag objectId, MillisecondTime time) MessageTagInfo resolveTag(MessageTag tag) const const MessageInfo& getMessageInfo(MessageIndex index) const
MIDI
To send a MIDI event to your patcher, just call processMidiEvent
with a ConstByteArray
(uint8_t *
) representing your byte data.
Index getNumMidiInputPorts() const Index getNumMidiOutputPorts() const void processMidiEvent(MillisecondTime time, int port, ConstByteArray data, Index length) { uint8_t midiNote[3]; midiNote[0] = 144; midiNote[1] = 60; midiNote[2] = 100; rnbo.processMidiEvent(0, 0, midiNote, 3); }
MIDI and Messages Out
To get MIDI data and/or messages out of the patcher, you need to overload the built in Engine. A code example can be found here: https://github.com/Cycling74/rnbo.example.baremetal
In general it would look something like this:
class MyEngine : public RNBO::MinimalEngine<> { public: MyEngine(RNBO::PatcherInterface* patcher) : RNBO::MinimalEngine<>(patcher) {} void sendMidiEvent(int port, int b1, int b2, int b3, RNBO::MillisecondTime time = 0.0) override { // your MIDI handling code here } void sendListMessage(RNBO::MessageTag tag, RNBO::MessageTag objectId, const RNBO::list& payload, RNBO::MillisecondTime time) override { // your Message handling code here } };
And used like this:
RNBO::rnbomatic<MyEngine> rnbo;
DateRefs
All data in RNBO is represented as untyped char*
data and held in a DataRef
. The accessing objects (e.g., groove~ or poke~) have a data view on the DataRef
, determining their type, which can be either: Untyped
, Float32AudioBuffer
, Float64AudioBuffer
or TypedArray
(not used at the moment).
DataRef* getDataRef(DataRefIndex index) DataRefIndex getNumDataRefs() const void processDataViewUpdate(DataRefIndex index, MillisecondTime time)
This means you can theoretically set 64-bit floating point audio data for a 32-bit reader, which would be quite a mess, so take care to use the correct type when you set the data from the outside.
Here a theoretical example of setting a 32-bit audio buffer:
// an interleaved 2 channel audio buffer of 1 second length const float mySuperSample[SAMPLERATE * 2] = {}; // this will establish the data ref to be a 32 bit audio buffer // BUT be aware that you CANNOT change the bitness // of the audio buffer, so you cannot go from 32 bit to 64 bit or back // but you can change length and/or // channel count compared to the originally generated one const RNBO::Float32AudioBuffer newType(2, SAMPLERATE); // get the data ref, you want to set the audio data on auto ref = rnbo.getDataRef(0); // all data is untyped (char *) data ref->setData((char *) mySuperSample, SAMPLERATE * 2 * sizeof(float)); // update the type, this is important if you changed samplerate or channel count ref->setType(newType); // update all the data views in to patcher (one data ref can have multiple views on it) rnbo.processDataViewUpdate(0, RNBO::TimeNow);
Do not forget to call processDataViewUpdate
after updating your data references, otherwise you might crash because the data views have not been updated to the correct size.
Time and Sync
There are three levels of time in RNBO:
- Engine Time - a time (in milliseconds) that increases (by one vector) steadily whenever you call
process
. It will never reverse and must not jump. It will always indicate the time at the beginning of the audio vector that is currently being processed or, inside an audio processing call, the time at the beginning of the next audio vector. It can be accessed by calling:
rnbo.getEngine()->getCurrentTime()
- Patcher Time - the current time (in milliseconds) in the patcher. Each scheduled event (for example a metro bang) is executed at a specific time. During the execution the patcher time is set to be the time of this event. This time might jump, but will also not go backwards. It cannot be accessed from the outside.
- Beat Time - a time that represents the internal timeline. 1 beat equals one quarter note. you can at any point in time jump to a different beat time by calling
processBeatTimeEvent
. Note: this will not change patcher of engine time, just the transport position.
If the transport is running, the beat time will automatically advance each time you call the process function.
In addition to beat time, RNBO can also process transport events in terms of bars, beats, and units (BBU), along with the current time signature. If you want to sync this to the outside world, you can use one of these functions:
void processTempoEvent(MillisecondTime time, Tempo tempo) void processTransportEvent(MillisecondTime time, TransportState state) void processBeatTimeEvent(MillisecondTime time, BeatTime beattime) void processTimeSignatureEvent(MillisecondTime time, int numerator, int denominator) void processBBUEvent(MillisecondTime time, number bars, number beats, number units)
The usual way to drive a RNBO patcher is calling process
on a tight, regular schedule, and in between call things like setParameterValue
and such.
If you need to schedule a change to a specific time, use a schedule
function from the engine.
void scheduleParameterChange(ParameterIndex index, ParameterValue value, MillisecondTime offset) override
or if you just want to execute events that are needed for the next audio buffer and have a lot of events that should not fill up a queue, you could do something like this:
// set a parameter normalized 0.5 milliseconds into the next audio buffer rnbo.setParameterValueNormalized(0, 0.5, rnbo.getEngine()->getCurrentTime() + rnbo.msToSamps(0.5, SAMPLERATE));
If your patcher has absolutely no audio processing, you could also drive it by never calling process
at all. This will leave Engine Time at 0 (and will never automatically advance beat time).
But since each call to a parameter change and such requires a time stamp, you could advance the internal notion of time by always providing the correct time stamp for each change. Be aware that this is not very well tested territory, so I would highly recommend sticking to the calling process regularly approach.
Feedback
Since this is in active development we do rely on feedback from the users of this code, so please make use of https://cycling74.com/forums?category=RNBO and tell us what works for you and what we might improve.
Have fun !