Hacking RNBO Operators
Thanks to a hint from a colleague I realised that it's possible to hack new operators into RNBO, however it's completely undocumented and there is no official SDK for this, so we need to do a bit of learning and reverse engineering to make this happen. In this post I'll try to outline what I've learned to help others with hacking in their own objects. This document is far from complete, and is more an overview of what is used in the existing c74 implemented objects than an exhaustive guide to what's possible. Hopefully it's useful as a primer though. It's worth noting that this is completely unsupported, and its possible that everything described below will change in future, so this is only really for you if you're comfortable experimenting.
After I go through these learnings we'll think about how to apply them to creating a basic but useful operator. This operator will function like a normal counter, but taking an audio input rather than a control one. If you are using RNBO for something like building a sequencer (where you might want your whole sequencer engine to be in the audio thread for sample accurate triggers/gate), or controlling a modular synth (where you will be sending out control voltage, and might be receiving it too) an object like this can be extremely useful as a clock divider, or the basis for constructing more complex timing patterns. We'll call this object a [pulse_counter~]
. It's worth pointing out that it's perfectly possible to build the same thing inside gen~, but it's much simpler and more reusable to have it as an operator.
The Basics
RNBO is completely contained in a max package, so we can find everything in the Max 8 > Packages
folder. RNBO is a really big package and there is a lot to look at here, but what we are interested in lives in the source > operators
folder. Inside here we find a lot of .js
files which make up the various RNBO objects inside the [rnbo~]
object. The API for these objects is not available anywhere so we'll need to do a bit of detective work here to figure out their structure (it's possible that you can discern more about how these objects work from all the C code in the RNBO project, but that's a little out of scope for basic hacking in of new objects).
The first thing that we can note is that all of these operators are written in a language which lands somewhere between Javascript, Typescript and C++. That makes IDE completion a complete non-starter at the moment (though not out of the realms of possiblity with a vscode plugin implementation). The first important thing is that all our operators in this folder either extend the Operator
or StatefulOperator
classes, which makes sense as we already know from our RNBO basics that we have two kinds of operators, those with or without state. So the first decision we need to make when hacking in our own objects is which of these classes to extend.
Simple Operators
Although we are mostly looking at code here we should briefly touch on the simpleoperators.json
file in the operators folder. This defines some fundamental RNBO objects which require only simple one line expressions. For example the following implements a log()
operator:
"log": {
"exprtext": "rnbo_log(in1)",
"safeexprtext": "rnbo_log(in1 <= 0.0000000001 ? 0.0000000001 : in1)",
"digest": "The natural logarithm of the input (log base e)",
"alias": "ln",
"comments": [
"@seealso rnbo_exp",
"@seealso rnbo_log10",
"@seealso rnbo_e",
"@tag RNBO",
"@tag RNBO Operators",
"@tag RNBO Math",
"@tag RNBO Powers",
"@category Powers"
],
"meta": {
"in1": {
"displayName": "Input",
"digest": "Input value to be processed."
},
"out1": {
"displayName": "Natural Log Output",
"digest": "The natural log of the input."
}
}
}
We can see that it's providing a prettier wrapper around the underlying rnbo_log(x)
call in the exprtext
, and interestingly it also provides a safe version of the expression which ensures that the input is greater than or equal to 0.0000000001
. It's not entirely clear when this safe version of the expression is used, or if there is a way to ensure the safe value is used, but presumably with more digging this could be determined.
We will see later that other rnbo operators make liberal use of these simple operators, and that operators using other operators of any type is a common pattern. There is no place in the existing operators, other than this file, where the underlying rnbo_x
calls are used, so we should also stick to this rule.
All the other fields we see in the json above are also used in the full operator classes, so we can learn about them below.
Header Metadata
At the top of most of these operator class files is some metadata which is used for the help/reference files. We won't cover making these for our hacked in operators, but it's worth having a quick look none-the-less. Since the counter.js
file is probably a good starting point for the pulse counter we'll eventually build let's start there.
/** Add an amount to the current count every sample.
* @seealso rnbo_accum
* @seealso counter~
* @seealso rnbo_counter
* @tag RNBO
* @tag RNBO Generators
* @category Generators
*/
This is all pretty standard stuff, it's just calling out other objects which are similar, and putting our own object into a specific category and giving it some tags. Descriptive stuff which won't affect object operation. Presumably this is used when generating help/reference data for the operators, but that's beyond the scope of this investigation.
Class @meta
You will see @meta in various places throughout the operator implementations, and it allows us to specify specific details which are important for the class. The first place you may encounter @meta is above the class.
//complex_conj.js
@meta({
digest: "complex conjugate",
internal: true,
publishAsObject: false
})
digest = "";
dsponly = true;
dsponly
value in the class to true
to tell RNBO that this object should only have a ~
version. Otherwise both versions of the object will be available inside RNBO.publishAsObject = false;
internal=true;
alias = [ "+=", "plusequals" ];
aliasonly:true
allowInline : true
__forceInline(op)
function, see below.Class Data
Looking through the inbuilt objects it's possible to find a few important values in the clases that we need to be aware of when hacking our own operators.
Stateless
Stateless operators extend
the Operator
class and perform their calculations in the compute()
function. The compute function can have as many arguments as needed and these should be typed. The return value of the compute function should also be typed, the return value can be a list, ie [number,number]
if the function does not return a value the return type should be void
.
Stateful
Stateful operators extend
the StatefulOperator
class and perform their calculations in the next()
function. The same rules about arguments and return values as stateless operators apply.
Shared
@option({ digest : "Initial value", comments : "The initial value from which to begin counting. Note that this only applies to the first count loop and only when counting is enabled at startup (reset 0), because resetting the counter~ during run time will always start counting from zero." }) init = 0;
[accum]
object provides a couple of examples which are worth looking at so we can understand more complex use cases for options. @ENUM resetmode = {
pre : 0,
post : 1
};
@option({ digest : "Specifies whether the count is reset before (pre) or after (post) the accumulation." }) resetmode : accum.resetmode = "post"
@option ({ digest : "Specifies the minimum value for the accumulated output." }) min : number = 0;
@option({ digest : "Specifies the maximum value for the accumulated output.", optional : true, doNotShowInMaxInspector : true }) max : number;
value : number = options.min;
number
. Giving an option the number type will mean that it's a float rather than the default int type which is used when no type is specified.pre/post
designations rather than 0/1
. We can see that this is implemented by creating the enum inside the operator class and then referencing it in a Typescript like way resetmode : accum.resetmode = "post"
optional : true, doNotShowInMaxInspector : true
. There are many objects with optional attributes, but only accum has an attribute not shown in the max inspector like this. There is also a hidden : true
value which can be set.options.optionVarName
defaultarg
@option({ digest : "Buffer to use." }, defaultarg: 1) buffername : symbol;
@option interp : buffer.interp = "linear";
@option channels : number = 1;
@ENUM
//bufferphasetoindex.js
@ENUM({ scope : "buffer", global : true }) indexmode = {
phase : 0, // 0..1
samples : 1, // sample offset in buffer
signal : 2, // -1..1
lookup : 3, // -1..1
wave : 4 // between start and end
};
@option indexmode : buffer.indexmode = "phase";
next()/compute() @meta
The next and compute functions also have a @meta field
@meta({
x: {
digest: "Input to be accumulated",
comments: "{@variation accum Values}{@variation accum~ Signals} sent through the left inlet will \
be accumulated as long as the calculation is not reset."
},
reset : {
digest: "Non-zero input resets calculation",
comments: "A non-zero number will trigger the output to reset to 0 on the next input value"
},
out1: {
digest: "The accumulated output"
}
})
This is fairly self explanatory, and uses the digest and comments fields to provide some descriptive information of what is expected at the objects inlets and outlets.
Values and functions
The classes also seem to have the following variable values and functions available to be called on this
this.sr
- get the current sample ratethis._voiceIndex
- report current voice index (0 if not in a polyphonic context)this.vs
- audio device vector sizethis.convertToNormalizedParameterValue(index, value)
- in the tonormalized operator this is called directly on the class.this.convertFromNormalizedParameterValue(index, normalizedValue)
- similarly called on the operator class in fromnormalized operator.Exprtext in classes
We saw earlier that the json defined simple operators make use of the exprtext field to define their functionality. This is also available in class metadata and can replace the need to add a next/compute function. We can see this in the implementation of the history operator.
@meta({
publishAsObject : false,
// since history is special in the way, that it wants the last and not the actual value
// we need t define our own expression text
exprtext : "var h = new history(); out1 = h; h = in1;"
})
class history extends StatefulOperator
{
// take care when renaming this state variable - it is used by name in gen.js
@meta({ defaultarg: 1 }) value: number = 0;
getvalue(): number {
return this.value;
}
setvalue(val: number) {
this.value = val;
}
};
Types
Inside your class you are going to have to define the various types of the values that you use. Remember that although this code has a js/ts feel it will end up as compile cpp code, and therefore we are really working with a strict type system.
Types are added to variables using a Typescript like format of var : type
int
- variables instantiated without a type are by default int
, but this can also be made exlicit if needed
number
- The number type is a double
symbol
- string type used for buffernames etc..
boolean
- can be either 0/1 or true/false. most commonly existing implementations see true/false when setting values in the class as 0/1 in function signatures.
//average.js
resetFlag : boolean = false;
wantsReset : boolean = false;
...
next(x : number, windowSize : int = 0, reset : boolean = 0) : number
{
...
if (reset != 0) {
if (this.resetFlag != 1) {
this.wantsReset = 1;
this.resetFlag = 1;
}
}
else {
this.resetFlag = 0;
}
...
}
void
- used to indicate there is no return value from functions
auto
- types can be designated as auto, so we can assume some modern cpp is under the hood! Useful for when you return arrays etc..
Index
- used for indexing channels in buffers, see buffernormalize for exampleSampleValue
- Value written to and read from a sample bufferSampleIndex
- Used for indexing sample positions in buffersMillisecondTime
- mstime has its own type, as seen in the currenttime operatorlist
- list types are lists and have a number of functions which can be called on them//found in append.js
let a: list = [];
a.concat(b); //if your input is a list ( a : list ) then you can concat another list to it with this function call
a.push(c); // push value onto list
a.pop();
a.shift();
a.unshift(x);
a.indexOf(x);
var l = a.length; //can get length like this. note not a function call
"T&"
- Templated type references in quotes are used in places where templated types are used in the cpp implementation. This is generally used for passing in buffers//bufferplay.js
next(buffer: "T&", trigger : number, start: number, length: number, rate: number, channel: number) : [ number, number, number ]
CONST
Types can be declared as const in function signatures as follows
//bufferreadsample.js
compute(
buffer : "T&",
sampleIndex : "const SampleValue", // declaring these parameters as const actually helps the inliner
channel : "const Index",
start : "const SampleIndex",
end : "const SampleIndex"
) : SampleValue
Inline
It's also possible to inline a return value
// cubicinterp.js
compute(a : number, w : number, x : number, y : number, z : number) : "inline number"
External Classes/Functions
You can reference other operators from within your operator code. For example the [average_rms]
object uses the average
and safesqrt
operators in its code. Notice that the stateful average
operator needs to be instantiated, but the stateless safesqrt
can simply be called
// average_rms.js
av = new average();
...
return safesqrt(av.next(x*x, windowSize, reset));
One of the complexities of writing code in a hybrid language such as rnbo operators are written in, is that it totally confuses our IDE's detection of what functions are available. It's fairly obvious to see when an operator calls another operator class, but not when it calls one of the operators defined in the simpleoperators.js. After having gone through the code and trying to filter out all of these operator calls the following additional function calls are available to us and there are probably more which are simply not used in any existing operators!
Inbuilt Functions
clamp(x, min, max); //standard clamping function for min/max bounds
clip(x, min, max); //clip a value
wrap(x, start, end);
trunc(x); // truncate a value
floor(x);
round(x);
fract(x);
abs(x);
fabs(x);
cos(x);
atan2(x);
sin(x);
exp(x);
sign(x);
log(x);
log2f(x);
max(x, y);
min(x, y);
div(x, y);
isnan(x);
fixnan(x);
fixdenorm(x); //Replace denormal values with zero, presumably works like the gen function?
pow(base, exponent);
nextpoweroftwo(x);
sqrt(x);
pi01(); //used only in phaseshift.js
uint32_add(x, y);
uint32_trunc(x);
uint32_rshift(x, shift); // is there an equivalent uint32_,lshift(x, shift) ?
iadd(x, y); // pink.js
imul(x, y);
mix(a, b, mixamnt); // mixamnt is a phasor value in rand.js
updateDataRef(this, this.buffer); // always needs calling after buffer.setSize(x) is called on a class member. see average.js for example
evalexpr(expr); //see delay.js for usage
t = globaltransport(); //get the global transport object
t.getBeatTime(); //current beat time
t.getBeatTimeAtSample(offset);
t.getTempo();
t.getTempoAtSample(offset);
t.getTimeSignature();
t.getTimeSignatureAtSample(offset);
t.getState();
t.getStateAtSample(sampleOffset);
systemticks(); // pink.js
//noise.js - this.state is a FixedUIntArray(4);
xoshiro_reset(seed, this.state) ; // in noise.js the seed is systemticks() + voiceindex() + random(0, 10000)
xoshiro_next(this.state);
//random.js
rand01();
console.log(...args);
__forceInline(functionCall) // eg sampleIndex = __forceInline(bufferphasetoindex(lookup, 0, bufferSize));
Classes
// found in allpass.js
d = new delay(44100); //create a delay line of a specific size
d.read(x); //reads a delay value at a specific delay time
d.write(x); //writes to the delay buffer
d.step(); //steps to the next location in the delay buffer
d.init(); //needs to be called to initialize the delay line, should be called from an operators init()/dspsetup() function
d.clear(); //clears the delay line
//average.js
buffer = new Float64Buffer({ channels : 1 });
buffer.getSampleRate();
buffer.getSize();
buffer.setSize(x); // don't forget to call updateDataRef(this, this.buffer); after resizing a buffer
buffer.requestSize(this.sr + 1, 1); // what does this do?
buffer.getSample(channel, i); //takes Index types for both arguments
buffer.setSample(channel, index, value);
buffer.setZero();
buffer.getChannels();
buffer.setTouched(true);
// pink.js
rows = new IntBuffer({ size: 16, channels: 1 });
//mtof.js
buffer = new AutoAudioBuffer({ buffername : "RNBODefaultMtofLookupTable256" });
//cycle.js
//MultiAutoAudioBuffer appears to have the same set of functions as buffer, with no additional arguments
buffer = new MultiAutoAudioBuffer({
buffers: options.buffername,
updateFunc: "bufferUpdated"
});
//fftstream.js
let fsa = new FixedSampleArray(size);
fsa[i] = x;
//noise.js
state = new FixedUIntArray(4);
[pulse_counter~] object
Well that was a lot to take in, and I'm sure there is plenty missing, but armed with all this information we can now take a look at implementing our [pulse_counter~]
operator.
So lets remind ourselves what we want to do. We would like to build an operator which increases a counter every time we get receive a signal which passes above a specific threshold, once the signal has crossed the threshold the counter value shall not be increased further until the signal crosses below the threshold, or the counter is reset. Since this is fundamentally the existing [counter~]
operator with some additional logic, we just need to alter the code to detect the transitions we are interested in. We will also add another inlet which allows us to set a threshold for when this happens, as some signals do not quite reach 1, or do so at a rate which doesn't work outside of the sample at a time (gen~) domain. We simply add a value to store the previous processed input value which we received, add a new input to the @meta and next() function calls, and make a few small adjustments to the code.
We can do so as follows:
/** Add an amount to the current count every time a pulse is received.
* @seealso rnbo_accum
* @seealso counter~
* @seealso rnbo_counter
* @tag RNBO
* @tag RNBO Generators
* @category Generators
*/
@meta({
dsponly : true,
digest : "count incoming pulses or gates"
})
class pulse_counter extends StatefulOperator
{
@option({ digest : "Initial value", comments : "The initial value from which to begin counting. Note that this only applies to the first count loop and only when counting is enabled at startup (reset 0), because resetting the counter~ during run time will always start counting from zero." }) init = 0;
count = 0;
carry : int = 0;
carry_flag : bool = false; //carry flag persists in this version because we sometimes return early
last : number = 0;
@meta({
a : { digest : "pulse train or gates in", comments : "Every time the input goes from 0.-1. 1 will be added to the count " },
reset : { digest : "non zero value resets the count", defaultarg : 1, comments : "A non-zero value resets the counter to zero and stops counting. A zero value starts the counter." },
limit : { digest : "count limit (zero means no limit)", defaultarg : 2, comments : "The upper limit for counting. Values above the limit are wrapped between 0 and the limit value." },
threshold : { digest : "set the threshold at which a new trigger will be registered", defaultarg: 3, comments : "The threshold at which the gate/trigger will be registered, useful for when your signal does not hit 1." },
out1 : { digest : "current count (running total)", comments : "The current value of the counter as a signal." },
out2 : { digest : "carry flag (counter hit maximum)", comments : "Outputs a signal value of 1. whenever the limit value is reached, otherwise outputs a 0." },
out3 : { digest : "carry count", comments : "The total number of times the counter~ object has reached the limit since the last reset, as a signal value. Sending a non-zero value to the middle inlet resets the carry count." }
})
next(a : number = 0, reset : number = 0, limit : number = 0, threshold : number = 0.99) : [ number, number, number ]
{
if (reset != 0) {
this.count = 0;
this.carry = 0;
this.carry_flag = 0;
}
//Ignore negative signals
else if(a >= 0) {
var floored = (a > threshold) ? 1 : 0;
//only add to the count if this is a new transition
if(floored == 1 && this.last != 1){
this.count += floored;
}
this.last = floored;
if (limit != 0) {
//TODO if count is higher than limit (ie limit changed) wrap to the correct value
if ((a > 0 && this.count >= limit) || (a < 0 && this.count <= limit)) {
this.count = 0;
this.carry += 1;
this.carry_flag = 1;
}
}
}
return [ this.count, this.carry_flag, this.carry];
}
init() {
this.count = options.init;
}
}
If you copy the above code and save it as ~/Documents/rnbo/operators/pulse_counter.js
you will then be able to open the following max patch, which shows our new rnbo object working. (Thanks to Pete Dowling for the tip on using this folder for custom objects)
----------begin_max5_patcher----------
4945.3oc68j1iiiicetpeEL5KoGLU4Q7PW6tYwljMAnAxjdwdjuLSCCZaZa0
QVRQG0wNX6e6gGRxhVxxRtjbUcMFCPMsIond7cw2Eo9kauwXQzSrTCvuA7Sf
at4Wt8lajMIZ3lheeiwN5SKCnoxgYDxdLZwWLtS0UF6oLYyAQzU6XoofnXVX
Yuwzrka8C2LOgsLS8RfNyLuCfLE+0S9WDZlI3yEOQX9N+v.Vl7cA22XTd1gs
5uR9h4.y8VkuP0vxdNlodaFFfOK54eb6sh+b2YsFeflDR2ImQijvEQeswhW1
J3Oj4ye6f37fT17kQ4gYrjxgF3GxjMIFO5TnGKawecIh+hsdwnG3QQOA9o7W
b4rKVFOvRR8iBkOKbFdFViVxWPJrVAZ6Fi09ArZOSwa+FCZbbsluo1iHv0eI
RNQt2U0jenpI6plRXO3W97jpVoIbTVFGekmnnHOYSL1OMQqXIg49RPQ0HmpW
.RR5qfRlFSWtmbV9zF6oBVdXApmX6IYScj+OaTMJAmCZSPzx+WlDKaV1nf42
OLNgkxBynYEPeU2qXqo4AYyWGElk5+2kv.jy92V+qKfwV6rhe7+hlseArIwe
UTnT.rNoPzb4aiygYIVLPs0hbDgz3VdXN+CGsbjNS4qw7zEzDAkZQ.qFyMW5
HJJPuqpmKfsNqn6X+vvCPhYQwGuyD+Ma63YWDw6bWWysrmz44gpdmyYJxlmR
ePGBynAAEZAzm9mng96nYrLeEE.YV0IKjxWnaSWlDEDnMapddnkdVwYxWxdz
eU1V4KpNu.e39wk7PFUT4U9aXoY5skQ2jp2RZ1yJjdslxWTHDOOisKNfuJzG
.W7fqOHcazioECrjOaO9SnhS9Tsol6fsSpKzWWkqV6Gp5Me2BVxWqDoaWMoq
ZmCOq8ZKQ3ZbzRMSGSNQhK72niukpLEKkGnJJjodeZZdQG7ZNTVsTUzgiUWS
c8dpqs1y3fouEUEspMmunB4P+c.i07si2qXuR49b9FVIz4zrrD+E4YJJ0MMn
HchIDLfoblmmKWf7s.R37i2o839Y9zferbD6h3Mv0uWMjJkxUPVdn++WNq.M
TvDLuI5PN1TVhOUSNppqJc7ZLS2V8Zus58Oo7mND4FHVDu2gLnXqqLn0XP0Q
GGvfhdixfZoLt7cJCJ5JCZcFTTGLn3KOCZcmqZ5D4BZ3lSv7BckZWwjt19uM
WkNt6RGxBgz10oM2J6D8WtPl2btF1NXUXjQj.rLZ2Nt6IsRAdbKM6eNEjv31
+9LHjaaJvOEjwaEDkmHbtOgqGHQzHMHMBPef5GHLtE3GxGFCrjywxAiu9OUe
5ay62iQbIDkauX4+C0zI3gPcMOB008xqwkfrd+pwEcUiacMtntz3Rt3ZbKkI
6GGJ1CoD.kghBhs6hG828iQgQx3or52abLVOROX8zYyvGw0HmWNaVi+k1haY
I4+OHhsACDuD7u.36XoGUuO7c+1eN4mEqOfrE9XhWNKjyo9A+P3cbcgHwevh
+PTikCQP9vjC+mL+bYanp1fUsgqZC84eqw4JN7Vdkv0kQWy1QUA8.VuC1SwI
pnRbbAO+v0QGaYGxXqR+XHeMGtj8whAp8FnIajAAr9LTeJDABLkko0sV+7of
i8+TxeJIJ9vYWO9L+YwDw2nL.nnAzvUfXZBcGeK0HPgFG.WmUNGbzmE+TZfO
8vMwjubQ6kAYoNyaAjQWtkdP.2plS4Kuk4bEKlqGfChqZoyRwHCMCQNfvH0f
BGCjlXZFEbAG2mcDLgNZxbTPSR8cs7tpPeJM6ch.QiCBD8qVDHdbPf3e0h.I
iCBj7qTDnXyww.CJmmIXGgwAOoLupKDUgsUchnPiDhB8dGQgGIDE9cMhpzxw
9gnPGEQImmCAcYpW4VN1pUUeKnZaOd74cKh5BOV2z7WJt7ukx.h4CHlPf.yx
RE4zOE7A1rMy9MfU9pj7CV7LvD7nO2T4vHtuHIzzse2LiuowzKh334VYj9eD
V7KGSVB+enQKtsc5Beuo37LsTq1jrT4VdSanqfJZtVTkNow2aiZSrb09ZePm
SpOvF5bgsCevWDTfOWn.OlPA4bgBROgBMoAgd1AvF0hUHGpOdHVv7BPTsrM+
PfDznBI3W.jfOKx1VVPbEDzR37Jz.9o0U6fo2qdHFacBJCs+gZzLJJiqOJAR
OOOGLlf0FAWCZHeM9mT0LxG+j9p4DwMsDXl2L.eCKUIUqpQLxohJZjtg0ZpR
Lm440yDU4nBrJ5zIpBMzDUgMGZhp5a.DqVn8ND3EXq4MgqgQIKw5WnbdoXeA
b7wlMrDP1VtQJaiBVcBhKRkCGrCREmbYdcPvwMQUNWNtYGqSsfgtSM2Lx5sI
2L5ETiGWXt4zG84TNQ7kEoiUP8jgclaUMSkGWUVZEw5uxb6MQf7X.uI+.vyQ
4.5pujmVaj0kJDyLMg25J.mm4dyYtGMSuVcyOgMkBPPn8LKQEoJyzqi6nJ.A
cuPktv2CfmR7oPQgZch7DK5QV7AhlJwmBu.58t5e+7lvyv1D36GSBWVzlMAr
dRfD5v4jFR4e6G6nzWQFWTYtpZe6NAmGi.1s9O+vWRBxiqxqkl0aQIqXICS0
nBcNuIDOLRbAU4xHflxB3T5njuBPmxpMhhAvTU1+NmdeN7f2mynGIqtAkdpy
6YTXz50iQrF+q4IgofOJhXA3SqWChR.+4HAqoXyiOIcA8cc9ANQPefcGt2wf
BLUHYyuIBE7HkIZEC7a37vbR9wdfoPiGlB8dFSsbaj+xCzddlQE++Vpk.DsF
TDT22uYa3rTHNJg.WsY1YEb0gu80DDj7yNvqsq0ZZBXe+fEzkIT0mKP1JE8L
hKakElGMvrsOBcihaeL8K5r264YRbrLIjQK7rUfy7F1sNLi82uttL16GuklF
cxpxFZgq4TtEbBhok2YYqeu8pSbJjKJK79RSKPMya.cCihVhgGQ5Y5RNi9oo
YpPshkgiDZpHclGPzDmo4kQAJQlehidvV1t1HGhkomqI16NdSVPrCxh3YB8r
McbEMUeHHnsnodxLrjFrb+gbPFb3ZnOgHXIjbDdriDMqE4q2WkytZy55M0Vg
RdWyp+bXfnWrQCaXhqu1kKcXMTAACEMgfPGahiokqKBagahMZ4RX3LJU4Nvb
MW+8VykjWZd6fV+Uao3Hubm+I4knABeBYfhZz2QRzcHSvIfh3LvXIN0QmPZ8
2X3rx+FWVTFe7hD9TDf9YeS5s7Icr4DEscY1IFA75OplJviaYgfhKP.wAESv
UyVM68mWNmNREx6WkS37SWkhEG8jkGKIgi.Ap9z8l0S8NXbGoRiJ93tJzBpY
3JNFlyfuDmdZlP2Sx8MIhqmgyRka0cTekZc.MObrMFRO8TBaCMcsgVtilmRk
Py7l6dN7y7qbYcwOippqUEhiJurj2WmQUx0qsh5mQURWFEa8F8Zq.pt8pJNi
3u63PIW4PqygR5fC09U3TTe7htQkY8S3xli5RqnnnWbj0+wHWtXjKdQSX6V6
p33UolIPleqUyDHyyuZxF+ZlnK95eG39625mwszUcfkWlv3dCnJJriV1Wj9H
G3XJ3+sjrM1jwsluHW3ZsaAK6QF2H5czvbZ.P3DAeIqNs2hCaO2gApe34dgn
nB0CTo1vxbBtNTfluYJRNRw96SVIx471pB4bNeMAW9BjSQbfudkGm82ZZ5sO
+adqKZwwUE34SUC3PaIOfKpukF2vi670vNeMryWC670vNeMryWC670vNOvvN
e9lSNIQcdLJG+hPKnNscSR03iuVM9WqF+qUi+0pw+Z03esZ7uVM9WqF+qUi+
0pw+afpwGe9V6ewqFe+XF.Z1qjIBUU0JwcJttTd6EdwQw3n+0vmyDHSgNwEL
vJV.8Y1p2eakMR17adAtBO+iJZ.nUH5apqdwSdqmV9wPaTPY.4r0EmK5Wydq
1Eq65f7zsiAg3ujEEqThHyhsZyevx7jDVXl5BK+a+6q7zLEZX7PVmGNgaxAi
lX7VB0T.QWFbCrEbSoIZBRzaW1oo9t5rcUBi4t9itqRsn7t2KmyS8+k9hurW
qhisu+Y3Hivz4i5CSiNO3HZdX28yyEGLwzBaQFua4QAfLuoA3C7.mJVMWF+U
x.ly.vY8yeE0WJmhK.vyrZHN1mxttyMgnheeExLwHYgrxUefrDa.HvGTRLe2
kHk8uQMjdrtU70vrvdhYQuqcQQk77QIhw+0srBs6hnFywd4hJDMku+aFKgKs
KBMo1PJjymA94r8+2+AGMUzCHkEtJU7C.yOSjPbJ3wsQAkywc.YY8emrzREA
.Ujh86zlMZJveEGJ7W6y26Ywy.087dMn6C97ogOE2AB9NcH4iqEvaBiOX9aL
Z+ybWs1ydLpbcvmjnrshE1ia84KBAzygXILN6cb3ykE1vkszP9oxhCYucGEv
wgHEsKGbgvO2lvU7ApJ6wWsZE4kXI0Xr+vDcmiOHP+LU.eF1JVnk6nlK1V+G
T1omSftgPLlfbsbfilAiYyaZ9y.ucIuTUxh+lScr0r8pcgRhMmhKNY7zdKyv
oMpU6tzdWGzBDy7l.2.u+MDX2QjPxWkYQg8xtd0QT404LMA6jbppLyt+DDqV
nyaNaCC+WfutLhRBJvWO4sPtYsarIh4KodvNhSWPaiA8sD8DTB4pZdyIdXDB
Et4s4csz4cmHcVW4Q0IS32t23PvWRtYG8qbHtk4bjc+NZGP0IMyxbXQyXbt9
qgS0oyhC096x2oNTigQgreulcNbrUWcWbpl+OiR1QyJwnTcSkFuyOjxVw4MQ
HCSgwAtDe49Pmq9NmaZ8d7H56d8D5W6D561A2oyk+JjnixOo92759w.ST604
1in5RF72wbiw3aU9.4ZTFvOjqIRMj17F.9.CduNInE5d0hSd7n0BvvdVAI8R
ze6LDoQ4IKKQjE2uk.cR2JtO59gzLt6q0FG2Sete5etWriCEFb5ILfUiaRfA
6dBCNSHLP5KhP7Y.CfzF3N+UwQbUeErEPSnv4Lrm5XwWbKTn8Kh5yKCQcdk0
+E1Q8scfH+zyH+k1aqZWayWYDQC7PEjAmFHqu.FbB4Sv8EJPSoDCF2WwVxTC
EvAfKNtLim7JmAZqtOIfdJYk5+BYV9MXxp3GS0pxbHZBvcpIvCIf1dHs6Xgd
Uj16+hsuq0hPfntlLz+E1S4oNhnzO5dr05zn+nuxLvl6K+VAxbNkbjkB0phi
KAoBpq9ubkUD.AqFo3WWTFtAvucD.CMI.FpuViUJxLMPg0XIOhPNmk73zrrv
CvLyoxTWDru.wTt0MZPau.6hD63nDgUW6IpB8Q6GpqypJKPcFlEnVDyWk8jP
CYOI3kTOMZ.bwc6hfUQ9aTegGLQU+5s3xAdRy2JWOXRIKV4utjLNs3Z1QnOV
Sn.dKxsGAJPSMTzK+TlTnvd.g3.MU.Qu2ScRQEnAfJlLfnuaAdwMAt2PVKrT
SsZkdBYtWbbVOALq940PoCBZdKnR0Vw9Tp64jC90qrN+dprkLop7649NjiGM
l8AXtntqlONeTbpeI9ZnMgdHOBDcm1+BaI9mG7Tr.lnhKO3CVjC27PGjssMm
xiTevhrrrs77fDSnsCR8AKB53Z63hbHPKhyAyKc4xFSKAah4Vg5RfXOjkbZM
sQXSKLFx+epO5Qt1HjokoqKjatq6AypnpBiYZyJxxEicMswdVVXSmhEJAwgX
WSD1ESTvOzC5Q7bIDrsI4focwl09AAxocdUk1odKsMjQmz0XxgS5ripvc13B
5qoIwhHmKSrqimSGSAMbiJK6Hm84.TaDwIQwQIkhGb5gWqySdVzlD5JeVXld
VWMXOIddNvFt1e+MAWoPpwCoY2SyuWMp8Rw6Ew0Fv89gosKpGEyBUiYkeh7T
o+bipKHHeie3b8jIceQxjtW0aqoRrDVejsnC.8KoQg221PFHXxwpr4smwqYp
Gcl3U0Fjda8+eIfqNzner1w4n.dL93mJaUTdz5OjJghC8oDorSjFwkzkaY6e
P8AIXVRoOTuxqMRCowoEETb8Wl.YTIE+u6P9Kkiqj8pnFRqmscQcZDkTqiJd
w8UGSwzT+VliuAnufCPVmT70QUFda+RoyHMeQUG+sP9lA7sgpN1bFrcKTkcK
rZ7GrD2KCLetptOZKkoF2pyEpC5MlphSW675urpb6av6IwWOon0YNOEx9H36
tP4ZXc9BxmyUsRKw06Q55q65YfuDlDjEsZ7nFInd6UH+5fQC7+AI2tGTgCS2
s.gkvVeziIUGKb0C2XDyQlHhIe6sY6nOIf3FOB2jG4oy13q+veLZo5HV7C+H
8If6OTRuRa9PQopZrtdGkHDw.Jq2S9VOPlEyAx2D20ygxscjtbwZ7JOBZ0JT
c7vs0pk71U7HDxWIR4OWQWKUWPoLdTbl+N++dkMXFeppBAZshvpjnxyK.a1B
5ZDdoy8NVHWtGJr02SE+zBsDtdsMiBoqMZC1p.pN.O4AUOZcSYs8vle3C9o9
GdXRpMffnvM6KBy5JWpOpcQqJq4FyV5lSqRxN8rTJQfOfnba4eKrgUu.Ijqp
F1S+K21tczZNpzchAZ7Zoww0TTHeEF6neQYEl7JsToMo5CzgQBSfdUiWVlLF
zjka8y3rU4IJrwS1piDprnrRBy8KblSRu2e5DRiKpiKY0.c6+31+esfVSQC
-----------end_max5_patcher-----------
That's pretty cool! We wrote an operator, we can see it inside of max, and we can use it in a codebox. But let's not forget that the end goal of rnbo is to export code to another target, so let's very quickly look into the code that rnbo generates for a cpp target.
void pulse_counter_tilde_01_perform(
const Sample * a,
number reset,
number limit,
number threshold,
SampleValue * out1,
SampleValue * out2,
SampleValue * out3,
Index n
) {
auto __pulse_counter_tilde_01_last = this->pulse_counter_tilde_01_last;
auto __pulse_counter_tilde_01_carry_flag = this->pulse_counter_tilde_01_carry_flag;
auto __pulse_counter_tilde_01_carry = this->pulse_counter_tilde_01_carry;
auto __pulse_counter_tilde_01_count = this->pulse_counter_tilde_01_count;
Index i;
for (i = 0; i < n; i++) {
if (reset != 0) {
__pulse_counter_tilde_01_count = 0;
__pulse_counter_tilde_01_carry = 0;
__pulse_counter_tilde_01_carry_flag = 0;
} else if (a[(Index)i] >= 0) {
number floored = (a[(Index)i] > threshold ? 1 : 0);
if (floored == 1 && __pulse_counter_tilde_01_last != 1) {
__pulse_counter_tilde_01_count += floored;
}
__pulse_counter_tilde_01_last = floored;
if (limit != 0) {
if ((a[(Index)i] > 0 && __pulse_counter_tilde_01_count >= limit) || (a[(Index)i] < 0 && __pulse_counter_tilde_01_count <= limit)) {
__pulse_counter_tilde_01_count = 0;
__pulse_counter_tilde_01_carry += 1;
__pulse_counter_tilde_01_carry_flag = 1;
}
}
}
out1[(Index)i] = __pulse_counter_tilde_01_count;
out2[(Index)i] = __pulse_counter_tilde_01_carry_flag;
out3[(Index)i] = __pulse_counter_tilde_01_carry;
}
this->pulse_counter_tilde_01_count = __pulse_counter_tilde_01_count;
this->pulse_counter_tilde_01_carry = __pulse_counter_tilde_01_carry;
this->pulse_counter_tilde_01_carry_flag = __pulse_counter_tilde_01_carry_flag;
this->pulse_counter_tilde_01_last = __pulse_counter_tilde_01_last;
}
It's pretty clear to see how the generated code implements our operator code, but we can note that here it's operating on a block of input data, hence the for (i = 0; i < n; i++) {
which wraps the main operator code.
Wrapping Up
Now we have a useful pulse counter operator which we can use in our rnbo patches and which will successfully compile and export to a target. Hopefully this gives a flavour of what's possible when we add our own operators to extend the functionality of rnbo~.
Another thing which hasn't been touched on yet is that if you are really observant you'll have noticed that operators are not the only type of rnbo object! If you look through the operator descriptions it's notable that objects such as [pack] are consipicuous by their absense! It appears for these objects which need features such as variable numbers of inlets and outlets, there is a different way to create them. More on this in future!
I'm sure there is plenty of info missing from this post, so if you have any additional info I'd love to hear from you, please let me know via Instagram, Mastodon or email!