declarative polyamorous cross-system intermedia objects
A zero-cost, compile-time, reflection-based, pure C++ solution to the quadratic glue MxN problem:
Read the documentation to get started !
The original blog post contains the motivation and some implementation details.
If you use this work in an academic setting, please cite the paper:
@inproceedings{celerier2022rage,
title={{Rage Against The Glue: Beyond Run-Time Media Frameworks with Modern C++}},
author={Celerier, Jean-Micha\"el},
booktitle={Proceedings of the International Computer Music Conference (ICMC)},
year={2022},
address={Limerick, Ireland}
}
This library is a proof-of-concept (based on the Boost.PFR library and a couple of tricks),
which showcases how with what little reflection modern C++ provides, it is possible to:
And some more advanced features:
Unlike many other reflection solutions in C++, this library has two properties:
It is entirely non-intrusive. The effects do not even need to include a single header. All the types are introspected from the content of the provided classes ; this library embraces protocols / traits / typeclass-based design mainly thanks to C++ concepts.
It is entirely done at compile-time through pure C++ mechanisms. No external code generator / scanner à la Qt’s moc is used, no forked version of libclang is necessary, etc etc. Instead, it expects types to conform to various concepts corresponding to common multi-media use-cases.
The current catch is that types being introspected must be aggregates. This is not a very hard restriction in practice and allows for plenty of useful things.
The API is not as clean as it could be - the end-goal would be to have the meta-class and compile-time programming papers merged in the C++ standard in order to remove the remaining little boilerplate there is and open the use-cases & lift various dumb restrictions:
Committee if you hear us 😃
See https://github.com/celtera/avendish/tree/main/examples for a list of examples.
Here is an example of various Max/MSP & PureData objects generated from the examples folder ; the patches are available in https://github.com/celtera/avendish/tree/main/docs.
The simplest way to get started is to take a look at the examples, and clone the template repository.
A recent enough clang version is provided for all platforms (check the CI scripts in .github/workflows/cmake
).
A most basic avendish audio processor would look like this:
#pragma once
struct Distortion
{
static consteval auto name() { return "Distortion"; }
static consteval auto c_name() { return "disto"; }
static consteval auto uuid() { return "dd4dd880-d525-44fb-9773-325c87b235c0"; }
struct {
struct {
static consteval auto name() { return "Preamp"; }
static consteval auto control() {
struct {
const float min = 0.001;
const float max = 1000.;
const float init = 1.;
} c; return c;
}
float value{0.5};
} preamp;
struct {
static consteval auto name() { return "Volume"; }
float value{1.0};
} volume;
} inputs;
void operator()(double** in, double** out, int frames)
{
const double preamp = inputs.preamp.value;
const double volume = inputs.volume.value;
for (int c = 0; c < channels; c++)
for (int i = 0; i < frames; i++)
out[c][i] = volume * std::tanh(in[c][i] * preamp);
}
};
}
It will create for instance an audio plug-in with two parameters.
The next example will create a PureData object which:
bamboozle
message which will work with the arguments specified in the C++ function declaration#pragma once
struct Addition
{
static consteval auto name() { return "Addition"; }
static consteval auto c_name() { return "avnd_addition"; }
static consteval auto uuid() { return "36427eb1-b5f4-4735-a383-6164cb9b2572"; }
struct {
struct { float value; } a;
struct { float value; } b;
} inputs;
struct {
struct { float value; } out;
} outputs;
struct {
struct {
static consteval auto name() { return "member"; }
static consteval auto func() { return &Messages::bamboozle; }
} member;
} messages;
void bamboozle(float x, float y, const char* str)
{
inputs.a.value = x;
inputs.b.value = y;
}
static constexpr std::tuple initialize{
[] (Init& self, float a) { std::cout << "A: " << a << std::endl; }
, [] (Init& self, const char* a, const char* b) { std::cout << "B: " << a << b << std::endl; }
};
void operator()()
{
outputs.out.value = inputs.a.value + inputs.b.value;
}
};
A small library of helpers types and macros is provided to simplify the most common use-cases but is in no way mandatory.
As this library currently focuses on the “concept” of an audio effect processor or synthesizer, it provides various amenities tailored for that use case:
void operator()(double** in, double** out, int frames) { /* my audio code */ }
float operator()(float in) { /* my audio code */ }
... etc ...
If a mono processor is written, the library will wrap it automatically in the case of a multichannel requirement from the host.
std::atomic
in order to provide thread-safe access to controls is implemented in one of the backends.std::optional<T>
as a way to indicate message-like input / output.The library is licensed under GPLv3+commercial. The concepts are licensed as permissively as possible (any of Boost license, public domain, BSD0, CC0, you name it) as it’s mainly the concepts which enable interoperability. The end goal being that people who make, e.g. plug-in specs provide their own generators based on these concepts, to make sure that we have a thriving interoperable ecosystem.