-
Notifications
You must be signed in to change notification settings - Fork 190
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
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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> |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
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 Thanks for the feedback! |
[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:
clip
SoftClip
(note the differences at the edge of saturation).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 sizeSMOOTHING * 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 bySC.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!