sensorium / Mozzi

sound synthesis library for Arduino

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

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Apparent frequency drops when amplitude increased beyond 8 bits on AVR

eclab opened this issue · comments

Actually a bug and a misfeature. First the bug.

BUG. In updateAudio() I'm pushing a saw/square/tri wave through a filter, where it's always in the range [-128,127]. I then multiply it by a gain and shift back down. It appears to be entirely proper.

Notionally I should be able to return 16-bit values from -244 to +243. But I can't. If in updateAudio() I return a value beyond +/-128 or so, the amplitude increases, but the frequency begins to drop. I'm pretty sure this is not a problem with my code -- it's either a bug in Mozzi or a misfeature of the PWM that had not been considered.

MISFEATURE. In AudioOutput, it states: "However, on AVR, STANDARD(_PLUS) (where about 8.5 bits are usable), the value will be shifted to the (almost) 9 bit range, instead of to the 8 bit range. allowing to make use of that extra half bit of resolution. In many cases it is useful to follow up this call with clip()."

I would expect that this function converts a full +/-32K signal to [-244,243]. But the function in fact appears to just shift to 9 bits, [-256,255]. Clipping back down to [-244,243] would sound awful.

But there's a straightforward and reasonably fast function to convert from +/-32K to [-244,243]:

int16_t toQuasi9Bit(int16_t val)
    {
    return ((val >> 6) * 61) >> 7;
    }

I would encourage Mozzi to adopt this rather than require us to make sure our signal fits in range before scaling; that would seem to be the library's job.

Of course, if we can't actually use the [-244,243] range per the above BUG, this is moot. :-(

Hi,

Please post a minimal working example of the bug you are describing (the frequency drop) so that we can replicate, especially as there are several filters in Mozzi.

The misfeature should probably be posted in a separate issue. A small note on that, even thought I have been using AVR except for testing for quite long and so never used this function, having a multiplication do imply an overhead that might not be easily foreseen by the user. Maybe @tfry-git will additional thoughts on that.

Documentation on this may not be brilliant, but specific suggestions welcome. Some points to consider / reasons why the implementation does what it does:

  1. The main point of these functions is to ease cross-platform development.
  2. The only platform where "8.5 bits" matter is AVR.
  3. AVR is also one of the computationally weakest platforms, i.e. saving a couple of operations per sample can really make a difference. This being an 8-bit platform actually means your conversion needs on the order of 30 extra assembly instructions.
  4. The idea is that some of the time you know that you are not fully using N bits, and that's (the only instance) where fromAlmostNBits() is meant to be used.

If your code runs on AVR, only, and you want to squeeze out every last bit of resolution, you still have the option to write entirely custom code returning a value in the supported range.

I count about 26 instructions? But if you want to actually take advantage of 8.5 bits without having to do all your values guaranted to be within 0..487 during computation -- which is likely to be VERY expensive -- then I don't see much of an alternative?

But there's another problem. As mentioned in issue #223, it appears from my tests that beyond about 400 AVR's PWM goes starts going sublinear (and it's not great near 0 either, but better), at least for my AE Modular GRAINS module. I don't know if this is the module or if it's a misfeature of the 328P. Anyway, this would suggest that the effective range should not be 0-487 but in fact about 0-400.

This, coupled with the fact that Mozzi doesn't actually convert 16 bit to -244...+243 in MonoOutput::from16Bit(...), but just -128...127 :-( :-( leads me to suggest the following functions instead for anyone on a GRAINS, and maybe an Arduino:


// Conversion of -32768...+32767 to -244...+243.  Full 0...487 range.
inline int16_t convertTo488(int16_t val) { return ((val >> 6) * 61) >> 7; }

// Conversion of -32768...+32767 to -240...+239.  Two fewer shifts, so a tiny bit faster
inline int16_t convertTo480(int16_t val) { return ((val >> 4) * 15) >> 7; }

// Conversion of -32768...+32767 to -244...+155 (range 0...399).  Biggest linear range on GRAINS but DC offset of -44
inline int16_t convertTo400(int16_t val) { return ((val >> 5) * 25) >> 7 - 44; }

// Conversion of -32768...+32767 to -160...+160 (range 0...399).  Biggest linear range on GRAINS with DC offset = 0, that is reasonbly close to the 400 limit
inline int16_t convertTo320(int16_t val) { return ((val >> 3) * 5) >> 7; }

These are somewhat fast. They're not nearly as fast as val >> 8 of course. It's too bad AVR doesn't have general shift instructions.