View Single Post
 06-21-2015, 02:26 AM #1 SaulT Human being with feelings   Join Date: Oct 2013 Location: Seattle, WA Posts: 809 Under the Hood : Loser/Saturation I saw a question on Reddit asking about how the Loser/Saturation and Schwa/Softclipper plugins work. Being as how Loser/Saturation is one of my favorite saturation algorithms, I figured I'd try and explain what's going on inside of it, maybe give some insight. Hopefully I'll have a chance to do the same for schwa/softclipper as well. Minus the graphics code, Loser/saturation looks like this: Code: ```desc:Saturation slider1:0<0,100,1>Amount (%) @slider foo=slider1/200*\$pi; bar = sin(slider1/200*\$pi); @sample slider1 ? ( spl0 = min(max( sin(max(min(spl0,1),-1)*foo)/bar ,-1) ,1); spl1 = min(max( sin(max(min(spl1,1),-1)*foo)/bar ,-1) ,1); );``` In short, this is a waveshaping function that uses the sine function. The curve is gentle and the effect is overall fairly subtle. The formula looks like this f(x) = sin(x * a * 0.5 * pi)/sin(a * 0.5 * pi) where a is (0,1] https://www.desmos.com/calculator/qadjuqwva3 As an explanation for why it works (which may or may not make sense)... if the input x is less than or equal to 1, and a is less than or equal to 1, then the output of the sin() will range from 0 to 1, because the sin(0) = 0 and sin(pi/2) = 1. Since sin() takes radians as an input, when we take sin() of an input that is clamped to 1 our maximum value is less than 1. In this function, instead of a maximum input of 1 our maximum input is pi/2. In effect, this normalizes the input. The output would still range from 0 to a maximum of sin(a*pi/2), though, so dividing it by that value (bar), we normalize the output to [-1,1]. Or.... you know, go back and play with the slider and stare blankly at the formula until it makes sense. One of my favorite aspects of this approach is that it stacks very well. https://www.desmos.com/calculator/0xxkcccuge So stepping through the code, Code: ```desc:Saturation slider1:0<0,100,1>Amount (%)``` Description and setting the slider value between 0 and 100. Code: ```@slider foo=slider1/200*\$pi; bar = sin(slider1/200*\$pi);``` On startup and when the slider changes we use the slider value to calculate foo, a value between 0 and pi/2. You can see how slider1/200*\$pi is another way to say a*0.5*\$pi, right? So anyways, foo is our input normalizer and bar is our output normalizer. Code: ```slider1 ? ( spl0 = min(max( sin(max(min(spl0,1),-1)*foo)/bar ,-1) ,1);``` Right off the bat, if slider1 is zero, then the plugin passes the audio through. After that we have to decode a nested statement, but once we look at our formula above it rolls apart pretty easily - Code: `min(max( sin(max(min(spl0,1),-1)*foo)/bar ,-1) ,1);` Here we clamp our input to between -1 and 1. Code: `min(max( sin(max(min(spl0,1),-1)*foo)/bar ,-1) ,1);` This is the waveshaping formula above. Code: `min(max(sin(max(min(spl0,1),-1)*foo)/bar,-1),1);` While strictly speaking shouldn't be necessary, for safety's sake we clamp the output as well. We do this step for both channels... and we're done. That's all. The hardest part is grasping what exactly is happening in that formula, it's a very interesting little tidbit. Note that you can also use an alternate formula to do this, e.g. the classic cubic soft clipper... f(x) = ((x*a)-((x*a)^3)/3)/(a-(a^3)/3) a = (0,1] https://www.desmos.com/calculator/dylerpjqrw One last way that I can help offer some insight into how the plugin works is to rewrite it, perhaps in a slightly more readable way (without graphics code, sorry). So, here you go! Code: ```Desc:Loser/Saturation rewrite Slider1:0<0,1,0.01>Saturation @slider foo = slider1 * \$pi/2; bar = sin(foo); @sample slider1 ? ( spl0 = max(min(spl0,1),-1); spl0 = sin(spl0*foo)/bar; spl1 = max(min(spl1,1),-1); spl1 = sin(spl1*foo)/bar; );``` And that's it! Hope this has been a useful insight into how this plugin works.