A piecewise, memoryless soft-knee hard clipper, where a monotonic quartic curve smoothly transitions from the linear region into hard saturation without foldback, with optional bias and baseline subtraction for DC compensation.
Higher drive pushes more signal into the knee/clip region, thus gives more distortion and loudness (unless compensated).
High-pass filter cutoff (StateVariableTPTFilter highpass) that runs before the waveshaper. User has the ability to remove low frequencies before clipping so bass doesn't dominate the distortion.
Sets the clip ceiling. Increasing threshold allows higher peaks through, often louder and less saturated.
Controls the width of the transition region around the threshold. The knee uses a monotonic C2 quartic curve to smoothly reduce the transfer slope from the linear region into hard clipping. With knee = 0, the waveshaper behaves like a hard clipper, producing stronger high-order harmonics and more aliasing. Increasing the knee widens the transition, producing a smoother and softer distortion character.
The original sign of
Adds a DC offset before shaping: waveshape(x + b, ...). Creates even harmonics and a more "dirty" tone breaking the odd-symmetric property of the transfer curve.
Low-pass filter cutoff (StateVariableTPTFilter lowpass) that runs after the waveshaper (and after mix). Removes high harmonics created by clipping and can remove hissing sounds that come from high frequencies, resulting in a less fizzy distortion.
Wet/dry blend between the current sample (dry) and waveshaped sample (y). Mix=0 yields the filtered clean path (preHPF then postLPF). Mix=1 yields fully distorted (then postLPF).
Applied to the distorted (wet) signal after waveshaping (before mix) for level trim.
Should be used to reduce aliasing artifacts introduced by the nonlinearity by processing at a higher sample rate and filtering before downsampling (pending).
Here we assume that the entire processing graph in the DAW contains only the HTekDistortion plugin.
-
JUCE_LIB
The project supports local JUCE checkout (via
JUCE_LIB) or automatic fetching using CMake FetchContent (if environment variableJUCE_LIBis not set).If you want to set the environment variable execute:
Windows (Powershell)
Temporary:
$env:JUCE_LIB="\absolute\path\to\JUCE"Persistent:
setx JUCE_LIB "\absolute\path\to\JUCE"Linux / macOS
Temporary (current shell session only):
export JUCE_LIB="/absolute/path/to/JUCE"Persistent (add to ~/.bashrc, ~/.zshrc, etc):
echo 'export JUCE_LIB="/absolute/path/to/JUCE"' >> ~/.bashrc source ~/.bashrcDefine the environment variable
JUCE_LIBto reference the JUCE source root directory (the directory containingCMakeLists.txtandmodules/).If you use a different environment variable name in your build system, replace
JUCE_LIBinCMakeLists.txtaccordingly with your defined environment variable name. JUCE modules are statically linked into the plugin for easy distribution, thus object code is embedded into your plugin binary. But, the plugin itself is a dynamic library.Using the same logic you can setup your environment variable
CATCH2_LIBto be able to run the tests ineffects/tests. -
CATCH2_LIB
CATCH2_LIBmust point to the Catch2 source root (contains CMakeLists.txt).Windows (PowerShell)
Temporary:
$env:CATCH2_LIB="\absolute\path\to\Catch2"Persistent:
setx CATCH2_LIB "\absolute\path\to\Catch2"Linux / macOS
Temporary:
export CATCH2_LIB="/absolute/path/to/Catch2"Persistent (add to ~/.bashrc, ~/.zshrc, etc):
echo 'export CATCH2_LIB="/absolute/path/to/Catch2"' >> ~/.bashrc source ~/.bashrc
Windows (GUI):
Alternatively, define JUCE_LIB and CATCH2_LIB as User Environment Variables in Environment Variables (View Advanced System Settings -> Environment Variables).
-
JUCE repository: https://github.com/juce-framework/JUCE
-
Catch2 repository: https://github.com/catchorg/Catch2
-
CMake source and binary distributions: https://cmake.org/download/
-
Note: As we will see in the next section, one can choose not to build with tests and have the plugin only. Tests are here for assuring the quality of the development process.
You can build the plugin in Debug or Release mode. In your project root directory execute:
-
cmake -B build cmake --build build --config Debugcmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug cmake --build buildcmake -B build -G Xcode cmake --build build --config Debugcmake -B build -DCMAKE_BUILD_TYPE=Debug cmake --build build -
cmake -B build cmake --build build --config Releasecmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build buildcmake -B build -G Xcode cmake --build build --config Releasecmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build
Plugin artefacts are under build/plugin/*_artefacts/*/VST3/....
If you want to build with tests, then execute:
cmake -B build -DBUILD_TESTS=ON
before cmake --build <build_folder> .
Test binary will be under build/effects/tests/ (.../Debug/HTekEffectsTests.exe on Windows).
Check effects/Distortion.cpp function waveshape().
For an input audio sample
For
with
-
$$a=t \cdot (1-k), \qquad b=t \cdot (1+k) $$ -
$$u= \frac{|x|-a}{b-a} $$ -
$$q(u)=u-u^3+\frac{1}{2}u^4 $$
where
-
$x$ : input audio sample. -
$t$ : clipping threshold; the maximum output magnitude after saturation. -
$k$ : knee parameter controlling the width of the transition region. -
$a$ : start of the knee region. -
$b$ : end of the knee region. -
$u$ : normalized position of the input magnitude inside the knee. -
$q(u)$ : quartic function whose role is to curve the normalized knee transition. -
$\mathrm{sgn}(x)$ : restores the sign of the original input sample.
For
For
the input sample passes through unchanged:
The slope is
At the boundary
so
When
we have
The linear region becomes
with
For the interior
At the boundary points
the derivative does not exist, because the linear region joins directly to the hard-clipped region.
For
the output magnitude is fixed at the clipping threshold:
Therefore,
The slope is
At the boundary
so
When
we have
The hard-clipped region becomes
with
For the interior
At the boundary points
the derivative does not exist, because the linear region joins directly to the hard-clipped region.
Notice that the knee branch is never reached for
Therefore, if
The input magnitude is mapped from the knee interval
to a normalized position
using
So that
and
The normalized position is then passed through the quartic transition
For this quartic,
and
so the mapping is
Also
and
Therefore:
-
$q'(0)=1$ : the knee joins the linear region with slope 1. -
$q'(1)=0$ : the knee joins the clipped region with slope 0. -
$q''(0)=q''(1)=0$ : the second derivative matches at both joins. -
$q'(u)>0$ for$u \in (0,1)$ : the knee is strictly monotonic and has no foldback.
The quartic value is then mapped back to an output magnitude:
Since
the output magnitude maps as
Because
we have
Therefore,
Therefore the complete knee mapping is
Finally, the original input sign is restored:





