A pretty big subject, but i think it might be useful to shed some light for newbies:
Contents
- What do you need to know to code objects
- How do you start coding an object?
- How do you edit a library object?
- Coding with input/output (and parameters)
- Local data
- Init code
- K-rate and S-rate cycles
- Inlets/Outlets
- Parameters
- Normal range and number format
- Elementary math in axoloti
- 32bit variables
- Bitshifts
- Fixed point math
- Functions
- Assembly functions
- Bitwise operations
What do you need to know to code objects?
Basic C/C++ knowledge. You don't necessarily need to have a PhD in C++ to code objects, but you'll definitely need to know something about statements, variables, etc..
Math can be really useful, also, but all depends on what you need to code.
How do you start coding an object?
Open a patch (or make a new one), create a patch/object object, click edit and you're good to go.
How do you edit a library object?
Load the object you want to edit in a patch. Click on the arrow and edit object definition.
It's not a good practice to edit factory object this way: you should do embed as patch/object and then edit the embedded object with the edit button.
If you just need to see how an object is coded you can do it the first way, but you should be careful not to overwrite stuff.
If you do, don't panic: you can always delete the object from the folder and do a sync, but it's better to avoid the trouble.
Coding with input/output (and parameters)
In the left side of the object editor you can see the inlets, outlets, attributes, parameters and displays tabs. Inlets and outlets are used to interface with other objects in the patcher. Parameters and displays provide some user input/output patcher-side, attributes are similar to parameters, but can be only edited before starting the patch.
They're generally defined as int32_t or uint32_t variables (however, some attributes use string type, and buffered inlet/outlets are int32_t*).
You can access inlets, outlets, parameters and displays in init code, k-rate code°, s-rate code, and midi code. You can't access them in local data (because they're still to be declared).
Attributes can be accessed in local data.
You can read inlets, attributes or parameters in the code using the inlet_x, attr_x or param_x formulation (where x is the name of the inlet you want to read)
Examples:
if (inlet_dog) dostuff();
int32_t x = inlet_dog + param_cat;
switch (attr_mice) {
{
case 1 : stuff1(); break;
case 2 : stuff2(); break;
}
You get the idea.
You can write in outlets or displays with the outlet_x or disp_x formulation (where x is the name of the outlet or display you want to write to)
Example:
outlet_elephant = 5;
disp_zebra = 10;
° Note: Inlets and outlets that contain "buffer" in the type are only accessible in s-rate code.
If you want to access them in k-rate code you must refer to them as arrays.
Example (k-rate):
outlet_blob[1] = inlet_plaff[6];
Local data
This section of the code is executed once, before inlets, outlets, params and displays are declared.
You can use this section to declare variables, constants, functions etc. You can't access i/o (because i/o does still not exist, at this point), but you can access attributes.
Example: table/alloc 16b
Init code
This code is executed once at the beginning of the patch after all inlets, outlets, parameters and displays are declared. You can therefore access i/o.
If you don't need this feature you can compact all the initialization code in the local data section.
Example: filter/allpass
K-rate and S-rate cycles
K-rate code is executed 3000 times in a second.
S-rate code is once for each sample (i.e. 48000/sec)
This might seem strange, but s-rate is executed after k-rate. (Therefore in s-rate you can read/write in variables created in k-rate).
the s-rate code really is just a short hand for a k-rate equivalent, basically its just a for-loop going around each sample in the audio buffer. you can also do this yourself in the k-rate code, which sometimes is more useful. (you will see the factory objects do it quite frequently)
(note: this means just because an object doesnt have any code in the s-rate section , doesn't mean it doesn't run audio rate code.
Example: osc/phasor
Inlets/Outlets
They can be of four main types: bool (yellow), int (green), frac (blue), frac buffer (red)
They're all 32 bit integers, however.
Buffered i/o can be accessed in k-rate with arrays (see osc/saw cheap) or in s-rate (see osc/sine).
A special note about integers and fractionals: since axoloti is a microcontroller based audio platform, fractional input/output is implemented with fixed point math (Q format, you can check it on wikipedia).
So, what you see in the patcher as 64 (dials) is actually a much bigger number: 2^27.
More on types later.
Parameters
There's a lot to say about parameters and very little time to do so.
frac32.X.map -> dials. They can be unipolar (u.map) and bipolar (s.map). Their range is normally 0~2^27 for unipolar and -2^27~2^27 for bipolar, however some of them are scaled non linearly for particular applications.
int32 -> integer parameters. Their value is what you see in the patcher.
bool32.mom and bool32.tgl -> buttons, boolean 1/0.
Normal range and number format
You'll sometimes read about normal range. An example of a normal range is the output of dial/b, which goes from -64 to +64 (in the ui)
the output of dial/p (the p stands for positive) is 0 to 64, which is a positive normal range.
You can see that if you multiply a dial set to 64 to some other fractional value, you'll get that same fractional value.
If you multiply 32 for another fractional value, you'll get as output half the value. (You can see where i'm going).
What happens under the hood is that 64 in the ui corresponds to a 2^27 integer (or 1<<27 if you like C) passed between objects, and most times it corresponds to a "real world" 1.
Most operations are done on this basis.
Elementary math in axoloti
operation1 = a + b -c;
operation2 = d*e;
operation2 = f/g;
Examples:
math/+
math/-
math/* (the ones with at least one green inlet!)
math/divremc
Remember that you're working with 32bit signed variables, so you have a limited range (overflows won't break boards, but can be pretty harsh sounding).
You can do operations in floating point, by casting variables (it's a bit more expensive than doing integer math, but it can serve purposes)
Example: math/reciprocal (the integer input is cast to a float variable "inf", and then 2^48 is divided by inf. )
Example: math/sqrt (the float variable is used as an argument for the VSQRTF function.)
32bit variables
They're mainly of two types: uint32_t (unsigned int) and int32_t (signed int).
This means that the processor works with 32 bits per variable, so you can use numbers that go from -2147483648 to 2147483647 (signed) or 0 to 4294967296 (unsigned).
Signed integers use two's complement representation: read about it on wikipedia, it's pretty cool: https://en.wikipedia.org/wiki/Two's_complement
Another pretty interesting resource i found about the topic is this:
Knowing the type of a variable is really important when you do bitshifts: in fact 1<<31 equals to 2147483648 in an unsigned int, but it also equals -2147483648, in a signed int (also, if you wonder what's 1<<31 like, here you have it: 10000000 00000000 00000000 00000000)
Bitshifts
The notation a << b means "bit shift left the variable a by a number b of bits"
So, if a = 15 ( 0000 1111 in binary) and b = 2; then a << b = 0011 1100 (binary) = 60 (decimal)
Bitshifting left by a number of bits n it's really like multiplying by 2^n.
This also stands for negative numbers (since they're in two's complement notation and the processor knows that), for example 1111 0000 << 3 = 1000 0000 (which is -16 * 2^3 = -128)
The same thing applies for right shifts: a >> b means "bit shift right the variable a by a number b of bits"
So, if a = 15 ( 0000 1111 in binary) and b = 2; then a >> b = 0000 0011 (binary) = 3 (decimal)
Which it's really like dividing by 2^n.
Same thing applies for negative numbers (in two's complement)
Examples:
math/<<
math/>>
Fixed point math (also not so easy math)
So, we said earlier that axoloti works with integer variables. This is due to the fact that the microprocessor works more efficiently with integer types (don't worry! there's also an fpu!).
If you already opened some library objects, you might have stumbled over functions like ___SMMUL or ___SMMLA.
Tools:
These are assembly functions that perform a "composite" operation all in one take, which is multiply a*b and shift right by 32 bits.
I'll enter in the details of these functions later, but remember some of the math you learned in high school:
the logarithm of a number returns the "magnitude" of that number, in a particular base.
Remember also one of the properties of logarithms: log(a*b) = log(a) + log(b), which you can read as "the magnitude of the product of two numbers is equal to the sum of the magnitude of those numbers"
So, if you multiply two 32-bit numbers (they have a magnitude of 32 maximum), you can get as result a number with a magnitude up to 64.
The same thing (but opposite) stands for division, so if you divide a 64 bit number (magnitude 64) by a 32bit number (magnitude 32) you'll get a number with a smaller magnitude.
Demonstration:
What defines 1 in real world? Well, if you multiply 1*1 you get 1. So, the magnitude remains the same.
If you want 2^27 (1<<27) to be your "digital world 1", you can simply perform operations in a way that retains the magnitude of (1<<27) X (1<<27) (where X is our digital world multiplication)
Sooo... if A and B are two 32bit variables, and the unity is represented by (1<<27), you can say that log2(aA X bB) = 27 when A = 1<<27 and B = 1<<27 (a and b are two coefficients to be defined)
We don't know what operation X is, but we know that ___SMMUL does x*y>>32, which is equal to x*y/(2^32)
log2(aA * bB /2^32 ) = 27
log2(a*2^27) + log2(b*2^27) - log2(2^32) = 27
log2(a) + 27 + log2(b) + 27 - 32 = 27
log2(a) + log2(b) = 5
In practice, to do fixed point math with the mathematical 1 corresponding to digital (1<<27) we have to increase the magnitude of our two numbers by 5.
You can do this in quite a few ways:
C = ___SMMUL(A<<2,B<<3);
C = ___SMMUL(A<<3,B<<2);
C = ___SMMUL(A<<1,B<<4);
C = ___SMMUL(A<<4,B<<1);
C = ___SMMUL(A,B)<<5;
C = ___SMMUL(A<<1,B)<<4;
You get the idea.
Examples:
math/* (the ones without green inlets)
math/*c
Functions
There isn't really a huge pool of functions that are commonly used in axoloti objects.
We already talked about ___SMMUL(a,b): this function belongs to a family of functions really useful to perform fixed point math (and you can really do lots of stuff with them: from simple operations, to filters, oscillators, physical modelling)
Signed multiplications:
out = ___SMMUL(in1,in2) Performs a multiplication between in1 and in2 and bitshifts right by 32bits
out = ___SMMLA(in1,in2,in3) Performs a multiplication between in1 and in2, bitshifts the result right by 32bits and adds the result to in3.
out = ___SMMLS(in1,in2,in3) Performs a multiplication between in1 and in2, bitshifts the result right by 32bits and subtracts the result from in3.
Examples:
math/* (smmul)
mix/mix 3 sq (smmla)
filter/allpass (smmls)
Saturation functions
out = __SSAT(input,N) Saturates a signed input to N bits
out = __USAT(input,N) Saturates an unsigned input to N bits
Important: N must be a constant number, you can't use variables in this argument.
Examples:
math/sat (ssat)
math/satp (usat)
Some particular lookup tables:
SINE2TINTERP(input,output) An integer sine function. It calculates sin(input) and stores the result in output variable. It's not a mathematical sine, meaning that both the input and output are scaled: their range goes from -2^31 to 2^31
HANNING2TINTERP(input,output) An integer hanning window function. Calculates the hanning window of the input and stores the result in output variable.
MTOF(input,output) and MTOFEXTENDED(input,output) Are used to map a pitch to a phase increment (it's used in oscillators and filters to tune them), but you can use these functions also to drive envelopes.
Examples:
math/sin (sine2tinterp)
math/cos (sine2tinterp)
osc/sine (sine2tinterp)
math/window (hanning2tinterp)
conv/mtof (mtof)
filter/vcf3 (mtof)
env/ad (mtof)
osc/sine (mtofextended)
Floating point functions:
_VSQRTF(input) calculates the square root of a float variable.
Example: math/sqrt
Assembly functions
You might wonder where do ___SMMUL, __SSAT and similar come from.
The answers can be found here: http://infocenter.arm.com/help/topic/com.arm.doc.dui0553a/CIHJJEIH.html (which is the arm cortex m4 instruction set), here: https://github.com/axoloti/axoloti/blob/605c1e9a81843a6b465193e956ae3d2e94596c81/firmware/axoloti_math.h (the definition of some of the math functions) and here: https://github.com/axoloti/axoloti/blob/605c1e9a81843a6b465193e956ae3d2e94596c81/CMSIS/Include/core_cm4_simd.h (the definition of other math functions)
Not all assembly functions are available to use, because they are not inserted in math.h and core_cm4_simd.h (which are those two includes i linked above), but don't worry: most of the time you can do everything with C.
However, sometimes you can take advantage of specific functions that do very specific operations (and it's the case of the fixed point math i mentioned above).
## Bitwise operations
int32_t A = 101;
int32_t B = -394;
These two numbers correspond to the binary
A = 00000000 00000000 00000000 01100101
B = 11111111 11111111 11111110 01110110
Bitwise negation: ~a (you can write the tilde pressing alt + 126)
Every bit is negated (it's switched with its opposite)
int32_t C = ~A;
int32_t D = ~B;
Will output these two numbers:
C = 11111111 11111111 11111111 10011010 = -102
D = 00000000 00000000 00000001 10001001 = 393
Bitwise and: a&b
This operation is performed between two variables (they should have the same size!)
An and operation between two bits outputs 1 if and only if both bits are set to 1, otherwise it will output 0:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
This is done for every bit of the two words
int32_t E = A & B;
Will output
E = 00000000 00000000 00000000 01100100 = 100
Bitwise or: a|b
This operation is performed between two variables (they should have the same size!)
An or operation between two bits outputs 0 if and only if both bits are set to 0, otherwise it will output 1;
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
This is done for every bit of the two words
int32_t F = A | B;
Will output
F = 11111111 11111111 11111110 01110111 = -393