Saturation Models (Neve, Tweaker, Oxford Inflator) in FAUST
-
I’m a simple person—I like loudness!
I’ve been experimenting with an inflator-like idea these past few days, and here’s the code I came up with:
declare name "Inflator-like test1)"; import("stdfaust.lib"); // Interface input_slider = hslider("Input (dB)", 0, -6, 12, 0.01); effect_slider = hslider("Effect (%)", 0, 0, 100, 0.1); curve_slider = hslider("Curve", 0, -50, 50, 0.1); clip_slider = checkbox("Clip 0 dB"); band_split_slider = checkbox("Band Split"); effect_in_slider = checkbox("Effect In"); output_slider = hslider("Output (dB)", 0, -12, 0, 0.01); // Utility functions clamp(x, minv, maxv) = min(maxv, max(minv, x)); sign(x) = (x > 0) - (x < 0); // Convert UI values to processing parameters pre = input_slider : ba.db2linear; post = output_slider : ba.db2linear; wet = effect_slider * 0.01 * 0.99999955296; dry = 1 - (effect_slider * 0.01); // Curve coefficients A = (curve_slider * 0.01 + 1.5); B = (curve_slider * -0.02); C = (curve_slider * 0.01 - 0.5); D = (0.0625 - curve_slider * 0.0025 + (curve_slider * curve_slider) * 0.000025); // SVF Filter implementation svf_coefficient(cutoff) = tan(ma.PI * (cutoff/ma.SR - 0.25)) * 0.5 + 0.5; svf_lpf(cutoff) = _ : fi.tf2(b0, b1, b2, a1, a2) with { c = svf_coefficient(cutoff); b0 = c * c; b1 = 2 * b0; b2 = b0; a1 = 2 * (b0 - 1); a2 = 1 - 2 * c + b0; }; svf_hpf(cutoff) = _ : fi.tf2(b0, b1, b2, a1, a2) with { c = svf_coefficient(cutoff); b0 = 1; b1 = -2; b2 = 1; a1 = 2 * (c * c - 1); a2 = 1 - 2 * c + c * c; }; // Band splitting with integrated gain calculation band_split(x) = low, (mid * mid_gain), high with { low_cutoff = 240; high_cutoff = 2400; c_low = svf_coefficient(low_cutoff); c_high = svf_coefficient(high_cutoff); mid_gain = c_high * (1 - c_low) / (c_high - c_low); low = x : svf_lpf(low_cutoff); high = x : svf_hpf(high_cutoff); mid = x - low - high; }; // Waveshaping function waveshaper(x) = select2(abs(x) < 1, select2(abs(x) < 2, x, // > 2 (2 * abs(x) - abs(x) * abs(x)) * sign(x)), // 1 < x < 2 (A * abs(x) + B * (abs(x) * abs(x)) + C * abs(x) * (abs(x) * abs(x)) - D * ((abs(x) * abs(x)) - 2 * (abs(x) * abs(x)) * abs(x) + (abs(x) * abs(x) * abs(x) * abs(x)))) * sign(x)) // x < 1 * wet + x * dry; // Main processing function process = par(i, 2, channel_process) with { channel_process(x) = x : input_stage : effect_stage : output_stage; input_stage(x) = x * pre <: select2(clip_slider, clamp(_, -2, 2), clamp(_, -1, 1)); effect_stage(x) = select2(effect_in_slider, x, select2(band_split_slider, waveshaper(x), band_split_process(x))); band_split_process(x) = band_split(x) : (waveshaper, waveshaper, waveshaper) :> _; output_stage(x) = select2(abs(x) < 0.0000000000000000555111512312578270211815834045, x * post, 0); };
-
@Mighty23 looks complicated but sounds nice as far as I can tell. just the band split does nothing
-
@Morphoice
abs(x) < 0.0000000000000000555111512312578270211815834045
hum seems like a non human intervention here ?
-
@sletz
Yes, it's not just a non-human intervention—it's a lifesaving one :)I’ve been dealing with crashes, and after investigating, I realized I need denormal protection. I received several suggestions and implemented the one that made the most intuitive sense to me—though perhaps I’m falling into some cognitive biases?
How can I test for denormal issues?
Can you explain real-world methods for denormal protection? -
We have tools to help debugging, read:
https://faustdoc.grame.fr/manual/debugging/#debugging-at-runtime
https://faustdoc.grame.fr/tutorials/debugging/This interp-tracer tool is currently to be used in the terminal, so requires a local installation. But in theory this kind of tool could be integrated in HISE , since the libfaust library used in HISE also embeds the needed Interpreter backend.
For more local NAN protection, using ma.EPSILON is a more portable solution, since is adapts the single/double compilation option.
-
@Mighty23
Nice work!
For the band split, you may want to use the linkwitz riley instead of svf.About the denormal protection, may I know what CPU you're are running this code on? Does this directly cause the crashing?
This might be useful for some legacy CPUs and it won't likely really be a problem for modern x64 CPU afaik. -
@Mighty23 Very interesting, and thank you for sharing. Am I correct that this is multiband distortion, or is there also some dynamics processing as well?
-
@Morphoice Thank you for sharing. Not a whacking, but I work with two of these companies; could I ask you to pls characterise these as more "…in the spirit of…" and not "…copies of…"?
-
@Allen I assume nobody is here posting actual transfer functions they measured.
-
@clevername27 said in Saturation Models (Neve, Tweaker, Oxford Inflator) in FAUST:
or is there also some dynamics processing as well?
there is no compressor/limiter/gate in the processing. I would consider it 100% multiband waveshaping.
@Allen said in Saturation Models (Neve, Tweaker, Oxford Inflator) in FAUST:
For the band split, you may want to use the linkwitz riley instead of svf.
Yes, for sure. Many Thanks.
@Allen said in Saturation Models (Neve, Tweaker, Oxford Inflator) in FAUST:
may I know what CPU you're are running this code on?
10510u i7 on Windows
Late 2016 Mini Mac overclocked and open-core operating system. -
@clevername27 they're not measured, someone on reddit reverse enginered them so they are somewhat common knowledge among the DSP community, I'm not claiming any of those is a copy of something, it's just knowledge I gathered off the web. This is an old post though, I'm making my own functions for saturation now, and I'm using desmos to create them, closely matching stuff I can indeed measure from real hardware and then bring them over in faust. Unfortunately it's all done by hand, I have no Idea on how to "automatically" transfer measurements into transfer functions. It's a lot of guesswork until the curve looks somewhat similar
Those are the two waveshaper curves I find most pleasing, sonically, anything in between those is great and much faster calculated than the popular tanh for saturation
-
@Morphoice Thank you for sharing.
-
If you want to find the functions automatically it's not that hard I can show you. It can be done with python or MATLAB very easily.
You'll quicky discover that a static waveshaper cannot represent the measurements of analog distortion, but you can however create the 'best fit' automatically.
-
@griffinboy it's probably a good idea to morph between waveshaping curves according to signal strength but then again here we are considering hysteresis again ;)
-
yeah no, morphing a waveshaper based on signal strength doesn't really have much effect unless it has memory or smoothing. Else you've just done a static transformation of the curve and created another static curve. It has to have memory / hysteresis to actually represent anything nonlinear.
But collecting transfer function data from analog devices can still be super useful and can inform approximations.
-
@Morphoice Where can I learn how to apply this? Maybe you can tell me what this is and I can do y research. Thanks brotha.