Codebox

Codebox and Codebox~ Reference

codebox and codebox~ enable you to use rnboscript, a JavaScript-like scripting language, to implement DSP and event code directly inside of RNBO.

The codebox and codebox~ objects use a JavaScript-like scripting language to implement DSP and event code directly inside of RNBO. The syntax is similar to the codebox in Gen, with some important differences.

Unline Gen, variables must be declared with var, let, or const, and functions must be declared with the function keyword. While this makes the RNBO codebox language look very similar to JavaScript, not all JavaScript features are supported. Overall, codebox is most like a super-powered expr object, rather than a replacement for the Max js object.

Basic Functionality

You can create and reference inlets and outlets using in1, in2, out1, out2, etc.

basic.png

or in codebox~

bascic-dsp.png

Referring to in2 is sufficent to create a second inlet. When using codebox and not codebox~, any assignment to an outlet will cause an output on that outlet.

Screen Shot 2022-06-27 at 2.57.49 PM.png

This will print 4 and then 5 whenever the codebox receives input at its first inlet. The codebox will always have a first inlet. As is often the case in Max, only the leftmost inlet is "hot". Sending a value to the first inlet will trigger output, while sending a value to the other inlets will instead simply update the internal value of in2, in3, etc. When working with codebox~, all outlets are always signals, and will output a signal vector every audio frame.

List are supported as well:

list-basic.png

You will have to use listout and listin identifiers to get accordingly typed inlets and outlets.

Built-in Functions and Objects

Codebox supports a subset of RNBO objects. Very simple objects, the ones that just wrap a stateless function call, are exposed as built-in functions in codebox. So one can write:

let car = cartopol(x, y);

This will have the same output as the cartopol object in RNBO. Some built-in functions return a number:

operators.png

Others return multiple values as an array:

operators-multi-return.png

RNBO objects with some internal state, like a phasor or a cycle, are exposed as codebox-supported objects. These must be created with new and given the @state decorator.

@state myPhasor = new phasor();

These objects all have a next method, which returns the object's output while advancing its internal state by one sample. So calling .next repeatedly on a phasor will advance its internal phase.

// Passing a 1 to the next method is the same as setting the phasor's frequency to 1
// At 44.1 kHz, this advances the internal phase by 0.000022675736961
myPhasor.next(1); // 0.000022675736961
myPhasor.next(1); // 0.000045351473923
myPhasor.next(1); // 0.000068027210884

The cycle and phasor objects are very common:

phasor_cycle_02.png

More complex RNBO objects like granulator~ are not currently supported. Built-in functions and codebox-supported RNBO objects are listed on this page's lefthand sidebar as operators and stateful operators.

Looping and Branching

Most of the control flow keywords from C++ and JavaScript are supported. The looping constructs for and while can be used in the basic way:

for (let i = 0; i < 10; i++) {
  post(i);
}

let j = 1;
while (j) {
  post(j);
  j = 0;
}

However, language-specific paradigms like for..in are not currently supported. You can also use branching constructs like if..else and switch.

let i = 1;
if (i) {
  post("I was right!");
} else {
  post("I was wrong...");
}

const a = 0;
switch (a) {
  case 0:
    post("a was zero");
    break;
  default: 
    post("a was something else");
}

These keywords should follow the C++ and JavaScript specification, so refer to the documentation for those languages for a more thorough discussion of how to use these keywords.

Functions

Function declarations are supported using the function keyword.

function.png

A function can return multiple values by returning an array.

functions.png

You could also write:

function bla(foo)
⁠{
⁠    return [foo + 1, 2];
⁠}
⁠out1 = bla(1)[0] + in1;

The return type of a function is automatically deduced (it will be a list, if you use an array expression as a return type). Anonymous functions, like you might create with big-arrow notation in JavaScript or C++ lambdas, are not currently available.

Special Functions

There are currently three special functions:

  • init is called once, on object creation (either manual or by loading it with a patcher)
  • dspsetup is only available in codebox~ where it will be called each time audio is turned on in Max (which is basically every time the audio state is toggled, or the sample rate or vector size is changed).
  • post prints to the Max console. It can print numbers or constant strings.
Screenshot 2022-05-31 at 19.15.14.png

Inlet Functions

Sometimes you want a function to be called whenever data comes into one of the inlets that is not the first one. This can be achieved through an inlet function. In this example, the function is called whenever a list is received at the second inlet:

Screenshot 2022-05-31 at 19.28.30.png

This will run the function called listin2 whenever the second inlet receives any input. This function will run in addition to setting the value of listin2, so you can think of the function as a side effect of setting listin2.

State and Params

In order to re-use state after codebox has triggered some output, use the @state decorator. This will create state that persists across the lifetime of the codebox object.

state2.png

While you can use the let keyword to declare variables that can be used during codebox execution, these are not reusable state variables. For example, something like this will not work:

Screen Shot 2022-06-27 at 3.50.57 PM.png

Finally, you can use the @param decorator to introduce a state variable that can be set via a message:

Screen Shot 2022-06-28 at 2.05.51 PM.png

The @param decorator function is the only place in the codebox language that you can use a JavaScript-style object. The function takes as its one and only argument an object describing how the parameter should behave. It can take the keys min and max to define a range, or it can take the key enum, followed by a list of enumerated values. Enumerated parameters always start at index 0.

@param({ enum: [ "saw", "sine", "square", "triangle"] }) myParam = 0;

Following the decorator function, the parameter must be given a name and an initial value.

Buffers

A codebox's state can also hold a reference to a buffer. This may be sized, unsized, named, or anonymous. For example, this creates a reference to a buffer "mybuf" defined outside of the codebox.

buffers.png

This will create an internal, anonymous buffer, that will always be twice the sample rate in size:

buffers2.png

note: the only two supported operators in size expressions are samplerate() and vectorsize().

This will create a named buffer (which will also be available in Max) of size 44100:

buffers3.png

You can see too how we use poke and peek to set and get sample values. Reading and writing in buffers does bound checking, so you cannot read or write out of bounds. As you can see, some built-in functions take a buffer as their argument. The list of operators in the left sidebar includes such functions.

Types

It is possible to give a codebox variable an explicit type.

types.png

This is usually unnecessary, but sometimes necessary (for example) when strict typing requires that a number have an integer type.

@state myarray = new FixedIntArray(100);

let a = 0;
out1 = myarray[a]; // throws an error--a must be an integer

let b: int = 0;
out1 = myarray[b]; // no error

Explicit types also allow you to declare a variable without setting it, if you want to. For example:

let x: list;
x = [];
out1 = x.concat([3]);

Without the explicit list type, codebox will assume that x is a number and will throw an error. Valid types include number, list, and int.

Finally, you may find that your codebox fails to compile sometimes in cases where you're using a custom function that returns a list. Sometimes the compiler can infer the return type of a function, and the explicit list type is unnecessary. However, you can always add the explicit return type to clear up the error if you're seeing a problem.

// You may find that functions like this fail to compile
function myListFunc() {
   return [1, 2];
}

// Make the return type explicit to clear the error
function myListFunc(): list {
   return [1, 2];
}

Fixed Arrays

Lists defined with square brackets like [] do not have a fixed size. Adding and removing elements from the list changes the size of the list. Sometimes it's convenient to work with fixed size arrays. These arrays are highly efficient, especially when it comes to setting and getting values from the array at a particular index. One thing that's particularly useful is that with fixed arrays, you can create multidimensional arrays that you can index along multiple dimensions. For example:

// Create a fixed array of integers with size 100
@state a = new FixedIntArray(100);

// Create a fixed array of 32-bit floats with width 10 and height 20 for total size 200
@state b = new FixedFloat32Array(10, 20);

// Create a fixed array of samples (float or double depending on export and host configuration)
//   with width 10, height 5, and depth 12 for total size 600
@state c = new FixedSampleArray(10, 5, 12);

// Fill each array with random values
@state n = new noise();

// If you don't put this in the init function, then it will be called once per audio frame
function init() {
  for (let i = 0; i < 100; i++) {
    a[i] = n.next();
  }

  for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 20; j++) {
      b[i][j] = n.next();
    }
  }

  for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 5; j++) {
      for (let k = 0; k < 12; k++) {
        c[i][j][k] = n.next();
      }
    }
  }
}

Here's another example, creating a codebox~ that fills up an array with a sinusoid, 180 degrees out of phase in each channel.

const arrayLength = 2048;

@state x = new FixedSampleArray(2, arrayLength);
@state index: int = 0;

function init() {
	for (let c = 0; c < 2; c++) {
		for (let i = 0; i < arrayLength; i++) {
			let f: number = arrayLength;
			x[c][i] = sin(PI * (2 * i / arrayLength + c)); 
		}
	}
}

index++;
if (index > arrayLength) index = 0;
out1 = x[0][index];
out2 = x[1][index];

The following types are supported for fixed size arrays:

Name ------------------ C Type --------------- Description
FixedInt8Array -------- int8_t --------------- Signed 8-bit integer
FixedUint8Array ------- uint8_t -------------- Unsigned 8-bit integer
FixedInt16Array ------- int16_t -------------- Signed 16-bit integer
FixedUint16Array ------ uint16_t ------------- Unsigned 16-bit integer
FixedInt32Array ------- int32_t -------------- Signed 32-bit integer
FixedUint32Array ------ uint32_t ------------- Unsigned 32-bit integer
FixedFloat32Array ----- float ---------------- 32-bit float
FixedFloat64Array ----- double --------------- 64-bit float
FixedBigInt64Array ---- int64_t -------------- Signed 64-bit integer
FixedBigUint64Array --- uint64_t ------------- Unsigned 64-bit integer
FixedSampleArray ------ float or double ------ The same type as an audio sample
FixedNumberArray ------ multiple ------------- Value may change depending on the 
                                               circumstances in which code is generated 
                                               and used. Use this if you don't need a 
                                               particular number type.
FixedIntArray --------- int ------------------ Signed integer, equivalent to C int
FixedFloatArray ------- float ---------------- Equivalent to C float
FixedDoubleArray ------ double --------------- Equivalent to C double

Sample Accuracy

A codebox~ object can also accept a number message at any of its inlets.

operators.png

If only messages and no signals are connected to the inlet of a codebox~, then the inlet value (in1, in2, etc.) will only update once every signal vector. This is much more efficient, though it does mean that the value cannot change during the computation of an audio block.

List Functions

List functions are similar to other built-in functions, except that they are defined as methods on a list, rather than global built-in functions.

// This does not work
let a = [];
let b = concat(a, [4, 5]); // no built-in function concat

// This is how list functions work
let c = [];
let d = c.concat([4, 5]); // concat is a method on a list object

They may or may not have a corresponding RNBO object. The handful of list functions that are currently supported are modeled after the specification as their JavaScript equivalent. Where possible, these are identical. For a full discussion of the behavior of each function, see the JavaScript documentation of that function.

  • push - Adds an element to a list, returning the length of the list.
  • pop - Removes the last element from a list and returns that element.
  • shift - Removes the first element from a list and returns that element
  • concat - Merges two lists. Does not modify either list, but returns a new list.
  • fill - Changes all list elements to a static value. Returns the modified list.
  • unshift - Adds one or more elements to the beginning of a list, returning the length of the new list.
  • splice - Changes a list by removing or replacing elements, or adding new element
  • slice - Returns a copy of part of a list.
  • includes - Determines if a list includes a certain value among its entries.
  • indexOf - Returns the first index at which a given element can be found in a list, or -1 if the element cannot be found.
  • reverse - Reverses a list in place, modifying the list.