Combining Pure Data and C++
Making the most of both languages
If you would like to use C++ to do something that is not easily achieved in Pure Data then you’ll want to use a custom
render.cpp file as part of your project.
Table of contents
- The structure of a Pure Data project on Bela
- The code
- Using custom
- Running the patch with the modified
- Custom render for interfacing with sensors
- More information on combining C++ and Pure Data
The structure of a Pure Data project on Bela
In a normal Pure Data project you will have a
_main.pd as your top level Pure Data file and any additional
.pd that are referenced from
_main.pd. In addition to these files it is possible to have a
render.cpp file which will replace the default wrapper file that comes with a Pure Data project.
This allows us to pass signals and messages back and forth from the Pure Data project to the C++ code.
Find the custom-render sketch in the Pure Data section of the Examples tab of the Bela IDE.
It is possible to modify the default libpd wrapper templates in order to combine c++ code with your Pure Data patches.
In this example you should hear a tremolo effect being applied to the output. This is done by taking the output buffer returned from libpd and applying further processing in the c++ code before writing the buffer to the Bela context.
It is also possible to define more input or output channels within the
render.cpp file in order to pass auxiliary signals (e.g. envelopes) across the pd patch and the c++ code
In this example we are sending float values to a receiver named ‘tremoloRate’ which is parsed by the modified
render.cpp file and used to control the rate of the tremolo effect applied to the output.
render.cpp file in the project folder for the libpd implementation. The heavy implementation can be found inside the enclosed /heavy folder. (This is where custom render.cpp files for heavy need to be placed when compiling with heavy).
Search for ‘MODIFICATION’ (no quotation marks) inside either of the render files to inspect all the modifications that were made for this example.
Running the patch with the modified
If using libpd, you can simply run this patch from the Bela IDE. The system will automatically detect the modified file and use it instead of the template. See comments in this example for more information.
Custom render for interfacing with sensors
One common use case for a custom render file is when you would like to interface with sensors require low level control and the accurate timing of digital pins in order to work. I2C based sensors are a good example where the whole interfacing protocol is much easier to achieve in c++ and then the all important readings can just be sent into the Pure Data patch.
In the example
custom-render-distance-sensor we are using a HC-SR04 Ultrasonic Ranging Module and interfacing with the sensor from within
render.cpp. Have a look a the distance sensor tutorial for more information on how the sensor works and how to connect it. In this case the Pure Data patch becomes just a receive object where the readings from the sensor are collected.
More information on combining C++ and Pure Data
When running Puredata patches on Bela, these are combined under the hood with some C++ code which provides a wrapper for the
By adding your own C++ files, you can override the default behaviour and combine C++ and Pd in your project.
When no custom C++ files are provided, the linker uses the
core/default_main.cpp to provide a default
main() function to your program and
core/default_libpd_render.cpp (in the case of
scripts/hvresources/render.cpp (in the case of
heavy) files to provide default versions of the
cleanup() functions to your program.
Inside the wrapper
Both wrappers take care of similar issues, using the
libpd API or the
heavy API as needed.
setup() function, they enable message passing between the Pd patch and the C++ code, preparing for the Bela digital I/O, enabling MIDI devices, enabling the Bela scope.
render() they prepare the audio buffers to be sent to the processing routine, convert digital I/O to audio channels as needed, parse incoming MIDI messages and pass them on to the processing engine.
The processing routine is
After the routine returns, the wrapper copies the output values from the output buffer of the routine to the output buffers of Bela, turns any digital channels processed at audio rate back into the Bela digital I/O format, and sends data to the scope channels as needed.
cleanup() de-allocates the memory and any resources allocated in
Providing custom C++ code
In order to add custom functionalities to the Pd wrapper, or modify the existing ones, you can provide your own C++ wrapper to replace the default one:
libpd: if you provide
.cppfiles in the project folder which contains your
_main.pdfile, these will be compiled. If they contain a definition of the
render(BelaContext*, void*)function, then the
core/default_libpd_render.cppfile will not be used and your files will be used instead.
heavy: if you provide
.cppfiles in the
heavy/folder within the folder that contains your
_main.pdfile, then the
scripts/hvresources/render.cppfile will not be used during the build and your files will be used instead.
An example project which is structured this way is
You are strongly encouraged to refer to the documentation for the
libpd API or the
heavy API and to the default wrappers provided by the Bela code as a starting point for your own customized wrappers.
Combining C++ and Pd
Using a custom wrapper, you can combine Pd and C++ code with bidirectional audio and message communication between the two, you can make more flexible mappings between Pd and the Bela I/Os and scope, optimize CPU usage, add support for I2C sensors or custom network communication within Pd and much more.
examples/PureData/custom-render/ project, messages are passed from Pd to the C++ wrapper to control an oscillator which is then used to do some post-processing on the audio outputs generated from Pd.
render() function could do some pre-processing on the audio. analog and digital inputs before they are passed to the processing routine, or some post-processing on the signals generated by the processing routine, so that implementing a C++->Pd->C++ audio-rate communication is pretty straightforward.
Implementing a Pd->C++->Pd audio-rate communication is a bit more complicated and it would typically involve a 1-block extra latency. One possible approach is a follows:
after the call to the processing routine, you would copy in a buffer the data for those channels that need to be fed back into Pd; at the next call to
render(), you would apply the C++ processing to those data and place them into one of the buffers to be passed to the processing routine, so that they can be accessed through an
Other workable solutions exist, including using multiple independent Pd patches, joined together by the wrapper.
This is already supported by
heavy (though not through the Bela scripts) by instantiating multiple contexts, and will be introduced in the 0.48 release of
The code in the default wrappers was written to give convenient access to the wide feature set of Bela from a Pd patch without having to write any C++ code.
As a natural result of this, the efficiency of the code is not optimal, and some use cases could save a significative amount of CPU by re-factoring the code in the wrapper.
For instance, scope channels are available by default on channels
[dac~ 27 28 29 30].
This means that the patch needs to have at least 26 output channels if you plan to use the scope, adding some overhead in resource usage.
render.cpp file could allow to use any arbitrary channel for logging data to the scope, lowering the channel count of the Pd patch and reducing the CPU usage.