DISTORTION PLUGIN AUDIO PROGRAMMING 2 ASSESMENT 1
Rachel Locke School of Computing and Engineering Digital Audio Engineering Stephen Oxnard May 2021
Table of Contents INTRODUCTION ..................................................................................................................................................................... 3 FULL-WAVE RECTIFICATION ........................................................................................................................................... 3 CLIPPING ................................................................................................................................................................................. 5 HARD CLIPPING.................................................................................................................................................................. 5 ARCTANGENT SOFT CLIPPING ....................................................................................................................................... 8 BIT REDUCTION ................................................................................................................................................................... 11 COMBINING DISTORTION EFFECTS IN SERIES AND PARALLEL ........................................................................ 14 MATLAB PROTOTYPE CODE ........................................................................................................................................... 14 JUCE PLUGIN ........................................................................................................................................................................ 16 TESTING ................................................................................................................................................................................. 18 METHODOLOGY............................................................................................................................................................... 18 PLOTS ................................................................................................................................................................................. 18 FURTHER CONSIDERATIONS .......................................................................................................................................... 24 REFERENCES ........................................................................................................................................................................ 25 APPENDICIES ........................................................................................................................................................................ 25
List of Figures Figure 1. Sinusoidal Input vs Full-Wave Rectification Waveform Figure 2. Full-Wave Rectification and Resulting Harmonic Distortion Figure 3. Full-Wave Rectification Characteristic Curve Figure 4. Even Harmonic Distortion of Full-Wave Rectification Figure 5. Sinusoidal Input Hard Clipping for Various Values of πΌ = ππππ πππ£ππ Figure 6. Hard Clipping Characteristic Curves for Various Values of πΌ = ππππ πππ£ππ Figure 7. Odd Harmonic Distortion of Odd Symmetrical Hard Clipping Figure 8. Even and Odd Harmonic Distortion of Odd Asymmetrical Hard Clipping Figure 9. Clipping for various input gains πΊ Figure 10. Effect of Gain Staging on the Magnitudes of Hard Clipping Harmonic Distortion Figure 11. Sinusoidal Input Arctangent Clipping for Various Values of πΌ Figure 12. Arctangent Clipping Characteristic Curves for Various Values of πΌ Figure 13. Odd Harmonic Distortion of Arctangent Clipping πΌ = 6 Figure 14. Sinusoidal Input Bit Reduction for Various Values of πππ‘ ππππ‘β Figure 15. Bit Reduction Characteristic Curves for Various Values of πππ‘ ππππ‘β Figure 16. Harmonic Distortion of Bit Reduction as Bit Depth Decreases Figure 17. Sinusoidal Input vs Bit Reduction πππ‘ ππππ‘β = 4 Figure 18. Distortion Plugin Signal Flow Diagram Figure 19. Wet/Wet Mix Figure 20. Full Wave Rectification MATLAB vs C++ Render for Test Sine 200π»π§ Figure 21. C++ Render vs MATLAB Render Full-Wave Rectification FFT Figure 22. Arctangent Clipping MATLAB vs C++ Render for Test Sine 200π»π§ Figure 23. C++ Render vs MATLAB Render Arctangent Clipping FFT Figure 24. Hard Clipping MATLAB vs C++ Render for Test Sine 200π»π§ Figure 25. C++ Render vs MATLAB Render Hard Clipping FFT Figure 26. Hard Clipping C++ Render for Various Pre-Gain Values Figure 27. Bit Reduction MATLAB vs C++ Render for Test Sine 200π»π§ Figure 28. C++ Render vs MATLAB Render Bit Reduction FFT Figure 29. C++ Render vs MATLAB Render Bit Reduction FFT β± = 600π»π§ Figure 30. Overall Distortion Output MATLAB vs C++ Render for Test Sine 200π»π§
INTRODUCTION This report covers the design and prototyping of a distortion algorithm in MATLAB and the implementation of the prototype in C++/JUCE as a VST3 Plugin. The main aim is to produce a fully functional distortion plugin that users can interact with in various ways in order to affect the character of the distortion. Distortion is a form of non-linear processing characterised by the deformation of a waveform and the subsequent generation of harmonic distortion. It is necessary here to clarify exactly what is meant by non-linear. Non-linearity describes the behaviour of a system where the output does not have a linear relationship with the input. A characteristic of linear effects is that they are incapable of creating harmonic content that is not present in the input signal. In contrast, the non-linear property of distortion effects can produce novel frequency occurrences in the output (Reiss and McPherson, 2015). Whilst linear effects such as filtering may alter the magnitude and phase response of an input signal they cannot generate harmonics and affect timbre in the same way that non-linear processing is capable of (ZΓΆlzer, 2011). Distortion effects can be broadly defined by their characteristic curve, a function that illustrates the relationship between output sample amplitude and input sample amplitude. Properties of characteristic curves and their relationship to harmonic distortion will be discussed in more detail throughout. Distortion is a memoryless effect, meaning that the current output sample only relies on the current input sample and isnβt dependant on any previous or future input values (Tarr, 2019). By contrast, memory-using effects like delay require storage of previous sample values to be played back at a later time to create the characteristic echo effect.
FULL-WAVE RECTIFICATION
Figure 1. Sinusoidal Input vs Full-Wave Rectification Waveform
Turning now to full-wave rectification as an introduction to the distortion effects employed in this project. Perhaps the most algorithmically simplistic distortion to implement, any negative values of the input signal are made positive. This is equivalent to taking the absolute value of the input signal.
π¦[π] = |π₯[π]| Full wave rectification halves the period of a sinusoidal signal and therefore severs the relationship between the original period and the fundamental frequency. When the period is halved the frequency is doubled. If the result of the full wave rectification is analysed in the frequency domain, for a sinusoidal input of 200Hz it can be seen that the fundamental is now at 400Hz, however, full-wave rectification creates harmonics at even multiples of the original 200Hz fundamental (fig 2).
Figure 2. Full-Wave Rectification and Resulting Harmonic Distortion (ZΓΆlzer, 2011) Examining the characteristic curve for full wave rectification (fig 3) it can be seen that the function has even symmetry. The condition for an even function is as follows:
π₯ [βπ] = π₯[π] This even symmetry results in the even harmonic distortion that full wave rectification creates (fig 4).
Figure 3. Full-Wave Rectification Characteristic Curve
Figure 4. Even Harmonic Distortion of Full-Wave Rectification
CLIPPING The algorithm is designed such that the user can choose between hard or soft arctangent clipping in parallel with the series modifiers. The user can then dictate how much the effect of their chosen clipping is present in the final output via a wet/wet mix parameter. HARD CLIPPING
Figure 5. Sinusoidal Input Hard Clipping for Various Values of πΌ = ππππ πππ£ππ The hard clipping implementation allows users to set upper and lower clip levels, giving them control over the amount of hard clipping and subsequently, the symmetry of the characteristic curve. If both upper and lower clip level sliders are set to the same Β± value, the resultant characteristic curve has odd symmetry (fig 6) and will produce odd harmonic distortion (fig 7). The condition for oddness is as follows.
π₯ [βπ] = β π₯[π]
Figure 6. Hard Clipping Characteristic Curves for Various Values of πΌ = ππππ πππ£ππ
Figure 7. Odd Harmonic Distortion of Odd Symmetrical Hard Clipping Otherwise, different values for upper and lower clip levels will result in odd asymmetry and allow users to introduce odd and even harmonic distortion into the mix (fig 8).
Figure 8. Even and Odd Harmonic Distortion of Odd Asymmetrical Hard Clipping The final hard clipping-based parameter that users are able to influence is the pre-gain. Amplification of the signal prior to hard clipping dictates how much of the signal is preserved and how much is clipped off affecting the tonal quality of the output (Creasey, 2017). The more the signal is amplified before applying the hard clipping, the more severe the clipping effect and the higher the percentage of the discarded signal values (fig 9, 10).
Figure 9. Clipping for various input gains πΊ (Reiss and McPherson, 2015)
Figure 10. Effect of Gain Staging on the Magnitudes of Hard Clipping Harmonic Distortion
ARCTANGENT SOFT CLIPPING
Figure 11. Sinusoidal Input Arctangent Clipping for Various Values of πΌ Arctangent distortion is a form of soft clipping based on the arctangent curve.
π¦[π] =
2 arctan (πΌ β π₯[π]) π
The value for πΌ dictates the shape of the characteristic curve and the relative distortion. For this reason, the parameter πΌ is made user controllable with a range πΌ β [1 , 9]. The effect on a sinusoidal input and the characteristic curve for various values of πΌ are show in (fig 11) and (fig 12). Compared to hard clipping, arctangent clipping generates a warmer tone owing to the smoothness in the way it approaches the clipping region. The characteristic curve of arctangent clipping maintains odd symmetry for all values of πΌ and therefore generates harmonics at odd multiples of the input frequency (fig 13).
Figure 12. Arctangent Clipping Characteristic Curves for Various Values of πΌ
Figure 13. Odd Harmonic Distortion of Arctangent Clipping πΌ = 6
Figure 14. Sinusoidal Input Bit Reduction for Various Values of πππ‘ ππππ‘β
BIT REDUCTION Bit reduction, or βbit crushingβ as itβs more commonly referred to in the audio effect literature, processes an input signal such that the resulting output can only retain certain possible amplitude values (Tarr, 2019). Reduction of the bit resolution results in the discarding of information previously available to represent the signal, in this case amplitude information. Conversely, increasing the bit resolution better represents the soundβs amplitude profile. Decreasing the bit depth gives a stepped character to the signal (fig 14), which is reminiscent of a signal after the quantisation stage in an analogue to digital converter. For this reason, bit reduction can be thought of as re-quantizing a signal, almost always at a lower bit depth, to create destructive distortion.
Figure 15. Bit Reduction Characteristic Curves for Various Values of πππ‘ ππππ‘β Bit reduction is achieved by rounding the samples of a signal to the nearest available amplitude value, the nearest available amplitude value is dictated by π in the formula:
π¦=
!"#$% ('( ) '
= π ππ’ππ (ππ₯) β
* '
To achieve an explicit relationship between bit depth and amplitude values the rounding function used here is ceil(). Ceiling functions always round up instead of rounding up or down based on the sample value. A limitation of MATLABβs rounding functions is that they round to the nearest integer, therefore, a sinusoid of Β± 1 is only going to have the ability to round off to β1, 0 or +1. To get around this, the amplitude of the signal can be increased by a scaling factor π, rounded and then divided by the same scaling factor, returning the signal to be within the range Β± 1. The user has control over the bit depth of the bit reduction. Lowering the bit depth results in fewer amplitude values at which to represent the signal, therefore, a lower resolution and a more extreme, destructive distortion. Bit reduction creates harmonic distortion at even and odd multiples of the input frequency, these are more prominent at odd multiples of the input due to the relative oddness of the characteristic curve. The magnitudes of the harmonics are dictated by the bit depth, with lower bit depths producing harmonics with greater magnitudes (fig 16).
Figure 16. Harmonic Distortion of Bit Reduction as Bit Depth Decreases The noise introduced by the bit crusher is called quantisation noise. Smaller amounts of quantisation noise for moderately reduced bit depths i.e., πππ‘ ππππ‘β = 8 can give the sound a warmer tone, whilst drastically reducing the bit depth (fig 16) gives a noisier, harsher quality to the sound (Tarr, 2019). In a distortion effect the quantization noise is a welcome characteristic and adds to the overall harmonic distortion profile. However, in analogue to digital converters this quantisation noise is the unwelcome product of quantization error (the difference between the continuous signal and the nearest available digital amplitude at which to represent it). In these systems quantization error can be corrected with dither, this involves adding noise to the signal pre-quantization resulting in quantization noise which is not harmonically related to the signal and therefore not as perceptible (Pohlmann, 2011). The relationship between bit depth and amplitude values is as follows. ππ’ππππ ππ πππ π ππππ ππππππ‘π’ππ π£πππ’ππ = 2! , π€βπππ π ππ πππ‘ ππππ‘β Thus, a bit depth of 4 allows for 16 possible amplitude values (fig 17).
Figure 17. Sinusoidal Input vs Bit Reduction πππ‘ ππππ‘β = 4
COMBINING DISTORTION EFFECTS IN SERIES AND PARALLEL
Figure 18. Distortion Plugin Signal Flow Diagram In series, an input signal is modified by two or more different modifiers where each effect builds on the modification of the previous effect, the output of the first modifier becomes the input to the second modifier and so on. Conversely, in parallel modifiers receive the same unprocessed input signal and alter the signal independently before their outputs are summed back together to form the overall output (Creasey, 2017). The mix between parallel instances each with one or more modifier is referred to as wet/wet (fig 19), as each parallel instance modifies the input in some way. This is opposed to an instance where the unprocessed input signal is combined in parallel with a modifier, the mix between the modified input signal and the unprocessed input signal is referred to as dry/wet.
Figure 19. Wet/Wet Mix (Creasey, 2017) Due to the fact that both the parallel and series forms of the distortion will distort the input signal, the output will have a wet/wet mix which will dictate the contribution from the distortion effects in series (bit reduction and full-wave rectification) and the distortion effects in parallel (hard and soft clipping). The range is β [0, 1] where 0 is purely the parallel hard/soft clipping portion, 1 is purely the series bit reduction and rectification portion and 0.5 gives equal contribution from both series and parallel. In the case that a signal is combined in parallel with a modifier, the mix between the modified input signal and the unprocessed input signal is referred to as dry/wet (Creasey, 2017).
MATLAB PROTOTYPE CODE Bit Reduction Function
function [y] = bitCrusher(x, bitDepth) %perform bit reduction ampValues = 2^(bitDepth - 1); for n = 1:length(x) y(n,1) = ceil(ampValues * x(n,1)) * (1/ampValues); end end
Full Wave Rectification Function function [y] = fullWaveRectification(x) %perform full-wave rectification for n = 1:length(x) if x(n,1) >= 0 y(n,1) = x(n,1); elseif x(n,1) < 0 y(n,1) = -1 * x(n,1); end end end
Hard Clipping/Arctangent Clipping Function In the MATLAB prototype the user can choose between hard clipping and arctangent clipping based on the argument βtypeβ for the function clipper(). This choice is later implemented as a button in the final C++/JUCE plugin. function [y] = clipper(x, type, upperClipLevel, lowerClipLevel, alpha) %perform hard or arctan soft clipping based on the users choice for 'type' if type == "hard" for n = 1:length(x) if x(n,1) > upperClipLevel y(n,1) = upperClipLevel; elseif x(n,1) <= -lowerClipLevel y(n,1) = -lowerClipLevel; else y(n,1) = x(n,1); end end elseif type == "soft" for n = 1:length(x) y(n,1) = (2/pi) * atan(alpha * x(n,1)); end end end
Series/Parallel Form %test sine Fs = 48000; T = 1/Fs; f = 200; t = (0:T:1).'; x = sin(2 * pi * f * t); level = 0.5; %output gain series_wet = 0.5;
parallel_wet = 1 - series_wet; hard_gain = 10; %SERIES DISTORTIONS %bit reduction bit_depth = 2; bit_out = bitCrusher(x, bit_depth); %full-wave rectification series_out = fullWaveRectification(bit_out); %PARALLEL DISTORTIONS parallel_out = clipper(hard_gain * x, "hard", 0.2, 0.2, 1); %OUTPUT y = level * (series_wet * series_out + parallel_wet * parallel_out);
The variables in the code that have to be changed manually in the MATLAB script are made user controllable via a JUCE plugin GUI in C++, these include:Β§ Β§ Β§ Β§ Β§ Β§ Β§ Β§
level series_wet/parallel_wet hard_gain bit_depth alpha upperClipLevel lowerClipLevel type
For a more detailed explanation of the transition made from hard coded values to user controllable parameters see below.
JUCE PLUGIN User controllable parameters and the distortion values they modify:Clipping Button Γ clipButton (Allows the user to switch between soft arctangent clipping and hard clipping) The button itself is implemented as type juce::TextButton. The virtual function void buttonClicked(juce::Button *button) of the inherited juce::Button::Listener class is overridden with a condition that checks whether the pointer passed into the function holds the address of the clipButton text button. If this condition is met another if statement checks whether the current value of the scoped enumeration clipState is currently equal to hard or soft. if (button == &clipButton) { if (clipState == ClipState::hard) { { clipButton.onClick = [this]() { soft();
}
};
}else if (clipState == ClipState::soft) { clipButton.onClick = [this]() { hard(); }; }
For each of the conditions a lambda function is assigned to the juce::Button class callback object onClick resulting in the lambda function being called when the button is clicked. In both cases the lambda captures a reference of the current object using the C++ keyword this. If clipstate = hard the body of the lambda function, clipButton.onClick calls the soft() method which sets the clipstate = soft, sets the value of bool mVal = 1 and changes the text and colour of the button to indicate that the user is in βsoft clippingβ mode. Conversely, if the clipState = soft, the body of the lambda function clipButton.onClick calls the hard() method and sets the clipState = hard, the value of mVal = 0 and changes the colour and text to indicate βhard clippingβ mode. The following sliders are broadly implemented in the same way, by way of inheriting the juce::Slider::Listener class and overriding the virtual function void sliderValueChanged (juce::Slider* slider). The body of the function is populated with a ladder of if statements that determine which slider has been changed, access the variable associated with the slider and set it equal to slider.getValue(). In the PluginEditor.cpp the sliders are styled, given labels and positioned. Output Gain Γ mLevel (Gives the user control over the output gain level with a range 0.0 β 1.0, a step size of 0.01 (to give a continuous feel) and an initial value of 0.5 (corresponding to -6dB). Bit Depth Rotary Slider Γ mBits (Allows the user to select a bit depth with a range of 2.0 β 8.0, a step size of 1.0 (to give a stepped feel) and an initial value of 0.5) float bitCrush{0.0}; //temp value for bit reduction current sample float ampValues = pow(2.0f, (mBits - 1.0f)); bitCrush = ceil(ampValues * ip) * (1.0f/ampValues);
Wet/Wet Mix Γ mWetWet (Dictates the contribution of series and parallel to the overall output. Range of 0.0 β 1.0 , a step size of 0.01 and an initial value of 0.5. Whereby, 0.0 is all parallel, 1.0 is all series and 0.5 is equal contribution from series and parallel.) channelDataL[n] = mLevel * (mWetWet * fullWaveRect + (1 - mWetWet) * parallel);
Upper Clip Level Γ mUpperClipLevel (Allows the user to select an upper clip level. Range of 0.2 β 0.8, step size 0.1, initial value 0.5.) Lower Clip Level Γ mLowerClipLevel (Allows the user to select a lower clip level. Range of 0.2 β 0.8, step size 0.1, initial value 0.5. This value is then made negative in the hard clipping algorithm in the ProcessBlock()) if (mHardGain > mUpperClipLevel) { parallel = mUpperClipLevel; }else if (mHardGain <= -mLowerClipLevel) { parallel = -mLowerClipLevel; }else { parallel = mHardGain; }
Hard Clipping Gain Γ mHardGainValue (Specifies the pre amplification going into the hard clipping distortion algorithm by multiplying the signal values by a scalar. Range of 1.0 β 6.0, step size 1.0, initial value 1.0 (which represents no pre amplification) the user can then add amplification as desired.)
mHardGain = mHardGainValue * ip;
Arctangent Alpha Γ mArctanLevel (Allows the user to choose the alpha value for the arctangent clipping algorithm and therefore dictate the shape and the intensity of the clipping. Range of 2.0 β 8.0, step size 1.0, initial value 5.0.) parallel = (2/M_PI) * atanf(mArctanLevel * ip);
TESTING METHODOLOGY In order to test that the output from the C++/JUCE plugin was yeilding the same results as the MATLAB prototype, the same 200Hz test sine generated in MATLAB and used in the testing of the prototype was written to a .wav file. The C++/JUCE plugin was then built as a VST3 and opened in Studio One DAW. Before connecting the distortions in series and parallel forms, the individual distortion algorithms for rectification, bit reduction, hard clipping and arctangent clipping were ported into C++/JUCE. For each distortion effect, the plugin was applied to the 200Hz test sine, before rendering the audio and finally reading the C++ render back into MATLAB and plotting the C++ render against the MATLAB render. Tests were performed in the time domain and frequency domain; this is to ensure that the distortions algorithms are not only shaping the waveform in the way that is expected but also that each distortion effect in the prototype and the final plugin are generating the same harmonic distortion for the same sinusoidal input.
PLOTS For the most part, the test plots below show a seamless relationship between the MATLAB and the C++ renders, with the waveforms and FFTs appearing to be identical and therefore plotted on top of each other. The only comparisons that did raise a point of interest is the C++ render and MATLAB render for bit reduction. In the waveform comparison (fig 27) it can be seen that the C++ render is larger in amplitude by 0.06. Because this is a marginal amount it could be assumed that this is a quirk of quantization in Studio One and the waveforms each have 16 possible amplitude values at which to represent the signal, which is what is expected of a πππ‘ ππππ‘β = 4. Another interesting occurrence is seen when comparing the C++ and MATLAB render FFTs (fig 28), where it appears that the quantization noise was removed during the C++ rendering process, this is not deliberate and it is unclear whether this is again, a quirk of the DAW used for testing, whether it applies dithering in the process. Encouragingly, looking closely at the harmonics they are almost identical to those of the MATLAB render (fig 29). Full Wave Rectification Testing Plots
Figure 20. Full Wave Rectification MATLAB vs C++ Render for Test Sine 200π»π§
Figure 21. C++ Render vs MATLAB Render Full-Wave Rectification FFT Arctangent Soft Clipping Testing Plots
Figure 22. Arctangent Clipping MATLAB vs C++ Render for Test Sine 200π»π§
Figure 23. C++ Render vs MATLAB Render Arctangent Clipping FFT Hard Clipping Testing Plots
Figure 24. Hard Clipping MATLAB vs C++ Render for Test Sine 200π»π§
Figure 25. C++ Render vs MATLAB Render Hard Clipping FFT
Figure 26. Hard Clipping C++ Render for Various Pre-Gain Values Bit Reduction Testing Plots
Figure 27. Bit Reduction MATLAB vs C++ Render for Test Sine 200π»π§
Figure 28. C++ Render vs MATLAB Render Bit Reduction FFT
Figure 29. C++ Render vs MATLAB Render Bit Reduction FFT β± = 600π»π§ Combined Output Testing Plots
Figure 30. Overall Distortion Output MATLAB vs C++ Render for Test Sine 200π»π§
FURTHER CONSIDERATIONS This project has provided a practical overview of non-linear processing and plugin development in C++/JUCE and has introduced the workflow of prototyping in MATLAB and realising a final product in JUCE/C++. There are several important considerations that have fallen outside of the scope of this project but are still interesting to consider for future work or as continuations of the current analysis. These are: Β§
Aliasing
Aliasing isnβt something that has been discussed within the body of this project simply because it either hasnβt been something that has caused concerns in terms of audible artifacts, or it has added to the overall distortion profile . However, aliasing is an important consideration to make when designing distortion algorithms as the generation of harmonic distortion can often surpass the Nyquist frequency and reflect back over Nyquist as audible artifacts. The common approach to avoid aliasing in distortion effects is oversampling. Β§
Gain Staging
For the purposes of this project gain staging has been explored for the clipping stage of the distortion plugin only. In further work gain staging could be beneficial at other points in the distortion algorithm i.e., between bit reduction and rectification. The input gain of a signal can change the effect of nonlinear processing considerably and increase the magnitudes of harmonic distortion, the creative effects of which have potential to be explored further. Β§
Total Harmonic Distortion
Some further evaluation this project could benefit from is exploration of Total Harmonic Distortion and accompanying THD plots to support the existing harmonic distortion analysis. Total Harmonic Distortion analyses the relationship between the amplitude of the fundamental frequency of a distorted
sinusoidal input and its subsequent harmonic distortion (usually the calculation is based on the first 5 harmonics after the fundamental) (Tarr, 2019). The biggest overall challenge of this project was adopting new workflows for prototyping and interpreting code between languages and frameworks. An additional challenge has been identifying the quirks of testing plugins in different DAWs and becoming familiar with plugin formats and compatibility conventions across DAWs and platforms.
REFERENCES Creasey, D., 2017. Audio Processes. 1st ed. New York: Routledge. Pohlmann, K. (2011) Principles of digital audio. 6th edn. New York: McGraw-Hill, p. 73. Reiss, J. and McPherson, A. (2015) Audio effects: theory, implementation and application. 1st edn. Hoboken: CRC Press, p. 173. Tarr, E., 2019. Hack audio. 1st ed. New York: Routledge, pp.147 - 181. ZΓΆlzer, U., 2011. DAFX. 2nd ed. Chichester: Wiley, p.131.
APPENDICIES
JUCE Plugin Code PluginEditor.h #pragma once #include <JuceHeader.h> #include "PluginProcessor.h" //======================================================================== ====== /** */ class DistLevelSliderAudioProcessorEditor : public juce::AudioProcessorEditor, public juce::Slider::Listener, public juce::Button::Listener { public: DistLevelSliderAudioProcessorEditor (DistLevelSliderAudioProcessor&); ~DistLevelSliderAudioProcessorEditor() override; //======================================================================== ====== void paint (juce::Graphics&) override; void resized() override; void sliderValueChanged (juce::Slider* slider) override; void buttonClicked (juce::Button *button) override; void hard();
void soft(); private: juce::Slider user juce::Slider juce::Slider level juce::Slider level juce::Slider clip level juce::Slider juce::Slider juce::Label juce::Label juce::Label juce::Label juce::Label juce::Label juce::Label
mGainSlider; //slider control for level controlled by mBitDepthSlider; //slider control for bit depth mLowerClipLevelSlider; //slider control for hard clip mUpperClipLevelSlider; //slider control for hard clip mArctanClipSlider; //slider control for arctangent soft mWetWetSlider; //slider for wet/wet control mHardGainSlider; //slider for hard clipping gain staging
mGainLabel; mBitDepthLabel; mLowerClipLevelLabel; mUpperClipLevelLabel; mArctanLevelLabel; mWetWetLabel; mHardClipGainLabel;
enum class ClipState { hard, soft }; ClipState clipState { ClipState::hard }; juce::TextButton clipButton { "Clipping Switch" }; // This reference is provided as a quick way for your editor to // access the processor object that created it. DistLevelSliderAudioProcessor& audioProcessor; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DistLevelSliderAudioProcessorEditor) };
PluginEditor.cpp #include "PluginProcessor.h" #include "PluginEditor.h" //======================================================================== ====== DistLevelSliderAudioProcessorEditor::DistLevelSliderAudioProcessorEditor (DistLevelSliderAudioProcessor& p) : AudioProcessorEditor (&p), audioProcessor (p) { clipButton.setToggleState(true, juce::NotificationType::dontSendNotification); clipButton.addListener(this); addAndMakeVisible(clipButton); mGainSlider.setSliderStyle(juce::Slider::SliderStyle::LinearHorizontal);
mGainSlider.setRange(0.0f, 1.0f, 0.01f); mGainSlider.setValue(0.5f); mGainSlider.setColour(juce::Slider::thumbColourId, juce::Colour(255,71,3)); mGainSlider.addListener(this); mGainSlider.setTextBoxStyle(juce::Slider::NoTextBox, 1, 0, 0); addAndMakeVisible(mGainLabel); mGainLabel.setText("Output Gain", juce::dontSendNotification); mGainLabel.attachToComponent(&mGainSlider, true); mGainLabel.setColour(juce::Label::textColourId, juce::Colour(255,71,3)); mBitDepthSlider.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontal Drag); mBitDepthSlider.setRange(2.0f, 8.0f, 1.0f); mBitDepthSlider.setValue(5.0f); mBitDepthSlider.setColour(juce::Slider::thumbColourId, juce::Colour(255,71,3)); mBitDepthSlider.addListener(this); mBitDepthSlider.setTextBoxStyle(juce::Slider::NoTextBox, 1, 0, 0); addAndMakeVisible(mBitDepthLabel); mBitDepthLabel.setText("Bit Depth", juce::dontSendNotification); mBitDepthLabel.attachToComponent(&mBitDepthSlider, true); mBitDepthLabel.setColour(juce::Label::textColourId, juce::Colour(255,71,3)); mUpperClipLevelSlider.setSliderStyle(juce::Slider::SliderStyle::RotaryHori zontalDrag); mUpperClipLevelSlider.setRange(0.2f, 0.8f, 0.1f); mUpperClipLevelSlider.setValue(0.5f); mUpperClipLevelSlider.setColour(juce::Slider::thumbColourId, juce::Colour(0, 150, 160)); mUpperClipLevelSlider.addListener(this); mUpperClipLevelSlider.setTextBoxStyle(juce::Slider::NoTextBox, 1, 0, 0); addAndMakeVisible(mUpperClipLevelLabel); mUpperClipLevelLabel.setText("Hard Clipping Upper Level", juce::dontSendNotification); mUpperClipLevelLabel.attachToComponent(&mUpperClipLevelSlider, true); mUpperClipLevelLabel.setColour(juce::Label::textColourId, juce::Colour(0, 150, 160)); mLowerClipLevelSlider.setSliderStyle(juce::Slider::SliderStyle::RotaryHori zontalDrag); mLowerClipLevelSlider.setRange(0.2f, 0.8f, 0.1f); mLowerClipLevelSlider.setValue(0.5f); mLowerClipLevelSlider.setColour(juce::Slider::thumbColourId, juce::Colour(0, 150, 160)); mLowerClipLevelSlider.addListener(this); mLowerClipLevelSlider.setTextBoxStyle(juce::Slider::NoTextBox, 1, 0, 0); addAndMakeVisible(mLowerClipLevelLabel);
mLowerClipLevelLabel.setText("Hard Clipping Lower Level", juce::dontSendNotification); mLowerClipLevelLabel.attachToComponent(&mLowerClipLevelSlider, true); mLowerClipLevelLabel.setColour(juce::Label::textColourId, juce::Colour(0, 150, 160)); mArctanClipSlider.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizont alDrag); mArctanClipSlider.setRange(1.0f, 9.0f, 1.0f); mArctanClipSlider.setValue(5.0f); mArctanClipSlider.setColour(juce::Slider::thumbColourId, juce::Colour(255,71,3)); mArctanClipSlider.addListener(this); mArctanClipSlider.setTextBoxStyle(juce::Slider::NoTextBox, 1, 0, 0); addAndMakeVisible(mArctanLevelLabel); mArctanLevelLabel.setText("Soft Clipping Level", juce::dontSendNotification); mArctanLevelLabel.attachToComponent(&mArctanClipSlider, true); mArctanLevelLabel.setColour(juce::Label::textColourId, juce::Colour(255,71,3)); mWetWetSlider.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalDr ag); mWetWetSlider.setRange(0.0f, 1.0f, 0.01f); mWetWetSlider.setValue(0.5f); mWetWetSlider.setColour(juce::Slider::thumbColourId, juce::Colour(255,71,3)); mWetWetSlider.addListener(this); mWetWetSlider.setTextBoxStyle(juce::Slider::NoTextBox, 1, 0, 0); addAndMakeVisible(mWetWetLabel); mWetWetLabel.setText("Distortion Mix", juce::dontSendNotification); mWetWetLabel.attachToComponent(&mWetWetSlider, true); mWetWetLabel.setColour(juce::Label::textColourId, juce::Colour(255,71,3)); mHardGainSlider.setSliderStyle(juce::Slider::SliderStyle::LinearHorizontal ); mHardGainSlider.setRange(1.0f, 6.0f, 1.0f); mHardGainSlider.setValue(1.0f); mHardGainSlider.setColour(juce::Slider::thumbColourId, juce::Colour(0, 150, 160)); mHardGainSlider.addListener(this); mHardGainSlider.setTextBoxStyle(juce::Slider::NoTextBox, 1, 0, 0); addAndMakeVisible(mHardClipGainLabel); mHardClipGainLabel.setText("Hard Clip Pre Gain", juce::dontSendNotification); mHardClipGainLabel.attachToComponent(&mHardGainSlider, true); mHardClipGainLabel.setColour(juce::Label::textColourId, juce::Colour(0, 150, 160)); addAndMakeVisible(mGainSlider); addAndMakeVisible(mBitDepthSlider); addAndMakeVisible(mUpperClipLevelSlider);
addAndMakeVisible(mLowerClipLevelSlider); addAndMakeVisible(mArctanClipSlider); addAndMakeVisible(mWetWetSlider); addAndMakeVisible(mHardGainSlider); setSize (500, 500); } DistLevelSliderAudioProcessorEditor::~DistLevelSliderAudioProcessorEditor( ) { } //======================================================================== ====== void DistLevelSliderAudioProcessorEditor::paint (juce::Graphics& g) { g.fillAll(juce::Colours::linen); } void DistLevelSliderAudioProcessorEditor::resized() { mGainSlider.setBounds(350, 75, 100, 100); mBitDepthSlider.setBounds(175, 0, 100, 250); mUpperClipLevelSlider.setBounds(175, 100, 100, 250); mLowerClipLevelSlider.setBounds(175, 200, 100, 250); mArctanClipSlider.setBounds(400, 300, 100, 250); mWetWetSlider.setBounds(175, 300, 100, 250); mHardGainSlider.setBounds(390, 180, 100, 100); clipButton.setBounds(300, 280, 150, 25); } void DistLevelSliderAudioProcessorEditor::buttonClicked (juce::Button *button) { if (button == &clipButton) { if (clipState == ClipState::hard) { clipButton.onClick = [this]() { soft(); }; }else if (clipState == ClipState::soft) { clipButton.onClick = [this]() { hard(); }; } } } void DistLevelSliderAudioProcessorEditor::hard() { clipState = ClipState::hard; audioProcessor.mVal = 0; clipButton.setButtonText("Hard Clip"); clipButton.setColour(juce::TextButton::ColourIds::buttonOnColourId, juce::Colour(0, 150, 160)); } void DistLevelSliderAudioProcessorEditor::soft() {
clipState = ClipState::soft; audioProcessor.mVal = 1; clipButton.setButtonText("Soft Clip"); clipButton.setColour(juce::TextButton::ColourIds::buttonOnColourId, juce::Colour(255,71,3)); } void DistLevelSliderAudioProcessorEditor::sliderValueChanged(juce::Slider *slider) { if (slider == &mGainSlider) { audioProcessor.mLevel = mGainSlider.getValue(); } if (slider == &mBitDepthSlider) { audioProcessor.mBits = mBitDepthSlider.getValue(); } if (slider == &mUpperClipLevelSlider) { audioProcessor.mUpperClipLevel = mUpperClipLevelSlider.getValue(); } if (slider == &mLowerClipLevelSlider) { audioProcessor.mLowerClipLevel = mLowerClipLevelSlider.getValue(); } if (slider == &mArctanClipSlider) { audioProcessor.mArctanLevel = mArctanClipSlider.getValue(); } if (slider == &mWetWetSlider) { audioProcessor.mWetWet = mWetWetSlider.getValue(); } if (slider == &mHardGainSlider) { audioProcessor.mHardGainValue = mHardGainSlider.getValue(); } }
PluginProcessor.h #pragma once #include <JuceHeader.h> //======================================================================== ====== class DistLevelSliderAudioProcessor { public:
: public juce::AudioProcessor
//======================================================================== ====== DistLevelSliderAudioProcessor(); ~DistLevelSliderAudioProcessor() override;
//======================================================================== ====== void prepareToPlay (double sampleRate, int samplesPerBlock) override; void releaseResources() override; #ifndef JucePlugin_PreferredChannelConfigurations bool isBusesLayoutSupported (const BusesLayout& layouts) const override; #endif void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override; //======================================================================== ====== juce::AudioProcessorEditor* createEditor() override; bool hasEditor() const override; //======================================================================== ====== const juce::String getName() const override; bool acceptsMidi() const override; bool producesMidi() const override; bool isMidiEffect() const override; double getTailLengthSeconds() const override; //======================================================================== ====== int getNumPrograms() override; int getCurrentProgram() override; void setCurrentProgram (int index) override; const juce::String getProgramName (int index) override; void changeProgramName (int index, const juce::String& newName) override; //======================================================================== ====== void getStateInformation (juce::MemoryBlock& destData) override; void setStateInformation (const void* data, int sizeInBytes) override; float mLevel{0.5f}; //level param tied to mGainSlider (controlled by user) float mBits{5.0f}; //Alpha value for bit depth (controlled by user) float mUpperClipLevel{0.2}; //Upper clip level for hard clipping (controlled by user) float mLowerClipLevel{0.2}; //Lower clip level for hard clipping (controlled by user) float mArctanLevel{5.0f}; //Clip level for hard clipping (controlled by user) float mWetWet; float mHardGain; float mHardGainValue{1.0f}; bool mVal{0};
private: //======================================================================== ====== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DistLevelSliderAudioProcessor) };
PluginProcessor.cpp #include "PluginProcessor.h" #include "PluginEditor.h" #include <math.h> #define PI 3.14 //======================================================================== ====== DistLevelSliderAudioProcessor::DistLevelSliderAudioProcessor() #ifndef JucePlugin_PreferredChannelConfigurations : AudioProcessor (BusesProperties() #if ! JucePlugin_IsMidiEffect #if ! JucePlugin_IsSynth .withInput ("Input", juce::AudioChannelSet::stereo(), true) #endif .withOutput ("Output", juce::AudioChannelSet::stereo(), true) #endif ) #endif { } DistLevelSliderAudioProcessor::~DistLevelSliderAudioProcessor() { } //======================================================================== ====== const juce::String DistLevelSliderAudioProcessor::getName() const { return JucePlugin_Name; } bool DistLevelSliderAudioProcessor::acceptsMidi() const { #if JucePlugin_WantsMidiInput return true; #else return false; #endif } bool DistLevelSliderAudioProcessor::producesMidi() const { #if JucePlugin_ProducesMidiOutput return true;
}
#else return false; #endif
bool DistLevelSliderAudioProcessor::isMidiEffect() const { #if JucePlugin_IsMidiEffect return true; #else return false; #endif } double DistLevelSliderAudioProcessor::getTailLengthSeconds() const { return 0.0; } int DistLevelSliderAudioProcessor::getNumPrograms() { return 1; // NB: some hosts don't cope very well if you tell them there are 0 programs, // so this should be at least 1, even if you're not really implementing programs. } int DistLevelSliderAudioProcessor::getCurrentProgram() { return 0; } void DistLevelSliderAudioProcessor::setCurrentProgram (int index) { } const juce::String DistLevelSliderAudioProcessor::getProgramName (int index) { return {}; } void DistLevelSliderAudioProcessor::changeProgramName (int index, const juce::String& newName) { } //======================================================================== ====== void DistLevelSliderAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) { // Use this method as the place to do any pre-playback // initialisation that you need.. } void DistLevelSliderAudioProcessor::releaseResources() {
any }
// When playback stops, you can use this as an opportunity to free up // spare memory, etc.
#ifndef JucePlugin_PreferredChannelConfigurations bool DistLevelSliderAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const { #if JucePlugin_IsMidiEffect juce::ignoreUnused (layouts); return true; #else // This is the place where you check if the layout is supported. // In this template code we only support mono or stereo. if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) return false; // This checks if the input layout matches the output layout #if ! JucePlugin_IsSynth if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) return false; #endif return true; #endif
} #endif
void DistLevelSliderAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) { //find the length of the buffer int numSamples = buffer.getNumSamples(); //check for samples if (numSamples == 0) { return; } //find number of output channels int numOutputs = getTotalNumOutputChannels(); //Find write (output) pointers auto channelDataL = buffer.getWritePointer(0); auto channelDataR = buffer.getWritePointer(0); if (numOutputs == 2) { channelDataR = buffer.getWritePointer(1); } //set up storage for input sample float ip = 0.0f; //---------------------------------------------
//BEGIN MAIN TIME LOOP for(int n = 0; n < numSamples; ++n) { //read input samples ip = buffer.getSample(0, n); //------------------------------------------------------------------------------------///Apply First Series Distortion(Bit Reduction) float bitCrush{0.0}; //temp value for bit reduction current sample float ampValues = pow(2.0f, (mBits - 1.0f)); bitCrush = ceil(ampValues * ip) * (1.0f/ampValues); ///Apply Second Series Distortion(Rectification) float fullWaveRect{0.0}; //temp value for bit reduction current sample if (bitCrush >= 0) { fullWaveRect = bitCrush; } else { fullWaveRect = -1 * bitCrush; } ///Apply Parallel Distortion (hard/arctan clipping) float parallel{0.0f}; if (mVal == 0) { mHardGain = mHardGainValue * ip; ///perform hard clipping if (mHardGain > mUpperClipLevel) { parallel = mUpperClipLevel; }else if (mHardGain <= -mLowerClipLevel) { parallel = -mLowerClipLevel; }else { parallel = mHardGain; } }else { ///perform soft arctangent clipping parallel = (2/M_PI) * atanf(mArctanLevel * ip); } //------------------------------------------------------------------------------------//declare output samples
channelDataL[n] = mLevel * (mWetWet * fullWaveRect + (1 - mWetWet) * parallel); //big mono if (numOutputs == 2) { channelDataR[n] = channelDataL[n]; } }
}
//======================================================================== ====== bool DistLevelSliderAudioProcessor::hasEditor() const { return true; // (change this to false if you choose to not supply an editor) } juce::AudioProcessorEditor* DistLevelSliderAudioProcessor::createEditor() { return new DistLevelSliderAudioProcessorEditor (*this); } //======================================================================== ====== void DistLevelSliderAudioProcessor::getStateInformation (juce::MemoryBlock& destData) { // You should use this method to store your parameters in the memory block. // You could do that either as raw data, or use the XML or ValueTree classes // as intermediaries to make it easy to save and load complex data. } void DistLevelSliderAudioProcessor::setStateInformation (const void* data, int sizeInBytes) { // You should use this method to restore your parameters from this memory block, // whose contents will have been created by the getStateInformation() call. } //======================================================================== ====== // This creates new instances of the plugin.. juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new DistLevelSliderAudioProcessor(); }
MATLAB Prototype Code Bit Reduction Function
function [y] = bitCrusher(x, bitDepth) %perform bit reduction ampValues = 2^(bitDepth - 1); for n = 1:length(x) y(n,1) = ceil(ampValues * x(n,1)) * (1/ampValues); end end
Full Wave Rectification Function function [y] = fullWaveRectification(x) %perform full-wave rectification for n = 1:length(x) if x(n,1) >= 0 y(n,1) = x(n,1); elseif x(n,1) < 0 y(n,1) = -1 * x(n,1); end end end
Hard Clipping/Arctangent Clipping Function function [y] = clipper(x, type, upperClipLevel, lowerClipLevel, alpha) %perform hard or arctan soft clipping based on the users choice for 'type' if type == "hard" for n = 1:length(x) if x(n,1) > upperClipLevel y(n,1) = upperClipLevel; elseif x(n,1) <= -lowerClipLevel y(n,1) = -lowerClipLevel; else y(n,1) = x(n,1); end end elseif type == "soft" for n = 1:length(x) y(n,1) = (2/pi) * atan(alpha * x(n,1)); end end end
Series and Parallel Form %test sine Fs = 48000; T = 1/Fs; f = 200; t = (0:T:1).'; x = sin(2 * pi * f * t); level = 0.5; %output gain series_wet = 0.5; parallel_wet = 1 - series_wet; hard_gain = 10; %SERIES DISTORTIONS %bit reduction bit_depth = 2;
bit_out = bitCrusher(x, bit_depth); %full-wave rectification series_out = fullWaveRectification(bit_out); %PARALLEL DISTORTIONS parallel_out = clipper(hard_gain * x, "hard", 0.2, 0.2, 1); %OUTPUT y = level * (series_wet * series_out + parallel_wet * parallel_out);
Test Script %test sine Fs = 48000; T = 1/Fs; f = 200; t = (0:T:1).'; x = sin(2 * pi * f * t); level = 0.5; hard_gain = 1; %% full-wave rectification rect_out = level * fullWaveRectification(x); [fullWaveRender, Fs] = audioread('rectRender.wav'); plot(rect_out) hold on plot(fullWaveRender(:,1)) grid on hold off title("Full Wave Rectification (MATLAB Render vs C++ Render)", "FontSize",14) legend("MATLAB Render", "C++ Render") xlabel("sample(n)") ylabel("amplitude") xlim([0 2400]) figure(2) plot(linspace(0,Fs,length(rect_out)), 20*log10(abs(fft(rect_out)))) hold on plot(linspace(0,Fs,length(fullWaveRender(:,1))), 20*log10(abs(fft(fullWaveRender(:,1))))) grid on title("C++ Render Vs MATLAB Render Full Wave Rectification", "FontSize",14) legend("MATLAB Render", "C++ Render") xlabel("frequency(Hz)") ylabel("magnitude") xlim([0 8000]) ylim([0 100]) %% arctangent soft clipping clip_out = level * clipper(x, "soft", 1, 1, 8); [arcClipRender, Fs] = audioread('arcClipRender.wav'); plot(clip_out)
hold on plot(arcClipRender(:,1)) grid on hold off title("Arctangent Soft Clipping \alpha = 8 (MATLAB Render vs C++ Render)", "FontSize",14) legend("MATLAB Render", "C++ Render") xlabel("sample(n)") ylabel("amplitude") xlim([0 2400]) figure(2) plot(linspace(0,Fs,length(arcClipRender(:,1))), 20*log10(abs(fft(arcClipRender(:,1))))) hold on plot(linspace(0,Fs,length(clip_out)), 20*log10(abs(fft(clip_out)))) grid on title("C++ Render Vs MATLAB Render Arctangent Soft Clipping \alpha = 8", "FontSize",14) legend("C++ Render", "MATLAB Render") xlabel("frequency(Hz)") ylabel("magnitude") xlim([0 8000]) ylim([0 100]) %% Bit Reduction bit_out = level * bitCrusher(x,4); [bitCrushRender, Fs] = audioread('bitCrushRender.wav'); plot(bit_out) hold on plot(bitCrushRender(:,1)) grid on hold off title("Bit Reduction (MATLAB Render vs C++ Render)", "FontSize",14) legend("MATLAB Render", "C++ Render (4bit)") xlabel("sample(n)") ylabel("amplitude") xlim([0 1200]) figure(2) plot(linspace(0,Fs,length(bit_out)), 20*log10(abs(fft(bit_out)))) hold on plot(linspace(0,Fs,length(bitCrushRender(:,1))), 20*log10(abs(fft(bitCrushRender(:,1))))) grid on title("MATLAB vs C++ Render Bit Reduction Bit Depth = 4", "FontSize",14) legend("MATLAB Render", "C++ Render") xlabel("frequency(Hz)") ylabel("magnitude") xlim([0 1800]) ylim([0 100]) %% hard clipping clip_out = level * clipper(hard_gain * x, "hard", 0.7, 0.7, 1); [hardClipRender, Fs] = audioread('hardClipRender.wav'); plot(clip_out) hold on
plot(hardClipRender(:,1)) hold off grid on title("MATLAB vs C++ Render Hard Clipping Clip Level 0.7", "FontSize",14) legend("MATLAB Render", "C++ Render") xlabel("sample(n)") ylabel("amplitude") xlim([0 2400]) figure(2) plot(linspace(0,Fs,length(hardClipRender(:,1))), 20*log10(abs(fft(hardClipRender(:,1))))) hold on plot(linspace(0,Fs,length(clip_out)), 20*log10(abs(fft(clip_out)))) grid on title("MATLAB vs C++ Render Hard Clipping Clip Level 0.7", "FontSize",14) legend("C++ Render", "MATLAB Render") xlabel("frequency(Hz)") ylabel("magnitude") xlim([0 2400]) ylim([0 100])