Learn Minimal Export

Getting Started

Welcome to RNBO

Quickstart

RNBO Basics

Key Differences

Why We Made RNBO

Coding Resources

Fundamentals

Audio IO

Messages to rnbo~

Using Parameters

MIDI in RNBO

Messages and Ports

Polyphony and Voice Control

Audio Files in RNBO

Using Buffers

Using the FFT

Export Targets

Export Targets Overview

VST/AudioUnit
Max External Target
Raspberry Pi Target
The Web Export Target
The C++ Source Code Target

Code Export

Working with JavaScript
Working with C++

Special Topics

Sample Accurate Patching
Scala and Custom Tuning

RNBO and Max for Live

RNBO Raspberry Pi OSCQuery Runner

Metadata

Export Description

Raspberry Pi GPIO

Updating the RNBO Package

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.

Screenshot 2025-02-17 at 10.19.01.png

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.

Screenshot 2025-02-17 at 10.19.53.png

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
Screenshot 2025-02-17 at 13.02.21.png

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:

  1. Execute all events whose time matches into the current aka. next audio buffer
  2. call process to do the audio processing
  3. 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 !