sensorium / Mozzi

sound synthesis library for Arduino

Home Page:https://sensorium.github.io/Mozzi/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Improving fixMath

tomcombriat opened this issue · comments

So, the fix maths also came as one point Mozzi could be improved on. Of course, as the majority of the boards Mozzi is supporting do not have an FPU, using fix maths is the way to go to keep efficiency. But fix maths can be a bit disturbing for new users, all these new types, that you need to take care of when doing operations and which display incorrectly on Serial.print

Here are a few suggestions we might think of. This is also a place for discussion.

  • documentation: there are some documentations about the numerous fix point types supported by Mozzi, but I think a more fundamental documentation about what are fix point types, how to handle to handle them and so on, might be helpful. Some questions remaining are: where to put that? On the website for instance in another topic like the tricks to make the code faster for instance?
  • adding a "smart" multiplication would also be helpful I think. For that I was thinking of implementing a (quite huge) set of functions on the basis of: fixMult<T1>(T2,T3) where T1 being the output type, already shifted the correct way. Here there are a few questions:
    • these might be implemented by hand (or with a script of some sort, I am not afraid for that) but might not be handled by the compiler as a regular template, so why use one and not T1 fixMult(T2, T3)? My personal opinion for the template is to force the user to specify the output type, and not rely on the compiler to decide like it would be the case with a non-templated function without an explicit cast from the user but I am happy to have external opinions.
    • even though it can be written automatically, some cases are not completely straightforward: should we implement for instance the multiplication of an unsigned fixmath with a signed one?
    • also, I wonder if someone would have a solution to make the compiler do this job via templates or macros?
    • finally, is it a good idea?

Cheers,

I believe it should be possible to even go one step further and replace (or supplement) the QXnY typedefs with proper classes. For the purpose of this discussion, let's call the replacement type NXnY (which I don't think will be the final name).

Internally these would simply carry an integer (8, 16, or 32 bits), but importantly they would have operator overloads so, you can simply write:

N16n16 number a = ...;
N8n8 number_b = ...;
number_a += number_b;  // compiler does the right thing, automatically.

. The compiler keeps track of what type each is, and do the appropriate thing, without ever have to spell out the types a second time.

This would also effectively do away with the complexity of specialized members in other classes such as Oscil::setFreq_Q16n16() etc. That would instead become an overload Oscil::setFreq(N16n16 freq).

Now, you are quite right in pointing out that not just two, but three types may be involved in a single calculation, e.g. a multiplication exceeding the original value range. We could again organize this in member functions such as: T2::mult<T1>(T3 factor_b). Alternatively, of course, we could stick to a simple rule of "first operand is always the return type". In that case, for a multiplication use a larger value range, users would write e.g.

N8n8 number_a, number_b;
number_c = ((N16n16) number_a) * number_b; // excessive quotes added for clarity. Same as (N16n16) number_a * number_b

. Not quite incidentally, the latter looks much like what you'd do with regular integers.

As for your questions:

Is it a good idea? Sounds like it would definitely be super-neat to have. I'm not entirely sure on how much work it will be. Perhaps we can try starting out with a smallish example of two or three supported types, and take a look at how that seems to work out?

Templates or macros? Templates would be great. Note that using templates does not imply we need to come up with a generic formula covering all cases (even if that would be the ideal). We can also spell out "specializations" one by one, if needed.

Signed and unsigned: Tricky question, but of course, if we can have completeness, then why not have completeness. Again, I'd suggest to start looking at a reduced example, at first, to see, how things work out.

Hi,
Thanks for your comments!

Indeed replacing everything with classes would be quite neat. But at the same time, I wonder if we should throw away the current paradigm so easily… IMO, replacing the current types with classes, with overload of the operators, is not a good idea, basically because the way it is done can usage specific. For instance, when multiplying two Q16n16 with a wanted result of Q16n16, the total >>16 shift that is needed can be distributed differently depending on where the precision should be kept:
Q16n16 a = (b>>8) * (c>>8); or
Q16n16 a = (b>>12) * (c>>4); are not exactly equivalent.
Overloading the standard operations (on the old types) would prevent that to be done (and potentially break quite a few codes that are using that, maybe it is just me). It is clearly not an easy question but I think the final paradigm should allow both inexperienced users to learn and do things easily while not preventing advanced one in going into micro-optimizations. It would of course be super neat to have fixed maths that could be handled as easily as floats, but I am not sure it is possible to have it behaving as freely as possible at the same time. Having new, class based types, would be the solution but we have to think how these two paradigms (Qxny and Nxny) do interact. Do we want two fix maths paradigms (legacy and new)? I do not have a clear answer on that basically.

I am probably over-thinking it though. I think the multiplication is the main problem with fix_math and will try to focus on that first, trying to make an operation that handles automatically the shifts and so on. As you said, it will probably become clearer with implementation so I'll try to start one as a branch here soon. I think I'll start by adapting the fix types already present in Mozzi and see how things and examples turn out. Even if moving to separate classes, the implementation in itself will probably be reused. That branch will also be a good discussing space!

Q16n16 a = (b>>8) * (c>>8); or Q16n16 a = (b>>12) * (c>>4); are not exactly equivalent.

Point taken. However, I'll throw in that for advanced tweaks, we could easily offer direct access to the underlying int in a class, too. Also, of course, we could have a member customMult(...) or so for this sort of thing. The operator methods would simply mean to wrap the "regular" case, comfortably.

As for breaking existing code, I'll admit that would be the case, but it would break at compilation (because shift operators will not be defined; access to the underlying integer would use a getter such as toInt() to prevent that kind of type-confusion). More to the point, adding real type-safety would be a central feature to the plan.

But yes, of course, this would leave the QXnY as a mostly independent legacy paradigm. More bloat to carry around...

Very good points, you are always a few steps ahead ;) !

Looking a bit around, I actually found an Arduino library that is performing fixMaths calculations: https://github.com/Pharap/FixedPointsArduino (it actually has a get getInteger member…).
The only problem is that, if I am correct, this library is made to ensure correct results, hence is promoting types automatically. Whereas I think Mozzi should ensure types. That being said, it is a good source of inspiration, will create a branch soon to try to do something similar for Mozzi (probably dev/fixMath).

As per the QXnY, more on the long run, we can keep it at first but plan to deprecate them for a Mozzi 2.0. Has it will probably not be completely back compatible, that's our chance!

Just to avoid, double work: I created a branch for this devel/FixMath2. There is basically nothing in there so far, but feel welcome if you want to join the fun!