Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SoftClip to allow smooth transition to saturated audio #136

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

tomcombriat
Copy link
Collaborator

[Work in progress]

The clip() function already present in Mozzi is very useful for cases where the output might overflow the available number of bits available for outputting the audio. This is especially true for devices which volumes are not completely known beforehand (for instance polyphonic synths: one try to keep enough resolution for monophonic sounds but at the same time not to overflow too much when a lot of polyphony is involved).

The only drawback of clip() is that it is an hard-clip: if the output value is over the maximum output capacity of the device the outputted value is just the maximum. This leads to a "sharp" wave when transitioning for non-saturated regime to the saturated regime. This sharpness can generate a lot of inharmonicity which are unpleasant. Alongside #124 this tries to alleviate inharmonicity if needed.
This PR implements a SoftClip which ensures a smooth transition (continuity of the first derivative) of the outputted sound when approaching the saturation a bit like the "diode saturation" present in analog system.

Below an example of result of this:

  • top: an overflowing signal clipped with clip
  • bottom: an overflow signal clipped with SoftClip (note the differences at the edge of saturation).

SoftClip_test

This looks good on the waves however I hardly distinct the two sounds apart when listening, hence I am not sure if this is really useful yet. This is why this is a draft for now. I implemented this to resolve the bad saturation on one of my device but I need to do some extensive testings to see if that is really worth the trouble.

For now a few details about the implementation:

template<unsigned int SMOOTHING, typename T = AudioOutputStorage_t> class SoftClip

The SMOOTHING parameter set the vertical extension of the clip: a SMOOTHING of 200 for instance will start clipping 200 units before the saturation. Because of the shape of the clipper, it will in this case, take more than 200 units to reach saturation, which is why the lookup table used to avoid on-the-fly calculations is of size SMOOTHING * PI / 2.
The profile used is sinusoidal: at the beginning of the clipping this behaves as the identity function - it nearly returns the input value - but saturate with a null derivative at the end of the clipping.
The max_value parameter in the constructor (which can be changed afterward) set the saturating value of the clipper. I think changing this value on the fly can lead to interesting effects as it will increase the "drive", hence the saturation in a warmly manner.

Practically, this is instanciated as: soft_clip<20,int> SC(500); for instance and then called by SC.next(audio_sample).

As I said, this is work in progress, at least to know if this has some good applications, but I will gladly receive some point of views!

@tomcombriat tomcombriat marked this pull request as draft October 2, 2021 21:33
Copy link
Collaborator

@tfry-git tfry-git left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a good thing to have, overall. Here are some initial thoughts.


#include "AudioOutput.h"

template<unsigned int SMOOTHING, typename T = AudioOutputStorage_t>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut feeling is that SMOOTHING should not be a template parameter (it would rather be specified in the constructor). This is a relatively heavy class, anyway, and having this as a regular parameter may allow a little extra flexibility (allowing to adjust smoothness, at runtime, even if it will be a somewhat slow operation).

Copy link
Collaborator Author

@tomcombriat tomcombriat Nov 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that so that the lookup table would be statically allocated. If the lookup is the right way to go, then if this is not a template parameter, I do not see the way to statically allocate it without a template but maybe you can enlighten me on that?

Note that, for a "compressor" effect, the maximum_value can be changed at runtime (and compensated afterward for a full compressor). This is a bit the way analog soft clipping works: if one is using a diode, the response of the diode cannot be changed, but the level of what goes in can, which is equivalent to changing the maximum value (or boosting the signal with the same maximum value).

max_value = max_val;
for (unsigned int i = 0; i < max_threshold; i++)
{
lookup_table[i] = SMOOTHING * sin(1.*i / SMOOTHING);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought: We're already shipping sinewave lookup tables. Arguably, that's 8 bits, only (not sure, what values you have in mind for SMOOTHING), but could perhaps be reused, here? That would save the heavy computation, but could possibly save RAM, too, in case that table is already used in some Oscil in the same sketch.

Copy link
Collaborator Author

@tomcombriat tomcombriat Nov 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the calculation is done only once, probably before audio starts if the user does not try to have "local" version of this class in the main loop, hence it is very fast. Memory is more my concern however, as you pointed, the main problem of the tables is that they are 8bits only whereas this can compute a soft clip on any resolution, which is crucial for the final flattening (when reaching saturation) otherwise there are huge jumps in the output values which are more like an hard clip. We could potentially interpolate them though, but for non power of 2 values of the SMOOTHING this might end up quite a calculation… As you probably guessed now, I am usually optimizing computing time at the expense of memory.

if (abs(in) < max_value - SMOOTHING) return in;
else if (in > 0)
{
if (in < (T) (max_value - SMOOTHING + max_threshold)) return lookup_table[in - max_value + SMOOTHING] + max_value - SMOOTHING;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My guess is that it may be worth caching max_value-SMOOTHING in a class member.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed! That's a good point!

@tomcombriat
Copy link
Collaborator Author

I have to say that I have stopped a bit working on this after I got a working version. The thing is that, even if it looked good on the scope, I could hardly hear the differences, even with huge SMOOTHING parameters and then started to be dubious about if it was worth the trouble.
Maybe I should add an example in order to see if other people hear a huge difference.
Also I got distracted by the Teensy4 port and all the fixes associated, but also in documenting #124 (for this one I can clearly hear the difference and is now is everyday use on my personal synths)

Thanks for the feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants