Doubts about fixed point representation and operations


#1

Hi all, santa brought me an axoloti, and since I'd like to use it to "stay fit" on dsp programming, after assembling a few effects for my bass using the (amazing) patcher, I ventured into coding custom objects.

I tought that axoloti would work in floating point, but I happily discovered it doesn't, I say "happily" because I thought this will force me to finally come to terms with real world fixed point dsp issues.

Now I'm trying to get my head around some basic stuff, after gathering all the info I could find in the docs, the forum and looking into the "standard" objects.

I read that the range for the fractional numbers is:

-0x08000000 to 0x07FFFFFF

which represents -64.0 to 64.0 (actually 63.9999...)

This means that the fractional point should be between the bits 20 and 21 (where the first bit's index is "0"), and that we are using 28 of the 32 available bits to represent our fractional values.

Then, I had a look at the code in [math/*], and found how a multiplication between fractional values is done:

outlet_result= ___SMMUL(inlet_a<<3,inlet_b<<2);

I learned that SMMUL multiplies two 32-bit registers and discards the 32 least-significant bits.

In a way - as I understand it - this is like considering the 32-bit values in the operands belonging to the [-1.0, 1.0[ interval, so that any product between two values still belongs to such interval, and the product never overflows. Basically, SMMUL implicitly "sets" the fractional point to be between bits 31 and 30.

Shifting the operands to the left before the SMMUL operation means that the operands may overflow during the shift, but since we are already limiting ourselves to the first 28 bits, we are actually recovering resolution in the least-significant bits. It can also be seen as moving the fractional point to the right.

What I find strange is that the SMMUL preceded by the 2 and 3 bits shifts for the operands, actually corresponds to setting the fractional point between the bits 27 and 26. It's like saying that 0x07FFFFFF is 1.0 (actually 0.999...)

And in fact, if create a patch with two [ctrl/dial b] going to a [math/*], going to a [disp/dial b], I can see that 64.0*64.0 = 64.0 (and, for example, 63.0*63.0=62.02).

So, the [64.0,64.0[ range "behaves" as it is a [-1.0,1.0[ range.

Even though this is just a representation of the values for the sake of our human eyes, I find this a bit counterintuitive, especially for patching purposes, but considering how bad I am with binary operations and fixed point math (not to mention the two's complements!), I guess there's something (big) I'm missing here: is there anyone willing shed some light on my doubts?

An (maybe not so much) unrelated question: how the 32-bit fractional values are truncated to fit into the 24 bit of the DAC output? I'd say that the 4 most significant bits (that should be kept empty anyway) and the 4 least significant bits are discarded, but this is just a wild guess.

Sorry for the long post and my clunky approach to the matter, hope this makes some sense.


Fastest way to convert to/from int32 and float in 0-1 range
#2

Correct, and intentional.

Using the [-64.0..64.0] convention to represent unity range results in many cases in round numbers.
Think of it as an analog modular with +-64V power supply and 1V/semitone calibration.
This choice is inspired by the Nord Modular, I don't remember significant complaints about this convention in that community. It promotes connecting anything to anything, with gentle scaling.

The 4 most significant bits are saturated rather than discarded, but yeah.


#3

Ok, I never used a nord modular or any analog modular synth, so I didn't know. Seems fair anyway, I will just keep this in mind when coding my objects. I'm actually glad to know I didn't get the math wrong!

Keep up the great work and thanks for creating axoloti! :smile: