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

SoftClip to allow smooth transition to saturated audio #136

Open tomcombriat opened 2 years ago

tomcombriat commented 2 years ago

[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:

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 commented 2 years ago

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!