sensorium / Mozzi

sound synthesis library for Arduino
https://sensorium.github.io/Mozzi/
GNU Lesser General Public License v2.1
1.07k stars 186 forks source link

16 bit wavetable support #132

Open daanklijn opened 3 years ago

daanklijn commented 3 years ago

Currently the wavetables have an 8 bit resolution. It would be nice to also support a higher resolution like 16 bit, especially in combination with a 12 bit DAW. Any idea how to implement this?

daanklijn commented 3 years ago

I managed to quickly add 16 bit wavetable support by slightly altering the Oscil class. My rough implementation can be found in the Oscil16Bit class. Although it might be better to include this functionality directly into the Oscil class / let the Oscil16Bit inherit from Oscil, I currently don't have the time / Mozzi knowledge to properly implement this. Feel free to extend my code.

133

donluca commented 2 years ago

I would definitely love to see this extended to all the other wave forms. Now that powerful devices like ESP8266 and ESP32 are around, using 16-bit wave forms should be a non-issue in most cases.

eclab commented 9 months ago

I would also like to see this, particularly for the AVR arduino. Yes, it's 8-bit, but its output range is -488...487 and we can't take advantage of that right now with Oscil: in effect, from16Bit isn't all that useful because the input was just upsampled from 8 bit! Perhaps we could make an Oscil16?

sensorium commented 9 months ago

I hope this doesn't sound defeatist, but as the initial author of Mozzi, I think at this time that to make meaningful use of 16 bit wavetables, most of the library would need redesigning.
The main challenge in first writing Mozzi was optimising it within the limitations of the 8 bit processors which were available then. Even if you have a 16 bit wavetable, you'd often need to scale it back to 8 bits to work with the synthesis classes in Mozzi.
I might be wrong, but I think the code would become unwieldy if you tried porting all of the synthesis classes differently for multiple processors. If you want higher resolution synthesis, I expect it would be simplest to find or begin a new library designed for more capable processors.

tomcombriat commented 9 months ago

Hi, Just to add a bit of fuel to this discussion: I actually think that most of the classes would manage more or less okay, especially the ones that have been modified recently where we tried to take 32 bitters (the ResonantFilter.h is a good example for instance). Now, as @sensorium said, some others might need the same type of modifications but that should be more or less limited to the ones dealing directly with waveforms (can't think of any on top of my head). There might also be a bit of the backbones to modify but recently with the modularization of AudioOutput should be quite straightforward (I managed to output more than 16 bits on some designs).

On the other hand, I always wondered what would be the use case of a 16bits wavetables? I personally nearly always end up with more than 16bits before outputting: you multiply a Oscil with a 8bits envelope and you are already at 16bits… I guess there are cases where the waveforms could gain having more resolution but I never felt the need. @eclab do you have something particular in mind?

Just to say, I'm not against it, I am just questioning the usefulness of it, especially for 8bitters like the Arduino where timings are already quite short…

eclab commented 9 months ago

Oscil is only 8 bits. If we're typically outputting data at a constant gain, it's still 8-bits no matter how you slice it. But to keep things simple, loet's say that all I want to do is output a sine wave, for example. In fact I do that all the time with additive synths on Mozzi. All Oscil can give me is 0...255, but the Arduino can output 0...487. So I multiply the Oscil by 256, then hand it to from16Bit(...) to convert it to 0...487, but it's still 8-bit data, not 9-bit as the Arduino can do.

But if Oscil (or an Oscil16) optionally outputted 16-bit, and we had 16-bit wavetables available (Or 8-bit mu-law!) I could likely get higher quality sound out of the Arduino than Mozzi is presently providing.

If you want to multiply the gain by an envelope, sure, you'll go into 32-bit territory and then divide back down to 16-bit likely. Or you could just have your envelope go 0...64, use a -244...+243 wavetable stored in 16-bit, and you'd still be in 16 bit and have the full wavetable resolution that the Arduino can generate.

eclab commented 9 months ago

For me, I don't need the rest of the library: I literally just need 16-bit wavetables and (ideally) 16-bit filters for them, so I can call next(). I handle the rest of it myself.

BTW, I have also been doing 8-bit u-law samples, which sound FAR better than the hissy 8-bit linear samples in Mozzi's Sample class. I just load u-law samples into Sample, call next(), and run it through a lookup table to convert to 16-bit. It is a huge improvement. I am a C guy, not a C++ guy, so I don't know exactly how to modify the Sample code to do this automatically, but I'd be glad to provide that table if someone was interested in incorporating it.

On Dec 19, 2023, at 10:14 AM, Mr Sensorium @.***> wrote:

I hope this doesn't sound defeatist, but as the initial author of Mozzi, I think at this time that to make meaningful use of 16 bit wavetables, most of the library would need redesigning. The main challenge in first writing Mozzi was optimising it within the limitations of the 8 bit processors which were available then. Even if you have a 16 bit wavetable, you'd often need to scale it back to 8 bits to work with the synthesis classes in Mozzi. I might be wrong, but I think the code would become unwieldy if you tried porting all of the synthesis classes differently for multiple processors. If you want higher resolution synthesis, I expect it would be simplest to find or begin a new library designed for more capable processors.

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you commented.

tomcombriat commented 9 months ago

For me, I don't need the rest of the library: I literally just need 16-bit wavetables and (ideally) 16-bit filters for them, so I can call next(). I handle the rest of it myself.

Alright, note that 16-bits filters are already there (ResonantFilter).

The u-bit law sample is interesting, but why using Sample instead of Oscil ? There are basically the same thing. I think the correct way to do it would be to send it afterward to WaveShaper, with the correct lookup table. Such a inverse u-law convert table would indeed be interesting to have built-in in Mozzi (probably just need to add the lookup somewhere and add an example of usage in combination with WaveShaper).

eclab commented 9 months ago

Well, there is a gotcha.

u-Law is stupidly designed: 0 input does not produce 0 output. So when the sample is over, obviously 0 is being produced, which when pumped through the lookup table will result in 32124.

So to do u-Law you have to do this:

if (sample.isPlaying()) return 0; else return ulaw[sample.next()]

Thus you can't do this with just a waveshaper. Since u-Law is normally only used for samples, it'd make sense to incorporate it into the Sample class.

On Dec 19, 2023, at 11:41 AM, Thomas Combriat @.***> wrote:

For me, I don't need the rest of the library: I literally just need 16-bit wavetables and (ideally) 16-bit filters for them, so I can call next(). I handle the rest of it myself.

Alright, note that 16-bits filters are already there (ResonantFilter).

The u-bit law sample is interesting, but why using Sample instead of Oscil ? There are basically the same thing. I think the correct way to do it would be to send it afterward to WaveShaper, with the correct lookup table. Such a inverse u-law convert table would indeed be interesting to have built-in in Mozzi (probably just need to add the lookup somewhere and add an example of usage in combination with WaveShaper).

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.

eclab commented 9 months ago

Here's the uLaw lookup table. It takes a uint8_t input from Sample.next() and produces a uint16_t output. From there you can pass it to from16Bit(...) to output it. But make sure that sample is playing: if it is not, you need to pass from16Bit(0).

const int16_t __ULAW[] = { -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956, -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764, -15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412, -11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316, -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, -876, -844, -812, -780, -748, -716, -684, -652, -620, -588, -556, -524, -492, -460, -428, -396, -372, -356, -340, -324, -308, -292, -276, -260, -244, -228, -212, -196, -180, -164, -148, -132, -120, -112, -104, -96, -88, -80, -72, -64, -56, -48, -40, -32, -24, -16, -8, 0, 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, 876, 844, 812, 780, 748, 716, 684, 652, 620, 588, 556, 524, 492, 460, 428, 396, 372, 356, 340, 324, 308, 292, 276, 260, 244, 228, 212, 196, 180, 164, 148, 132, 120, 112, 104, 96, 88, 80, 72, 64, 56, 48, 40, 32, 24, 16, 8, 0, };

This table was generated from a famous and old chunk of code:

/*
** This routine converts from ulaw to 16 bit linear.
**
** Craig Reese: IDA/Supercomputing Research Center
** 29 September 1989
**
** References:
** 1) CCITT Recommendation G.711  (very difficult to follow)
** 2) MIL-STD-188-113,"Interoperability and Performance Standards
**     for Analog-to_Digital Conversion Techniques,"
**     17 February 1987
**
** Input: 8 bit ulaw sample
** Output: signed 16 bit linear sample
*/

/*
  #include <stdio.h>

  typedef unsigned char uint8_t;
  typedef signed short int16_t;

  const int16_t exp_lut[8] = {0,132,396,924,1980,4092,8316,16764};
  int16_t ulaw2linear(uint8_t ulawbyte)
  {
  ulawbyte = ~ulawbyte;
  uint8_t sign = (ulawbyte & 0x80);
  uint8_t exponent = (ulawbyte >> 4) & 0x07;
  uint8_t mantissa = ulawbyte & 0x0F;
  int16_t sample = exp_lut[exponent] + (mantissa << (exponent + 3));
  if (sign != 0) sample = -sample;
  return sample;
  }
  int main( int argc, char *argv[] )
  {
  for(int i = 0; i < 256; i++)
  {
  printf("%d, ", (ulaw2linear(i) * 244) >> 15);
  if (i % 16 == 15) printf("\n");
  }
tomcombriat commented 9 months ago

u-Law is stupidly designed: 0 input does not produce 0 output.

Have to say I do not really get it… You can put any lookup table in the WaveShaper, in particular ones that artificially maps 0 to 0. Having a test at AUDIO_RATE could also lead to some troubles in terms of performances.

Also, I guess this would integrate way more if working on signed samples (like the ones handled by Mozzi).

This triggered my curiosity, will probably try at some point!

eclab commented 9 months ago

For me, I don't need the rest of the library: I literally just need 16-bit wavetables and (ideally) 16-bit filters for them, so I can call next(). I handle the rest of it myself.

Alright, note that 16-bits filters are already there (ResonantFilter).

That's right. All we really need is a 16-bit oscil, and sin/square/tri/saw tables in 16-bit.

tomcombriat commented 9 months ago

@eclab Are you using u-law with PWM output? I read here and there that this is mainly used for PCM, does it also improves the quality for PWM and DAC outputs?

eclab commented 9 months ago

Before I answer that question, one of my own. Supposedly the PWM output on the Arduino can range from -244 to + 243 (hence the "8.5 bit" stuff). Indeed the fromAlmostNBit(...) function documentation says:

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().

But it does nothing of the sort. It just scales to 9 bit (+/-256) and expects you to clip the result. That's gonna sound terrible. I don't understand why it would do this when there's such an easy procedure to scale to exactly the right value:

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

Is it in fact the case that the PWM range is 488 values? And if so, why is Mozzi just clipping?

eclab commented 9 months ago

At any rate, uLaw is just a companding algorithm: it logarithmically stretches 8 bits into 12 bits in a clever way designed to sound like 12 bits to human ears. It's useful for reducing the noise floor and for taking advantage of the 9 bits if you had them.

tomcombriat commented 9 months ago

That's interesting, will try to test that in a WaveShaper, once I get a bit of time from #212 and #211. In that case we could "just" provide tables for expansion to some higher number of bits (10, 12, 14) and the transformed tables (there are a bit more than four of them if you consider the band-limited ones).