diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 009679533ae..7a6b266cf34 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -61,6 +61,7 @@ SET(LMMS_PLUGIN_LIST Sf2Player Sfxr Sid + SlewDistortion SlicerT SpectrumAnalyzer StereoEnhancer diff --git a/include/Draggable.h b/include/Draggable.h new file mode 100644 index 00000000000..d81f5dcf29c --- /dev/null +++ b/include/Draggable.h @@ -0,0 +1,63 @@ +/* + * Draggable.h + * + * Copyright (c) 2022 Lost Robot <r94231/at/gmail/dot/com> + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef DRAGGABLE_H +#define DRAGGABLE_H + +#include "FloatModelEditorBase.h" + +namespace lmms::gui +{ + +class LMMS_EXPORT Draggable : public FloatModelEditorBase +{ + Q_OBJECT + +public: + Draggable(FloatModelEditorBase::DirectionOfManipulation directionOfManipulation, + FloatModel* floatModel, const QPixmap &pixmap, int pointA, int pointB, QWidget* parent = nullptr); + + QSize sizeHint() const override; + void setPixmap(const QPixmap &pixmap); + void setDefaultValPixmap(const QPixmap &pixmap, float value = 0.f); + +protected: + void paintEvent(QPaintEvent *event) override; + void mouseMoveEvent(QMouseEvent *me) override; + +protected slots: + void handleMovement(); + +private: + QPixmap m_pixmap; + QPixmap m_defaultValPixmap; + float m_pointA; + float m_pointB; + float m_defaultValue; + bool m_hasDefaultValPixmap; +}; + +} // namespace lmms::gui + +#endif diff --git a/include/FloatModelEditorBase.h b/include/FloatModelEditorBase.h index 72f1450de5a..458459c9fdb 100644 --- a/include/FloatModelEditorBase.h +++ b/include/FloatModelEditorBase.h @@ -85,13 +85,6 @@ class LMMS_EXPORT FloatModelEditorBase : public QWidget, public FloatModelView void leaveEvent(QEvent *event) override; virtual float getValue(const QPoint & p); - -private slots: - virtual void enterValue(); - void friendlyUpdate(); - void toggleScale(); - -private: virtual QString displayValue() const; void doConnections() override; @@ -114,6 +107,11 @@ private slots: bool m_buttonPressed; DirectionOfManipulation m_directionOfManipulation; + +private slots: + virtual void enterValue(); + void friendlyUpdate(); + void toggleScale(); }; } // namespace lmms::gui diff --git a/include/OversamplingHelpers.h b/include/OversamplingHelpers.h new file mode 100755 index 00000000000..79fa99e195b --- /dev/null +++ b/include/OversamplingHelpers.h @@ -0,0 +1,226 @@ +/* + * OversamplingHelpers.h + * + * Copyright (c) 2023 Lost Robot <r94231/at/gmail/dot/com> + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef OVERSAMPLING_HELPERS_H +#define OVERSAMPLING_HELPERS_H + +#include <algorithm> +#include <array> + +#include "hiir/PolyphaseIir2Designer.h" +#include "hiir/Downsampler2xFpu.h" +#include "hiir/Upsampler2xFpu.h" + +constexpr float HIIR_DEFAULT_PASSBAND = 19600; +constexpr int HIIR_DEFAULT_MAX_COEFS = 8; + +namespace lmms +{ + +template<int MaxStages, int MaxCoefs = 8> +class Upsampler +{ +public: + void reset() + { + float bw = 0.5f - m_passband / m_sampleRate; + + // Stage 1 + double coefsFirst[MaxCoefs]; + hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsFirst, MaxCoefs, bw); + m_upsampleFirst.set_coefs(coefsFirst); + m_upsampleFirst.clear_buffers(); + bw = (bw + 0.5f) * 0.5f; + + // Stage 2 + constexpr int secondCoefCount = std::max(MaxCoefs / 2, 2); + double coefsSecond[secondCoefCount]; + hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsSecond, secondCoefCount, bw); + m_upsampleSecond.set_coefs(coefsSecond); + m_upsampleSecond.clear_buffers(); + bw = (bw + 0.5f) * 0.5f; + + // Remaining stages + constexpr int restCoefCount = std::max(MaxCoefs / 4, 2); + for (int i = 0; i < m_stages - 2; ++i) + { + double coefsRest[restCoefCount]; + hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsRest, restCoefCount, bw); + m_upsampleRest[i].set_coefs(coefsRest); + m_upsampleRest[i].clear_buffers(); + bw = (bw + 0.5f) * 0.5f; + } + } + + void setup(int stages, float sampleRate, float passband = HIIR_DEFAULT_PASSBAND) + { + assert(stages <= MaxStages); + m_stages = stages; + m_sampleRate = sampleRate; + m_passband = passband; + reset(); + } + + void process_sample(float* outSamples, float inSample) + { + int total = 1 << m_stages; + outSamples[0] = inSample; + int gap1 = total / 2; + + if (m_stages >= 1) + { + m_upsampleFirst.process_sample(outSamples[0], outSamples[gap1], outSamples[0]); + } + + if (m_stages >= 2) + { + int gap2 = gap1 / 2; + m_upsampleSecond.process_sample(outSamples[0], outSamples[gap2], outSamples[0]); + m_upsampleSecond.process_sample(outSamples[gap1], outSamples[gap1 + gap2], outSamples[gap1]); + } + + for (int i = 2; i < m_stages; ++i) + { + int count = 1 << i; + int gap = total / count; + for (int j = 0; j < count; ++j) + { + int temp = j * gap; + m_upsampleRest[i - 2].process_sample(outSamples[temp], outSamples[temp + gap / 2], outSamples[temp]); + } + } + } + + int getStages() const { return m_stages; } + float getSampleRate() const { return m_sampleRate; } + float getPassband() const { return m_passband; } + + void setStages(int stages) { m_stages = stages; reset(); } + void setSampleRate(float sampleRate) { m_sampleRate = sampleRate; reset(); } + void setPassband(float passband) { m_passband = passband; reset(); } + +private: + hiir::Upsampler2xFpu<MaxCoefs> m_upsampleFirst; + hiir::Upsampler2xFpu<std::max(MaxCoefs / 2, 2)> m_upsampleSecond; + std::array<hiir::Upsampler2xFpu<std::max(MaxCoefs / 4, 2)>, MaxStages - 2> m_upsampleRest; + + int m_stages = 0; + float m_sampleRate = 44100; + float m_passband = HIIR_DEFAULT_PASSBAND; +}; + + +template<int MaxStages, int MaxCoefs = 8> +class Downsampler +{ +public: + void reset() + { + float bw = 0.5f - m_passband / m_sampleRate; + + // Stage 1 + double coefsFirst[MaxCoefs]; + hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsFirst, MaxCoefs, bw); + m_downsampleFirst.set_coefs(coefsFirst); + m_downsampleFirst.clear_buffers(); + bw = (bw + 0.5f) * 0.5f; + + // Stage 2 + constexpr int secondCoefCount = std::max(MaxCoefs / 2, 2); + double coefsSecond[secondCoefCount]; + hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsSecond, secondCoefCount, bw); + m_downsampleSecond.set_coefs(coefsSecond); + m_downsampleSecond.clear_buffers(); + bw = (bw + 0.5f) * 0.5f; + + // Remaining stages + constexpr int restCoefCount = std::max(MaxCoefs / 4, 2); + for (int i = 0; i < m_stages - 2; ++i) + { + double coefsRest[restCoefCount]; + hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsRest, restCoefCount, bw); + m_downsampleRest[i].set_coefs(coefsRest); + m_downsampleRest[i].clear_buffers(); + bw = (bw + 0.5f) * 0.5f; + } + } + + void setup(int stages, float sampleRate, float passband = HIIR_DEFAULT_PASSBAND) + { + assert(stages <= MaxStages); + m_stages = stages; + m_sampleRate = sampleRate; + m_passband = passband; + reset(); + } + + float process_sample(float* inSamples) + { + for (int i = m_stages - 1; i >= 2; --i) + { + for (int j = 0; j < 1 << i; ++j) + { + inSamples[j] = m_downsampleRest[i - 2].process_sample(&inSamples[j * 2]); + } + } + + if (m_stages >= 2) + { + for (int j = 0; j < 2; ++j) + { + inSamples[j] = m_downsampleSecond.process_sample(&inSamples[j * 2]); + } + } + + if (m_stages >= 1) + { + inSamples[0] = m_downsampleFirst.process_sample(&inSamples[0]); + } + + return inSamples[0]; + } + + int getStages() const { return m_stages; } + float getSampleRate() const { return m_sampleRate; } + float getPassband() const { return m_passband; } + + void setStages(int stages) { m_stages = stages; reset(); } + void setSampleRate(float sampleRate) { m_sampleRate = sampleRate; reset(); } + void setPassband(float passband) { m_passband = passband; reset(); } + +private: + hiir::Downsampler2xFpu<MaxCoefs> m_downsampleFirst; + hiir::Downsampler2xFpu<std::max(MaxCoefs / 2, 2)> m_downsampleSecond; + std::array<hiir::Downsampler2xFpu<std::max(MaxCoefs / 4, 2)>, MaxStages - 2> m_downsampleRest; + + int m_stages = 0; + float m_sampleRate = 44100; + float m_passband = HIIR_DEFAULT_PASSBAND; +}; + + +} // namespace lmms + +#endif + diff --git a/include/lmms_math.h b/include/lmms_math.h index bdadd7ba0c4..d80248221ce 100644 --- a/include/lmms_math.h +++ b/include/lmms_math.h @@ -35,6 +35,10 @@ #include "lmms_constants.h" #include "lmmsconfig.h" +#ifdef __SSE2__ +#include <emmintrin.h> +#endif + namespace lmms { @@ -266,6 +270,53 @@ class LinearMap T m_b; }; +#ifdef __SSE2__ +// exp approximation for SSE2: https://stackoverflow.com/a/47025627/5759631 +// Maximum relative error of 1.72863156e-3 on [-87.33654, 88.72283] +static inline __m128 fast_exp_sse(__m128 x) +{ + __m128 f, p, r; + __m128i t, j; + const __m128 a = _mm_set1_ps (12102203.0f); /* (1 << 23) / log(2) */ + const __m128i m = _mm_set1_epi32 (0xff800000); /* mask for integer bits */ + const __m128 ttm23 = _mm_set1_ps (1.1920929e-7f); /* exp2(-23) */ + const __m128 c0 = _mm_set1_ps (0.3371894346f); + const __m128 c1 = _mm_set1_ps (0.657636276f); + const __m128 c2 = _mm_set1_ps (1.00172476f); + + t = _mm_cvtps_epi32 (_mm_mul_ps (a, x)); + j = _mm_and_si128 (t, m); /* j = (int)(floor (x/log(2))) << 23 */ + t = _mm_sub_epi32 (t, j); + f = _mm_mul_ps (ttm23, _mm_cvtepi32_ps (t)); /* f = (x/log(2)) - floor (x/log(2)) */ + p = c0; /* c0 */ + p = _mm_mul_ps (p, f); /* c0 * f */ + p = _mm_add_ps (p, c1); /* c0 * f + c1 */ + p = _mm_mul_ps (p, f); /* (c0 * f + c1) * f */ + p = _mm_add_ps (p, c2); /* p = (c0 * f + c1) * f + c2 ~= 2^f */ + r = _mm_castsi128_ps (_mm_add_epi32 (j, _mm_castps_si128 (p))); /* r = p * 2^i*/ + return r; +} + +// Lost Robot's SSE2 adaptation of Kari's vectorized log approximation: https://stackoverflow.com/a/65537754/5759631 +// Maximum relative error of 7.922410e-4 on [1.0279774e-38f, 3.4028235e+38f] +static inline __m128 fast_log_sse(__m128 a) { + __m128i aInt = _mm_castps_si128(a); + __m128i e = _mm_sub_epi32(aInt, _mm_set1_epi32(0x3f2aaaab)); + e = _mm_and_si128(e, _mm_set1_epi32(0xff800000)); + __m128i subtr = _mm_sub_epi32(aInt, e); + __m128 m = _mm_castsi128_ps(subtr); + __m128 i = _mm_mul_ps(_mm_cvtepi32_ps(e), _mm_set1_ps(1.19209290e-7f)); + __m128 f = _mm_sub_ps(m, _mm_set1_ps(1.0f)); + __m128 s = _mm_mul_ps(f, f); + __m128 r = _mm_add_ps(_mm_mul_ps(_mm_set1_ps(0.230836749f), f), _mm_set1_ps(-0.279208571f)); + __m128 t = _mm_add_ps(_mm_mul_ps(_mm_set1_ps(0.331826031f), f), _mm_set1_ps(-0.498910338f)); + r = _mm_add_ps(_mm_mul_ps(r, s), t); + r = _mm_add_ps(_mm_mul_ps(r, s), f); + r = _mm_add_ps(_mm_mul_ps(i, _mm_set1_ps(0.693147182f)), r); + return r; +} +#endif // __SSE2__ + } // namespace lmms #endif // LMMS_MATH_H diff --git a/plugins/SlewDistortion/CMakeLists.txt b/plugins/SlewDistortion/CMakeLists.txt new file mode 100755 index 00000000000..e5e2f695b6a --- /dev/null +++ b/plugins/SlewDistortion/CMakeLists.txt @@ -0,0 +1,4 @@ +INCLUDE(BuildPlugin) + +BUILD_PLUGIN(slewdistortion SlewDistortion.cpp SlewDistortionControls.cpp SlewDistortionControlDialog.cpp MOCFILES SlewDistortion.h SlewDistortionControls.h SlewDistortionControlDialog.h EMBEDDED_RESOURCES *.png) +TARGET_LINK_LIBRARIES(slewdistortion hiir) diff --git a/plugins/SlewDistortion/SlewDistortion.cpp b/plugins/SlewDistortion/SlewDistortion.cpp new file mode 100755 index 00000000000..ab49f4f60fd --- /dev/null +++ b/plugins/SlewDistortion/SlewDistortion.cpp @@ -0,0 +1,723 @@ +/* + * SlewDistortion.cpp + * + * Copyright (c) 2025 Lost Robot <r94231/at/gmail/dot/com> + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SlewDistortion.h" + +#include "embed.h" +#include "plugin_export.h" + +namespace lmms +{ + +extern "C" +{ +Plugin::Descriptor PLUGIN_EXPORT slewdistortion_plugin_descriptor = +{ + LMMS_STRINGIFY(PLUGIN_NAME), + "Slew Distortion", + QT_TRANSLATE_NOOP("PluginBrowser", "A 2-band distortion and slew rate limiter plugin."), + "Lost Robot <r94231/at/gmail/dot/com>", + 0x0100, + Plugin::Type::Effect, + new PluginPixmapLoader("logo"), + nullptr, + nullptr, +}; +} + + +SlewDistortion::SlewDistortion(Model* parent, const Descriptor::SubPluginFeatures::Key* key) : + Effect(&slewdistortion_plugin_descriptor, parent, key), + m_sampleRate(Engine::audioEngine()->outputSampleRate()), + m_lp(m_sampleRate), + m_hp(m_sampleRate), + m_slewdistortionControls(this) +{ + connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), this, SLOT(changeSampleRate())); + changeSampleRate(); +} + + +#ifdef __SSE2__ +Effect::ProcessStatus SlewDistortion::processImpl(SampleFrame* buf, const fpp_t frames) +{ + const float d = dryLevel(); + const float w = wetLevel(); + + const int oversampling = m_slewdistortionControls.m_oversamplingModel.value(); + const int oversampleVal = 1 << oversampling; + if (oversampleVal != m_oldOversampleVal) + { + m_oldOversampleVal = oversampleVal; + changeSampleRate(); + } + + const int distType1 = m_slewdistortionControls.m_distType1Model.value(); + const int distType2 = m_slewdistortionControls.m_distType2Model.value(); + const float drive1 = dbfsToAmp(m_slewdistortionControls.m_drive1Model.value()); + const float drive2 = dbfsToAmp(m_slewdistortionControls.m_drive2Model.value()); + const float slewUp1 = dbfsToAmp(m_slewdistortionControls.m_slewUp1Model.value()) / oversampleVal; + const float slewUp2 = dbfsToAmp(m_slewdistortionControls.m_slewUp2Model.value()) / oversampleVal; + const float slewDown1 = dbfsToAmp(m_slewdistortionControls.m_slewDown1Model.value()) / oversampleVal; + const float slewDown2 = dbfsToAmp(m_slewdistortionControls.m_slewDown2Model.value()) / oversampleVal; + const float bias1 = m_slewdistortionControls.m_bias1Model.value(); + const float bias2 = m_slewdistortionControls.m_bias2Model.value(); + const float warp1 = m_slewdistortionControls.m_warp1Model.value(); + const float warp2 = m_slewdistortionControls.m_warp2Model.value(); + const float crush1 = dbfsToAmp(m_slewdistortionControls.m_crush1Model.value()); + const float crush2 = dbfsToAmp(m_slewdistortionControls.m_crush2Model.value()); + const float attack1 = msToCoeff(m_slewdistortionControls.m_attack1Model.value()); + const float attack2 = msToCoeff(m_slewdistortionControls.m_attack2Model.value()); + const float attackInv1 = 1.f - attack1; + const float attackInv2 = 1.f - attack2; + const float release1 = msToCoeff(m_slewdistortionControls.m_release1Model.value()); + const float release2 = msToCoeff(m_slewdistortionControls.m_release2Model.value()); + const float releaseInv1 = 1.f - release1; + const float releaseInv2 = 1.f - release2; + const float dynamics1 = m_slewdistortionControls.m_dynamics1Model.value(); + const float dynamics2 = m_slewdistortionControls.m_dynamics2Model.value(); + const float dynamicSlew1 = m_slewdistortionControls.m_dynamicSlew1Model.value(); + const float dynamicSlew2 = m_slewdistortionControls.m_dynamicSlew2Model.value(); + const float outVol1 = dbfsToAmp(m_slewdistortionControls.m_outVol1Model.value()); + const float outVol2 = dbfsToAmp(m_slewdistortionControls.m_outVol2Model.value()); + const float split = m_slewdistortionControls.m_splitModel.value(); + const bool dcRemove = m_slewdistortionControls.m_dcRemoveModel.value(); + const bool multiband = m_slewdistortionControls.m_multibandModel.value(); + const float mix1 = m_slewdistortionControls.m_mix1Model.value(); + const float mix2 = m_slewdistortionControls.m_mix2Model.value(); + const bool slewLink1 = m_slewdistortionControls.m_slewLink1Model.value(); + const bool slewLink2 = m_slewdistortionControls.m_slewLink2Model.value(); + + const __m128 drive = _mm_set_ps(drive2, drive2, drive1, drive1); + const __m128 slewUp = _mm_set_ps(slewUp2, slewUp2, slewUp1, slewUp1); + const __m128 slewDown = _mm_set_ps(slewDown2, slewDown2, slewDown1, slewDown1); + const __m128 warp = _mm_set_ps(warp2, warp2, warp1, warp1); + const __m128 crush = _mm_set_ps(crush2, crush2, crush1, crush1); + const __m128 outVol = _mm_set_ps(outVol2, outVol2, outVol1, outVol1); + const __m128 attack = _mm_set_ps(attack2, attack2, attack1, attack1); + const __m128 attackInv = _mm_set_ps(attackInv2, attackInv2, attackInv1, attackInv1); + const __m128 release = _mm_set_ps(release2, release2, release1, release1); + const __m128 releaseInv = _mm_set_ps(releaseInv2, releaseInv2, releaseInv1, releaseInv1); + const __m128 dynamics = _mm_set_ps(dynamics2, dynamics2, dynamics1, dynamics1); + const __m128 dynamicSlew = _mm_set_ps(dynamicSlew2, dynamicSlew2, dynamicSlew1, dynamicSlew1); + const __m128 mix = _mm_set_ps(mix2, mix2, mix1, mix1); + const __m128 minFloor = _mm_set1_ps(SLEW_DISTORTION_MIN_FLOOR); + const int link1Mask = -static_cast<int>(slewLink1); + const int link2Mask = -static_cast<int>(slewLink2); + const __m128 slewLinkMask = _mm_castsi128_ps(_mm_set_epi32(link2Mask, link2Mask, link1Mask, link1Mask)); + + const __m128 zero = _mm_setzero_ps(); + const __m128 one = _mm_set1_ps(1.0f); + + if (m_slewdistortionControls.m_splitModel.isValueChanged()) + { + m_lp.setLowpass(split); + m_hp.setHighpass(split); + } + + for (fpp_t f = 0; f < frames; ++f) + { + // interpolate bias to remove crackling when moving the parameter + m_trueBias1 = m_biasInterpCoef * m_trueBias1 + (1.f - m_biasInterpCoef) * bias1; + m_trueBias2 = m_biasInterpCoef * m_trueBias2 + (1.f - m_biasInterpCoef) * bias2; + const __m128 bias = _mm_set_ps(m_trueBias2, m_trueBias2, m_trueBias1, m_trueBias1); + + if (oversampleVal > 1) + { + m_upsampler[0].process_sample(m_overOuts[0].data(), buf[f][0]); + m_upsampler[1].process_sample(m_overOuts[1].data(), buf[f][1]); + } + else + { + m_overOuts[0][0] = buf[f][0]; + m_overOuts[1][0] = buf[f][1]; + } + + for (int overSamp = 0; overSamp < oversampleVal; ++overSamp) + { + alignas(16) std::array<float, 4> inArr = {0}; + if (multiband) + { + inArr[0] = m_hp.update(m_overOuts[0][overSamp], 0); + inArr[1] = m_hp.update(m_overOuts[1][overSamp], 1); + inArr[2] = m_lp.update(m_overOuts[0][overSamp], 0); + inArr[3] = m_lp.update(m_overOuts[1][overSamp], 1); + } + else + { + inArr[0] = m_overOuts[0][overSamp]; + inArr[1] = m_overOuts[1][overSamp]; + inArr[2] = 0; + inArr[3] = 0; + } + + __m128 in = _mm_load_ps(&inArr[0]); + __m128 absIn = _mm_and_ps(in, _mm_castsi128_ps(_mm_set1_epi32(0x7FFFFFFF))); + + // store volume for display + _mm_store_ps(&m_inPeakDisplay[0], _mm_max_ps(_mm_load_ps(&m_inPeakDisplay[0]), _mm_mul_ps(absIn, drive))); + + __m128 inEnv = _mm_load_ps(&this->m_inEnv[0]); + __m128 slewOut = _mm_load_ps(&this->m_slewOut[0]); + + // apply attack and release to envelope follower + __m128 cmp = _mm_cmpgt_ps(absIn, inEnv); + __m128 envRise = _mm_add_ps(_mm_mul_ps(inEnv, attack), _mm_mul_ps(absIn, attackInv)); + __m128 envFall = _mm_add_ps(_mm_mul_ps(inEnv, release), _mm_mul_ps(absIn, releaseInv)); + inEnv = _mm_or_ps(_mm_and_ps(cmp, envRise), _mm_andnot_ps(cmp, envFall)); + inEnv = _mm_max_ps(inEnv, minFloor); + + // this is the input signal's slew rate + __m128 rate = _mm_sub_ps(in, slewOut); + + __m128 scaledLog = _mm_mul_ps(dynamicSlew, fast_log_sse(inEnv)); + // clamp to [-87.0f, 87.0f] since fast_exp_sse breaks outside of those bounds + __m128 clampedScaledLog = _mm_max_ps(_mm_min_ps(scaledLog, _mm_set1_ps(87.0f)), _mm_set1_ps(-87.0f)); + __m128 slewMult = fast_exp_sse(clampedScaledLog); + + // determine whether we should use the slew up or slew down parameter + __m128 finalMask = _mm_or_ps(_mm_cmpge_ps(rate, zero), slewLinkMask); + __m128 finalSlew = _mm_or_ps(_mm_and_ps(finalMask, _mm_mul_ps(slewUp, slewMult)), + _mm_andnot_ps(finalMask, _mm_mul_ps(slewDown, slewMult))); + + __m128 clampedRate = _mm_max_ps(_mm_sub_ps(zero, finalSlew), _mm_min_ps(rate, finalSlew)); + slewOut = _mm_add_ps(slewOut, clampedRate); + + // apply drive and bias + __m128 biasedIn = _mm_add_ps(_mm_mul_ps(slewOut, drive), bias); + + // apply warp and crush + // distIn = (biasedIn - copysign(warp[i] / crush[i], biasedIn)) / (1.f - warp[i]); + __m128 signBiasedIn = _mm_and_ps(biasedIn, _mm_castsi128_ps(_mm_set1_epi32(0x80000000))); + __m128 warpOverCrush = _mm_div_ps(warp, crush); + __m128 copysignWarpOverCrush = _mm_or_ps(warpOverCrush, signBiasedIn); + __m128 distIn = _mm_div_ps(_mm_sub_ps(biasedIn, copysignWarpOverCrush), _mm_sub_ps(one, warp)); + + alignas(16) std::array<float, 4> distInArr; + _mm_store_ps(&distInArr[0], distIn); + alignas(16) std::array<float, 4> distOutArr; + + // if both bands have the same distortion type, we can process all four channels simultaneously + // otherwise we have to do two at a time + int loopCount = (distType1 == distType2 || !multiband) ? 1 : 2; + + for (int pair = 0; pair < loopCount; ++pair) + { + int currentDistType = (pair == 0) ? distType1 : distType2; + + __m128 distInFull = _mm_load_ps(&distInArr[0]); + __m128 distOutFull; + + // switch-case applies the distortion to the full set of 4 values + switch (currentDistType) + { + case 0:// Hard Clip => clamp(x, -1, 1) + { + __m128 minVal = _mm_set1_ps(-1.0f); + __m128 maxVal = one; + distOutFull = _mm_max_ps(_mm_min_ps(distInFull, maxVal), minVal); + break; + } + case 1: // Tanh => 2 / (1 + exp(-2x)) - 1 + { + // clamp to [-87.0f, 87.0f] since fast_exp_sse breaks outside of those bounds + __m128 clampedInput = _mm_max_ps(_mm_min_ps(_mm_mul_ps(_mm_set1_ps(-2.0f), + distInFull), _mm_set1_ps(87.0f)), _mm_set1_ps(-87.0f)); + __m128 expResult = fast_exp_sse(clampedInput); + distOutFull = _mm_sub_ps(_mm_div_ps(_mm_set1_ps(2.0f), _mm_add_ps(one, expResult)), one); + break; + } + case 2: // Fast Soft Clip 1 => x / (1 + x^2 / 4) + { + __m128 temp = _mm_max_ps(_mm_min_ps(distInFull, _mm_set1_ps(2.f)), _mm_set1_ps(-2.f));// clamp + distOutFull = _mm_div_ps(temp, _mm_add_ps(one, + _mm_mul_ps(_mm_set1_ps(0.25f), _mm_mul_ps(temp, temp)))); + break; + } + case 3: // Fast Soft Clip 2 => x - (4/27) * x^3 + { + __m128 temp = _mm_max_ps(_mm_min_ps(distInFull, _mm_set1_ps(1.5f)), _mm_set1_ps(-1.5f));// clamp + distOutFull = _mm_sub_ps(temp, _mm_mul_ps(_mm_set1_ps(4.f / 27.f), + _mm_mul_ps(_mm_mul_ps(temp, temp), temp))); + break; + } + case 4: // Sinusoidal => sin(x) + { + // SSE2 sine approximation I created + __m128 pi = _mm_set1_ps(3.14159265358979323846f); + __m128 piOverTwo = _mm_set1_ps(1.57079632679489661923f); + __m128 tau = _mm_set1_ps(6.28318530717958647692f); + + __m128 distMinusPiOverTwo = _mm_sub_ps(distInFull, piOverTwo); + __m128 divByTwoPi = _mm_div_ps(distMinusPiOverTwo, tau); + + // SSE2 floor replacement + __m128 trunc = _mm_cvtepi32_ps(_mm_cvttps_epi32(divByTwoPi)); + __m128 floorDivByTwoPi = _mm_sub_ps(trunc, _mm_and_ps(_mm_cmplt_ps(divByTwoPi, trunc), one)); + + // x mod 2pi = x - floor(x / 2pi) * 2pi + __m128 floorMulTwoPi = _mm_mul_ps(floorDivByTwoPi, tau); + __m128 modInput = _mm_sub_ps(distMinusPiOverTwo, floorMulTwoPi); + + // abs(in - pi) - pi/2 + __m128 x = _mm_sub_ps(_mm_andnot_ps(_mm_set1_ps(-0.0f), _mm_sub_ps(modInput, pi)), piOverTwo); + + // polynomial sine approximation + // sin(x) ≈ x - x^3 / 6 + x^5 / 120 + __m128 x2 = _mm_mul_ps(x, x); + __m128 x3 = _mm_mul_ps(x2, x); + __m128 x5 = _mm_mul_ps(x3, x2); + __m128 sinApprox = _mm_sub_ps(x, _mm_mul_ps(x3, _mm_set1_ps(1.0f / 6.0f))); + distOutFull = _mm_add_ps(sinApprox, _mm_mul_ps(x5, _mm_set1_ps(1.0f / 120.0f))); + break; + } + case 5: // Foldback => |(|x - 1| mod 4) - 2| - 1 = |2 - |(x - 1) - 4 * floor((x - 1) / 4)|| - 1 + { + __m128 four = _mm_set1_ps(4.0f); + __m128 distInMinusOne = _mm_sub_ps(distInFull, one); + __m128 divByFour = _mm_div_ps(distInMinusOne, four); + + // floor + __m128 trunc = _mm_cvtepi32_ps(_mm_cvttps_epi32(divByFour)); + __m128 correction = _mm_and_ps(_mm_cmplt_ps(divByFour, trunc), one); + __m128 floorOverFour = _mm_sub_ps(trunc, correction); + + distOutFull = _mm_sub_ps(_mm_andnot_ps(_mm_set1_ps(-0.0f), _mm_sub_ps(_mm_sub_ps( + distInMinusOne, _mm_mul_ps(floorOverFour, four)), _mm_set1_ps(2.0f))), one); + break; + } + case 6: // Full-wave Rectify => |x| + { + distOutFull = _mm_and_ps(distInFull, _mm_castsi128_ps(_mm_set1_epi32(0x7FFFFFFF))); + break; + } + case 7: // Smooth Rectify => sqrt(x^2 + 0.04) - 0.2 + { + distOutFull = _mm_sub_ps(_mm_sqrt_ps(_mm_add_ps(_mm_mul_ps(distInFull, distInFull), + _mm_set1_ps(0.04f))),_mm_set1_ps(0.2f)); + break; + } + case 8: // Half-wave Rectify => max(0, x) + { + distOutFull = _mm_max_ps(_mm_setzero_ps(), distInFull); + break; + } + case 9: // Bitcrush => round(x / drive * scale) / scale + { + // scale = 16 / drive + __m128 scale = _mm_div_ps(_mm_set1_ps(16.f), drive); + __m128 scaledVal = _mm_mul_ps(_mm_div_ps(distInFull, drive), scale); + + // round to nearest integer + __m128 signMask = _mm_cmplt_ps(scaledVal, zero); + __m128 half = _mm_set1_ps(0.5f); + __m128 addVal = _mm_or_ps(_mm_andnot_ps(signMask, half), _mm_and_ps(signMask, _mm_sub_ps(zero, half))); + __m128 rounded = _mm_cvtepi32_ps(_mm_cvttps_epi32(_mm_add_ps(scaledVal, addVal))); + + distOutFull = _mm_div_ps(rounded, scale); + break; + } + default: + { + distOutFull = distInFull; + break; + } + } + + if (loopCount == 1)// we can store all four simultaneously + { + _mm_store_ps(&distOutArr[0], distOutFull); + break; + } + else// need to store two at a time + { + if (pair == 0) + { + // for elements 0 and 1 + _mm_storel_pi((__m64*)&distOutArr[0], distOutFull); + } + else + { + // for elements 2 and 3 + _mm_storeh_pi((__m64*)&distOutArr[2], distOutFull); + } + } + } + + __m128 distOut = _mm_load_ps(&distOutArr[0]); + + // (1 - warp) * distOut + copysign(warp, biasedIn) + __m128 distOutScaled = _mm_add_ps(_mm_mul_ps(distOut, _mm_sub_ps(one, warp)), _mm_or_ps(warp, signBiasedIn)); + + // if (abs(biasedIn) < warp / crush) {distOut = biasedIn * crush;} + __m128 absBiasedIn = _mm_and_ps(biasedIn, _mm_castsi128_ps(_mm_set1_epi32(0x7FFFFFFF))); + __m128 condition = _mm_cmplt_ps(absBiasedIn, _mm_div_ps(warp, crush)); + __m128 biasedInCrush = _mm_mul_ps(biasedIn, crush); + + distOut = _mm_or_ps(_mm_and_ps(condition, biasedInCrush), _mm_andnot_ps(condition, distOutScaled)); + + // DC offset calculation + __m128 dcOffset = _mm_load_ps(&this->m_dcOffset[0]); + __m128 dcCoeff = _mm_set1_ps(m_dcCoeff); + dcOffset = _mm_add_ps(_mm_mul_ps(dcOffset, dcCoeff), _mm_mul_ps(distOut, _mm_sub_ps(one, dcCoeff))); + + __m128 distOutMinusDC = _mm_sub_ps(distOut, dcOffset); + + // even with DC offset removal disabled, we should still apply it for the envelope follower + __m128 outEnv = _mm_load_ps(&this->m_outEnv[0]); + __m128 absOut = _mm_and_ps(distOutMinusDC, _mm_castsi128_ps(_mm_set1_epi32(0x7FFFFFFF))); + + cmp = _mm_cmpgt_ps(absOut, outEnv); + __m128 outEnvRise = _mm_add_ps(_mm_mul_ps(outEnv, attack), _mm_mul_ps(absOut, attackInv)); + __m128 outEnvFall = _mm_add_ps(_mm_mul_ps(outEnv, release), _mm_mul_ps(absOut, releaseInv)); + outEnv = _mm_max_ps(_mm_or_ps(_mm_and_ps(cmp, outEnvRise), _mm_andnot_ps(cmp, outEnvFall)), minFloor); + + // remove DC + __m128 finalDistOut = (dcRemove) ? distOutMinusDC : distOut; + + // crossfade between a multiplier of 1 and (inEnv/outEnv) for dynamics feature + __m128 distDyn = _mm_mul_ps(finalDistOut, _mm_add_ps(one, + _mm_mul_ps(_mm_sub_ps(_mm_div_ps(inEnv, outEnv), one), dynamics))); + + // apply mix + __m128 outFinal = _mm_mul_ps(_mm_add_ps(in, _mm_mul_ps(mix, _mm_sub_ps(distDyn, in))), outVol); + + // store volume for display + __m128 outAbs = _mm_and_ps(outFinal, _mm_castsi128_ps(_mm_set1_epi32(0x7FFFFFFF))); + _mm_store_ps(&m_outPeakDisplay[0], _mm_max_ps(_mm_load_ps(&m_outPeakDisplay[0]), outAbs)); + + // write updated stuff back into member variables + _mm_store_ps(&this->m_inEnv[0], inEnv); + _mm_store_ps(&this->m_slewOut[0], slewOut); + _mm_store_ps(&this->m_dcOffset[0], dcOffset); + _mm_store_ps(&this->m_outEnv[0], outEnv); + + alignas(16) std::array<float, 4> outArr; + _mm_store_ps(&outArr[0], outFinal); + + m_overOuts[0][overSamp] = outArr[0] + outArr[2]; + m_overOuts[1][overSamp] = outArr[1] + outArr[3]; + } + + std::array<float, 2> s; + if (oversampleVal > 1) + { + s[0] = m_downsampler[0].process_sample(m_overOuts[0].data()); + s[1] = m_downsampler[1].process_sample(m_overOuts[1].data()); + } + else + { + s[0] = m_overOuts[0][0]; + s[1] = m_overOuts[1][0]; + } + + buf[f][0] = d * buf[f][0] + w * s[0]; + buf[f][1] = d * buf[f][1] + w * s[1]; + } + + return ProcessStatus::ContinueIfNotQuiet; +} + + +#else +Effect::ProcessStatus SlewDistortion::processImpl(SampleFrame* buf, const fpp_t frames) +{ + const float d = dryLevel(); + const float w = wetLevel(); + + const int oversampling = m_slewdistortionControls.m_oversamplingModel.value(); + const int oversampleVal = 1 << oversampling; + if (oversampleVal != m_oldOversampleVal) + { + m_oldOversampleVal = oversampleVal; + changeSampleRate(); + } + + const int distType1 = m_slewdistortionControls.m_distType1Model.value(); + const int distType2 = m_slewdistortionControls.m_distType2Model.value(); + const float drive1 = dbfsToAmp(m_slewdistortionControls.m_drive1Model.value()); + const float drive2 = dbfsToAmp(m_slewdistortionControls.m_drive2Model.value()); + const float slewUp1 = dbfsToAmp(m_slewdistortionControls.m_slewUp1Model.value()) / oversampleVal; + const float slewUp2 = dbfsToAmp(m_slewdistortionControls.m_slewUp2Model.value()) / oversampleVal; + const float slewDown1 = dbfsToAmp(m_slewdistortionControls.m_slewDown1Model.value()) / oversampleVal; + const float slewDown2 = dbfsToAmp(m_slewdistortionControls.m_slewDown2Model.value()) / oversampleVal; + const float bias1 = m_slewdistortionControls.m_bias1Model.value(); + const float bias2 = m_slewdistortionControls.m_bias2Model.value(); + const float warp1 = m_slewdistortionControls.m_warp1Model.value(); + const float warp2 = m_slewdistortionControls.m_warp2Model.value(); + const float crush1 = dbfsToAmp(m_slewdistortionControls.m_crush1Model.value()); + const float crush2 = dbfsToAmp(m_slewdistortionControls.m_crush2Model.value()); + const float attack1 = msToCoeff(m_slewdistortionControls.m_attack1Model.value()); + const float attack2 = msToCoeff(m_slewdistortionControls.m_attack2Model.value()); + const float attackInv1 = 1.f - attack1; + const float attackInv2 = 1.f - attack2; + const float release1 = msToCoeff(m_slewdistortionControls.m_release1Model.value()); + const float release2 = msToCoeff(m_slewdistortionControls.m_release2Model.value()); + const float releaseInv1 = 1.f - release1; + const float releaseInv2 = 1.f - release2; + const float dynamics1 = m_slewdistortionControls.m_dynamics1Model.value(); + const float dynamics2 = m_slewdistortionControls.m_dynamics2Model.value(); + const float dynamicSlew1 = m_slewdistortionControls.m_dynamicSlew1Model.value(); + const float dynamicSlew2 = m_slewdistortionControls.m_dynamicSlew2Model.value(); + const float outVol1 = dbfsToAmp(m_slewdistortionControls.m_outVol1Model.value()); + const float outVol2 = dbfsToAmp(m_slewdistortionControls.m_outVol2Model.value()); + const float split = m_slewdistortionControls.m_splitModel.value(); + const bool dcRemove = m_slewdistortionControls.m_dcRemoveModel.value(); + const bool multiband = m_slewdistortionControls.m_multibandModel.value(); + const float mix1 = m_slewdistortionControls.m_mix1Model.value(); + const float mix2 = m_slewdistortionControls.m_mix2Model.value(); + const bool slewLink1 = m_slewdistortionControls.m_slewLink1Model.value(); + const bool slewLink2 = m_slewdistortionControls.m_slewLink2Model.value(); + + std::array<float, 4> in = {0}; + std::array<float, 4> out = {0}; + const std::array<float, 4> drive = {drive1, drive1, drive2, drive2}; + const std::array<float, 4> slewUp = {slewUp1, slewUp1, slewUp2, slewUp2}; + const std::array<float, 4> slewDown = {slewDown1, slewDown1, slewDown2, slewDown2}; + const std::array<int, 4> distType = {distType1, distType1, distType2, distType2}; + const std::array<float, 4> warp = {warp1, warp1, warp2, warp2}; + const std::array<float, 4> crush = {crush1, crush1, crush2, crush2}; + const std::array<float, 4> outVol = {outVol1, outVol1, outVol2, outVol2}; + const std::array<float, 4> attack = {attack1, attack1, attack2, attack2}; + const std::array<float, 4> attackInv = {attackInv1, attackInv1, attackInv2, attackInv2}; + const std::array<float, 4> release = {release1, release1, release2, release2}; + const std::array<float, 4> releaseInv = {releaseInv1, releaseInv1, releaseInv2, releaseInv2}; + const std::array<float, 4> dynamics = {dynamics1, dynamics1, dynamics2, dynamics2}; + const std::array<float, 4> dynamicSlew = {dynamicSlew1, dynamicSlew1, dynamicSlew2, dynamicSlew2}; + const std::array<float, 4> mix = {mix1, mix1, mix2, mix2}; + const std::array<bool, 4> slewLink = {slewLink1, slewLink1, slewLink2, slewLink2}; + + if (m_slewdistortionControls.m_splitModel.isValueChanged()) + { + m_lp.setLowpass(split); + m_hp.setHighpass(split); + } + + for (fpp_t f = 0; f < frames; ++f) + { + // interpolate bias to remove crackling when moving the parameter + m_trueBias1 = m_biasInterpCoef * m_trueBias1 + (1.f - m_biasInterpCoef) * bias1; + m_trueBias2 = m_biasInterpCoef * m_trueBias2 + (1.f - m_biasInterpCoef) * bias2; + const std::array<float, 4> bias = {m_trueBias1, m_trueBias1, m_trueBias2, m_trueBias2}; + + if (oversampleVal > 1) + { + m_upsampler[0].process_sample(m_overOuts[0].data(), buf[f][0]); + m_upsampler[1].process_sample(m_overOuts[1].data(), buf[f][1]); + } + else + { + m_overOuts[0][0] = buf[f][0]; + m_overOuts[1][0] = buf[f][1]; + } + + for (int overSamp = 0; overSamp < oversampleVal; ++overSamp) + { + if (multiband) + { + in[0] = m_hp.update(m_overOuts[0][overSamp], 0); + in[1] = m_hp.update(m_overOuts[1][overSamp], 1); + in[2] = m_lp.update(m_overOuts[0][overSamp], 0); + in[3] = m_lp.update(m_overOuts[1][overSamp], 1); + } + else + { + in[0] = m_overOuts[0][overSamp]; + in[1] = m_overOuts[1][overSamp]; + in[2] = 0; + in[3] = 0; + } + + m_inPeakDisplay[0] = std::max(m_inPeakDisplay[0], std::abs(in[0] * drive[0])); + m_inPeakDisplay[1] = std::max(m_inPeakDisplay[1], std::abs(in[1] * drive[1])); + m_inPeakDisplay[2] = std::max(m_inPeakDisplay[2], std::abs(in[2] * drive[2])); + m_inPeakDisplay[3] = std::max(m_inPeakDisplay[3], std::abs(in[3] * drive[3])); + + for (int i = 0; i < 4 - !multiband * 2; ++i) { + const float absIn = std::abs(in[i]); + m_inEnv[i] = absIn > m_inEnv[i] ? m_inEnv[i] * attack[i] + absIn * attackInv[i] : m_inEnv[i] * release[i] + absIn * releaseInv[i]; + m_inEnv[i] = std::max(m_inEnv[i], SLEW_DISTORTION_MIN_FLOOR); + + float rate = in[i] - m_slewOut[i]; + float slewMult = dynamicSlew[i] ? std::pow(m_inEnv[i], dynamicSlew[i]) : 1.f; + const float trueSlew = ((rate >= 0 || slewLink[i]) ? slewUp[i] : slewDown[i]) * slewMult; + rate = std::clamp(rate, -trueSlew, trueSlew); + m_slewOut[i] = m_slewOut[i] + rate; + + float biasedIn = m_slewOut[i] * drive[i] + bias[i]; + float distIn = (biasedIn - copysign(warp[i] / crush[i], biasedIn)) / (1.f - warp[i]); + float distOut; + switch(distType[i]) { + case 0: {// hard clip + distOut = std::clamp(distIn, -1.f, 1.f); + break; + } + case 1: {// tanh + distOut = 2.f / (1.f + std::exp(-2.f * distIn)) - 1; + break; + } + case 2: {// fast soft clip 1 + const float temp = std::clamp(distIn, -2.f, 2.f); + distOut = temp / (1 + 0.25f * temp * temp); + break; + } + case 3: {// fast soft clip 2 + const float temp = std::clamp(distIn, -1.5f, 1.5f); + distOut = temp - (4.f / 27.f) * temp * temp * temp; + break; + } + case 4: { // sinusodal + // using a polynomial approximation so it matches with the SSE2 code + // x - x^3 / 6 + x^5 / 120 + float modInput = std::fmod(distIn - F_PI * 0.5f, 2.f * F_PI); + if (modInput < 0) {modInput += 2.f * F_PI;} + const float x = std::abs(modInput - F_PI) - F_PI * 0.5f; + const float x2 = x * x; + const float x3 = x2 * x; + const float x5 = x3 * x2; + distOut = x - (x3 / 6.0f) + (x5 / 120.0f); + break; + } + case 5: {// foldback distortion + distOut = std::abs(std::abs(std::fmod(distIn - 1.f, 4.f)) - 2.f) - 1.f; + break; + } + case 6: {// rectify + distOut = std::abs(distIn); + break; + } + case 7: // smooth rectify + { + distOut = std::sqrt(distIn * distIn + 0.04f) - 0.2f; + break; + } + case 8: // half-wave rectify + { + distOut = std::max(0.0f, distIn); + break; + } + case 9: // bitcrush + { + const float scale = 16 / drive[i]; + distOut = std::round(distIn / drive[i] * scale) / scale; + break; + } + default: + { + distOut = distIn; + } + } + distOut = distOut * (1.f - warp[i]) + copysign(warp[i], biasedIn); + if (std::abs(biasedIn) < warp[i] / crush[i]) {distOut = biasedIn * crush[i];} + + m_dcOffset[i] = m_dcOffset[i] * m_dcCoeff + distOut * (1.f - m_dcCoeff); + + // even with DC offset removal disabled, we should still apply it for the envelope follower + const float absOut = std::abs(distOut - m_dcOffset[i]); + m_outEnv[i] = absOut > m_outEnv[i] ? m_outEnv[i] * attack[i] + absOut * attackInv[i] : m_outEnv[i] * release[i] + absOut * releaseInv[i]; + m_outEnv[i] = std::max(m_outEnv[i], SLEW_DISTORTION_MIN_FLOOR); + + if (dcRemove) { distOut -= m_dcOffset[i]; } + + distOut *= linearInterpolate(1.f, m_inEnv[i] / m_outEnv[i], dynamics[i]); + + out[i] = linearInterpolate(in[i], distOut, mix[i]) * outVol[i]; + } + + m_outPeakDisplay[0] = std::max(m_outPeakDisplay[0], std::abs(out[0])); + m_outPeakDisplay[1] = std::max(m_outPeakDisplay[1], std::abs(out[1])); + m_outPeakDisplay[2] = std::max(m_outPeakDisplay[2], std::abs(out[2])); + m_outPeakDisplay[3] = std::max(m_outPeakDisplay[3], std::abs(out[3])); + + m_overOuts[0][overSamp] = out[0] + out[2]; + m_overOuts[1][overSamp] = out[1] + out[3]; + } + + std::array<float, 2> s; + if (oversampleVal > 1) + { + s[0] = m_downsampler[0].process_sample(m_overOuts[0].data()); + s[1] = m_downsampler[1].process_sample(m_overOuts[1].data()); + } + else + { + s[0] = m_overOuts[0][0]; + s[1] = m_overOuts[1][0]; + } + + buf[f][0] = d * buf[f][0] + w * s[0]; + buf[f][1] = d * buf[f][1] + w * s[1]; + } + + return ProcessStatus::ContinueIfNotQuiet; +} +#endif + +void SlewDistortion::changeSampleRate() +{ + m_sampleRate = Engine::audioEngine()->outputSampleRate(); + const int oversampleStages = m_slewdistortionControls.m_oversamplingModel.value(); + const int oversampleVal = 1 << oversampleStages; + float sampleRateOver = m_sampleRate * oversampleVal; + + for (int i = 0; i < 2; ++i) + { + m_upsampler[i].setup(oversampleStages, m_sampleRate); + m_downsampler[i].setup(oversampleStages, m_sampleRate); + } + + m_lp.setSampleRate(sampleRateOver); + m_lp.setLowpass(m_slewdistortionControls.m_splitModel.value()); + m_lp.clearHistory(); + + m_hp.setSampleRate(sampleRateOver); + m_hp.setHighpass(m_slewdistortionControls.m_splitModel.value()); + m_hp.clearHistory(); + + m_coeffPrecalc = -1.f / (sampleRateOver * 0.001f); + + m_dcCoeff = std::exp(-2.f * F_PI * SLEW_DISTORTION_DC_FREQ / sampleRateOver); + + std::fill(std::begin(m_inPeakDisplay), std::end(m_inPeakDisplay), 0.0f); + std::fill(std::begin(m_slewOut), std::end(m_slewOut), 0.0f); + std::fill(std::begin(m_dcOffset), std::end(m_dcOffset), 0.0f); + std::fill(std::begin(m_inEnv), std::end(m_inEnv), 0.0f); + std::fill(std::begin(m_outEnv), std::end(m_outEnv), 0.0f); + std::fill(std::begin(m_outPeakDisplay), std::end(m_outPeakDisplay), 0.0f); + for (auto& subArray : m_overOuts) {std::fill(subArray.begin(), subArray.end(), 0.0f);} + + m_biasInterpCoef = std::exp(-1 / (0.01f * m_sampleRate)); +} + + +extern "C" +{ +// necessary for getting instance out of shared lib +PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* parent, void* data) +{ + return new SlewDistortion(parent, static_cast<const Plugin::Descriptor::SubPluginFeatures::Key*>(data)); +} +} + +} // namespace lmms diff --git a/plugins/SlewDistortion/SlewDistortion.h b/plugins/SlewDistortion/SlewDistortion.h new file mode 100755 index 00000000000..78567e909bd --- /dev/null +++ b/plugins/SlewDistortion/SlewDistortion.h @@ -0,0 +1,91 @@ +/* + * SlewDistortion.h + * + * Copyright (c) 2025 Lost Robot <r94231/at/gmail/dot/com> + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_SLEW_DISTORTION_H +#define LMMS_SLEW_DISTORTION_H + +#include "Effect.h" +#include "SlewDistortionControls.h" + +#include "BasicFilters.h" +#include "lmms_math.h" +#include "OversamplingHelpers.h" + +namespace lmms +{ +constexpr inline float SLEW_DISTORTION_MIN_FLOOR = 0.0012589f;// -72 dBFS +constexpr inline float SLEW_DISTORTION_DC_FREQ = 7.f; + +class SlewDistortion : public Effect +{ + Q_OBJECT +public: + SlewDistortion(Model* parent, const Descriptor::SubPluginFeatures::Key* key); + ~SlewDistortion() override = default; + ProcessStatus processImpl(SampleFrame* buf, const fpp_t frames) override; + + EffectControls* controls() override + { + return &m_slewdistortionControls; + } + + float msToCoeff(float ms) + { + return (ms == 0) ? 0 : exp(m_coeffPrecalc / ms); + } +private slots: + void changeSampleRate(); +private: + alignas(16) std::array<float, 4> m_inPeakDisplay = {0}; + alignas(16) std::array<float, 4> m_slewOut = {0}; + alignas(16) std::array<float, 4> m_dcOffset = {0}; + alignas(16) std::array<float, 4> m_inEnv = {0}; + alignas(16) std::array<float, 4> m_outEnv = {0}; + alignas(16) std::array<float, 4> m_outPeakDisplay = {0}; + alignas(16) std::array<std::array<float, 1 << SLEWDIST_MAX_OVERSAMPLE_STAGES>, 2> m_overOuts = {{0}}; + + float m_sampleRate = 44100.f; + + int m_oldOversampleVal = -1; + float m_coeffPrecalc = 0; + float m_dcCoeff = 0; + float m_biasInterpCoef = 0; + float m_trueBias1 = 0; + float m_trueBias2 = 0; + + std::array<Upsampler<SLEWDIST_MAX_OVERSAMPLE_STAGES>, 2> m_upsampler; + std::array<Downsampler<SLEWDIST_MAX_OVERSAMPLE_STAGES>, 2> m_downsampler; + + StereoLinkwitzRiley m_lp; + StereoLinkwitzRiley m_hp; + + SlewDistortionControls m_slewdistortionControls; + + friend class SlewDistortionControls; + friend class gui::SlewDistortionControlDialog; +}; + +} // namespace lmms + +#endif // LMMS_SLEW_DISTORTION_H diff --git a/plugins/SlewDistortion/SlewDistortionControlDialog.cpp b/plugins/SlewDistortion/SlewDistortionControlDialog.cpp new file mode 100755 index 00000000000..ed06eeb7b97 --- /dev/null +++ b/plugins/SlewDistortion/SlewDistortionControlDialog.cpp @@ -0,0 +1,479 @@ +/* + * SlewDistortionControlDialog.cpp + * + * Copyright (c) 2025 Lost Robot <r94231/at/gmail/dot/com> + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SlewDistortionControlDialog.h" +#include "SlewDistortionControls.h" +#include "SlewDistortion.h" + +#include "embed.h" +#include "Knob.h" +#include "MainWindow.h" +#include <QPainter> +#include "GuiApplication.h" +#include "PixmapButton.h" +#include "Draggable.h" +#include "lmms_math.h" + +#include <QPainterPath> + +namespace lmms::gui +{ + +SlewDistortionControlDialog::SlewDistortionControlDialog(SlewDistortionControls* controls) : + EffectControlDialog(controls), + m_controls(controls) +{ + setAutoFillBackground(true); + QPalette pal; + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); + setPalette(pal); + setFixedSize(638, 271); + + auto makeKnob = [this](int x, int y, const QString& hintText, const QString& unit, FloatModel* model, bool smol = false) + { + Knob* newKnob = new Knob(smol ? KnobType::Small17 : KnobType::Bright26, this); + newKnob->move(x, y); + newKnob->setModel(model); + newKnob->setHintText(hintText, unit); + return newKnob; + }; + + auto makeToggleButton = [this](int x, int y, const QString& tooltip, const std::string& activeIcon, const std::string& inactiveIcon, BoolModel* model) + { + PixmapButton* button = new PixmapButton(this, tooltip); + button->setActiveGraphic(PLUGIN_NAME::getIconPixmap(activeIcon)); + button->setInactiveGraphic(PLUGIN_NAME::getIconPixmap(inactiveIcon)); + button->setToolTip(tooltip); + button->move(x, y); + button->setCheckable(true); + button->setModel(model); + return button; + }; + + auto makeGroupButton = [this](int x, int y, const QString& tooltip, const std::string& activeIcon, const std::string& inactiveIcon) + { + PixmapButton* button = new PixmapButton(this, tooltip); + button->setActiveGraphic(PLUGIN_NAME::getIconPixmap(activeIcon)); + button->setInactiveGraphic(PLUGIN_NAME::getIconPixmap(inactiveIcon)); + button->setToolTip(tooltip); + button->move(x, y); + return button; + }; + + ComboBox* distType1Box = new ComboBox(this); + distType1Box->setGeometry(85, 26, 115, 22); + //distType1Box->setFont(pointSize<8>(distType1Box->font())); + distType1Box->setModel(&controls->m_distType1Model); + + ComboBox* distType2Box = new ComboBox(this); + distType2Box->setGeometry(85, 147, 115, 22); + //distType2Box->setFont(pointSize<8>(distType2Box->font())); + distType2Box->setModel(&controls->m_distType2Model); + + Draggable* drive1Draggable = new Draggable(FloatModelEditorBase::DirectionOfManipulation::Vertical, + &controls->m_drive1Model, PLUGIN_NAME::getIconPixmap("handle"), 108, 34, this); + drive1Draggable->move(16, drive1Draggable->y()); + drive1Draggable->setDefaultValPixmap(PLUGIN_NAME::getIconPixmap("handle_zero")); + Draggable* drive2Draggable = new Draggable(FloatModelEditorBase::DirectionOfManipulation::Vertical, + &controls->m_drive2Model, PLUGIN_NAME::getIconPixmap("handle"), 229, 155, this); + drive2Draggable->move(16, drive2Draggable->y()); + drive2Draggable->setDefaultValPixmap(PLUGIN_NAME::getIconPixmap("handle_zero")); + + Draggable* bias1Draggable = new Draggable(FloatModelEditorBase::DirectionOfManipulation::Vertical, + &controls->m_bias1Model, PLUGIN_NAME::getIconPixmap("handle"), 112, 34, this); + bias1Draggable->move(416, bias1Draggable->y()); + bias1Draggable->setDefaultValPixmap(PLUGIN_NAME::getIconPixmap("handle_zero")); + Draggable* bias2Draggable = new Draggable(FloatModelEditorBase::DirectionOfManipulation::Vertical, + &controls->m_bias2Model, PLUGIN_NAME::getIconPixmap("handle"), 233, 155, this); + bias2Draggable->move(416, bias2Draggable->y()); + bias2Draggable->setDefaultValPixmap(PLUGIN_NAME::getIconPixmap("handle_zero")); + + m_slewUp1Knob = makeKnob(96, 65, tr("Slew Up 1:"), "", &controls->m_slewUp1Model); + m_slewUp2Knob = makeKnob(96, 186, tr("Slew Up 2:"), "", &controls->m_slewUp2Model); + m_slewDown1Knob = makeKnob(163, 65, tr("Slew Down 1:"), "", + controls->m_slewLink1Model.value() ? &controls->m_slewUp1Model : &controls->m_slewDown1Model); + m_slewDown2Knob = makeKnob(163, 186, tr("Slew Down 2:"), "", + controls->m_slewLink2Model.value() ? &controls->m_slewUp2Model : &controls->m_slewDown2Model); + makeKnob(329, 26, tr("Warp 1:"), "", &controls->m_warp1Model); + makeKnob(329, 147, tr("Warp 2:"), "", &controls->m_warp2Model); + makeKnob(371, 26, tr("Crush 1:"), "", &controls->m_crush1Model); + makeKnob(371, 147, tr("Crush 2:"), "", &controls->m_crush2Model); + makeKnob(225, 65, tr("Attack 1:"), "", &controls->m_attack1Model); + makeKnob(225, 186, tr("Attack 2:"), "", &controls->m_attack2Model); + makeKnob(267, 65, tr("Release 1:"), "", &controls->m_release1Model); + makeKnob(267, 186, tr("Release 2:"), "", &controls->m_release2Model); + makeKnob(225, 26, tr("Dynamics 1:"), "", &controls->m_dynamics1Model); + makeKnob(225, 147, tr("Dynamics 2:"), "", &controls->m_dynamics2Model); + makeKnob(267, 26, tr("Dynamic Slew 1:"), "", &controls->m_dynamicSlew1Model); + makeKnob(267, 147, tr("Dynamic Slew 2:"), "", &controls->m_dynamicSlew2Model); + + Draggable* outVol1Draggable = new Draggable(FloatModelEditorBase::DirectionOfManipulation::Vertical, + &controls->m_outVol1Model, PLUGIN_NAME::getIconPixmap("handle"), 108, 34, this); + outVol1Draggable->move(594, outVol1Draggable->y()); + outVol1Draggable->setDefaultValPixmap(PLUGIN_NAME::getIconPixmap("handle_zero")); + Draggable* outVol2Draggable = new Draggable(FloatModelEditorBase::DirectionOfManipulation::Vertical, + &controls->m_outVol2Model, PLUGIN_NAME::getIconPixmap("handle"), 229, 155, this); + outVol2Draggable->move(594, outVol2Draggable->y()); + outVol2Draggable->setDefaultValPixmap(PLUGIN_NAME::getIconPixmap("handle_zero")); + + PixmapButton* slewLink1Button = makeToggleButton(132, 70, tr("Slew Link 1"), "link_on", "link_off", &controls->m_slewLink1Model); + connect(slewLink1Button, &PixmapButton::clicked, this, [this, controls]{ + controls->m_slewDown1Model.setValue(controls->m_slewUp1Model.value()); + m_slewDown1Knob->setModel(controls->m_slewLink1Model.value() ? &controls->m_slewUp1Model : &controls->m_slewDown1Model); + }, Qt::DirectConnection); + PixmapButton* slewLink2Button = makeToggleButton(132, 191, tr("Slew Link 2"), "link_on", "link_off", &controls->m_slewLink2Model); + connect(slewLink2Button, &PixmapButton::clicked, this, [this, controls]{ + controls->m_slewDown2Model.setValue(controls->m_slewUp2Model.value()); + m_slewDown2Knob->setModel(controls->m_slewLink2Model.value() ? &controls->m_slewUp2Model : &controls->m_slewDown2Model); + }, Qt::DirectConnection); + + makeToggleButton(9, 248, tr("DC Offset Removal"), "dc_on", "dc_off", &controls->m_dcRemoveModel); + makeToggleButton(99, 248, tr("Multiband"), "mb_on", "mb_off", &controls->m_multibandModel); + + makeKnob(190, 249, tr("Split:"), "", &controls->m_splitModel, true); + makeKnob(338, 78, tr("Mix 1:"), "", &controls->m_mix1Model); + makeKnob(338, 199, tr("Mix 2:"), "", &controls->m_mix2Model); + + PixmapButton* oversample1xButton = makeGroupButton(454, 248, tr("Disable Oversampling"), "oversample_1x_on", "oversample_1x_off"); + PixmapButton* oversample2xButton = makeGroupButton(479, 248, tr("2x Oversampling"), "oversample_2x_on", "oversample_2x_off"); + PixmapButton* oversample4xButton = makeGroupButton(504, 248, tr("4x Oversampling"), "oversample_4x_on", "oversample_4x_off"); + PixmapButton* oversample8xButton = makeGroupButton(529, 248, tr("8x Oversampling"), "oversample_8x_on", "oversample_8x_off"); + PixmapButton* oversample16xButton = makeGroupButton(554, 248, tr("16x Oversampling"), "oversample_16x_on", "oversample_16x_off"); + PixmapButton* oversample32xButton = makeGroupButton(579, 248, tr("32x Oversampling"), "oversample_32x_on", "oversample_32x_off"); + + automatableButtonGroup* oversampleGroup = new automatableButtonGroup(this); + oversampleGroup->addButton(oversample1xButton); + oversampleGroup->addButton(oversample2xButton); + oversampleGroup->addButton(oversample4xButton); + oversampleGroup->addButton(oversample8xButton); + oversampleGroup->addButton(oversample16xButton); + oversampleGroup->addButton(oversample32xButton); + oversampleGroup->setModel(&controls->m_oversamplingModel); + + PixmapButton* m_helpBtn = new PixmapButton(this, nullptr); + m_helpBtn->move(614, 250); + m_helpBtn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("help_on")); + m_helpBtn->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("help_off")); + m_helpBtn->setToolTip(tr("Open help window")); + connect(m_helpBtn, SIGNAL(clicked()), this, SLOT(showHelpWindow())); + + connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(updateDisplay())); +} + +void SlewDistortionControlDialog::updateDisplay() +{ + update(); +} + +void SlewDistortionControlDialog::paintEvent(QPaintEvent *event) +{ + if (!isVisible()) + { + return; + } + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + QRect inMeters[] = { {22, 31, 8, 75}, {30, 31, 8, 75}, {22, 152, 8, 75}, {30, 152, 8, 75} }; + QRect outMeters[] = { {600, 31, 8, 75}, {608, 31, 8, 75}, {600, 152, 8, 75}, {608, 152, 8, 75} }; + + float* inPeak = &m_controls->m_effect->m_inPeakDisplay[0]; + float* outPeak = &m_controls->m_effect->m_outPeakDisplay[0]; + + for (int i = 0; i < 4; ++i) + { + m_lastInPeaks[i] = std::max((inPeak[i] != -1.0f) ? inPeak[i] : m_lastInPeaks[i], SLEW_DISTORTION_MIN_FLOOR); + m_lastOutPeaks[i] = std::max((outPeak[i] != -1.0f) ? outPeak[i] : m_lastOutPeaks[i], SLEW_DISTORTION_MIN_FLOOR); + inPeak[i] = outPeak[i] = -1.0f; + } + + auto drawInverseMeters = [&p](const QRect meters[], const float values[], QColor coverColor) + { + const float dbfsMin = -24.0f; + const float dbfsMax = 24.0f; + + for (int i = 0; i < 4; ++i) + { + float valueDbfs = ampToDbfs(values[i]); + + float normalizedValue = (valueDbfs - dbfsMin) / (dbfsMax - dbfsMin); + normalizedValue = std::clamp(normalizedValue, 0.0f, 1.0f); + + int coveredHeight = static_cast<int>(meters[i].height() * (1.0f - normalizedValue)); + QRect coveredRect(meters[i].left(), meters[i].top(), meters[i].width(), coveredHeight); + + p.fillRect(coveredRect, coverColor); + } + }; + + drawInverseMeters(inMeters, &m_lastInPeaks[0], QColor(10, 10, 10)); + drawInverseMeters(outMeters, &m_lastOutPeaks[0], QColor(10, 10, 10)); + + QRect curveRect1(452, 10, 100, 100); + QRect curveRect2(452, 131, 100, 100); + + QPen gridPen(QColor(36, 40, 48)); + gridPen.setStyle(Qt::DotLine); + p.setPen(gridPen); + + auto drawGrid = [&p](const QRect& rect) + { + for (int i = 1; i < 8; ++i) + { + int x = rect.left() + i * rect.width() / 8 + 1; + p.drawLine(x, rect.top() + 1, x, rect.bottom()); + + int y = rect.top() + i * rect.height() / 8 + 1; + p.drawLine(rect.left() + 1, y, rect.right(), y); + } + }; + + drawGrid(curveRect1); + drawGrid(curveRect2); + + QPen axisPen(QColor(62, 66, 75)); + axisPen.setWidth(2); + p.setPen(axisPen); + + auto drawAxes = [&p](const QRect& rect) + { + p.drawLine(rect.center().x() + 2, rect.top() + 1, rect.center().x() + 2, rect.bottom()); + p.drawLine(rect.left() + 1, rect.center().y() + 2, rect.right(), rect.center().y() + 2); + }; + + drawAxes(curveRect1); + drawAxes(curveRect2); + + auto drawCurve = [&](const QRect& rect, int band) + { + QVector<QPointF> points; + + QPen curvePen(QColor(34, 226, 108)); + curvePen.setWidth(2); + p.setPen(curvePen); + + const int distType = band == 0 ? m_controls->m_distType1Model.value() : m_controls->m_distType2Model.value(); + const float drive = dbfsToAmp(band == 0 ? m_controls->m_drive1Model.value() : m_controls->m_drive2Model.value()); + const float bias = band == 0 ? m_controls->m_bias1Model.value() : m_controls->m_bias2Model.value(); + const float warp = band == 0 ? m_controls->m_warp1Model.value() : m_controls->m_warp2Model.value(); + const float crush = dbfsToAmp(band == 0 ? m_controls->m_crush1Model.value() : m_controls->m_crush2Model.value()); + + const float halfLineWidth = curvePen.widthF() / 2.0f; + const float amplitudeScale = (rect.height() - curvePen.widthF()) / rect.height(); + + const int numSteps = curveRect1.width() * 2; + for (int i = 0; i <= numSteps; ++i) + { + float x = -1.0f + 2.0f * i / numSteps; + + float biasedIn = x * drive + bias; + float distIn = (biasedIn - copysign(warp / crush, biasedIn)) / (1.0f - warp); + float distOut; + + switch (distType) + { + case 0: {// hard clip + distOut = std::clamp(distIn, -1.f, 1.f); + break; + } + case 1: {// tanh + distOut = 2.f / (1.f + std::exp(-2.f * distIn)) - 1; + break; + } + case 2: {// fast soft clip 1 + const float temp = std::clamp(distIn, -2.f, 2.f); + distOut = temp / (1 + 0.25f * temp * temp); + break; + } + case 3: {// fast soft clip 2 + const float temp = std::clamp(distIn, -1.5f, 1.5f); + distOut = temp - (4.f / 27.f) * temp * temp * temp; + break; + } + case 4: {// sinusoidal + distOut = std::sin(distIn); + break; + } + case 5: {// foldback distortion + distOut = std::abs(std::abs(std::fmod(distIn - 1.f, 4.f)) - 2.f) - 1.f; + break; + } + case 6: {// rectify + distOut = std::abs(distIn); + break; + } + case 7: // half-wave rectify + { + distOut = std::max(0.0f, distIn); + break; + } + case 8: // smooth rectify + { + distOut = std::sqrt(distIn * distIn + 0.04f) - 0.2f; + break; + } + case 9: // bitcrush + { + const float scale = 16 / drive; + distOut = std::round(distIn / drive * scale) / scale; + break; + } + default: + { + distOut = distIn; + } + } + + distOut = distOut * (1.0f - warp) + copysign(warp, biasedIn); + if (std::abs(biasedIn) < warp / crush) + { + distOut = biasedIn * crush; + } + + distOut *= amplitudeScale; + + float px = rect.left() + (x + 1.f) * 0.5f * rect.width(); + float py = rect.bottom() - (distOut + 1.f) * 0.5f * rect.height(); + + py += halfLineWidth; + + points.append(QPointF(px, py)); + } + + QPainterPath path; + path.addPolygon(QPolygonF(points)); + p.save(); + p.setClipRect(rect); + p.drawPath(path); + p.restore(); + }; + + drawCurve(curveRect1, 0); + drawCurve(curveRect2, 1); +} + +void SlewDistortionControlDialog::showHelpWindow() +{ + SlewDistortionHelpView::getInstance()->close(); + SlewDistortionHelpView::getInstance()->show(); +} + + +QString SlewDistortionHelpView::s_helpText = +"<div style='text-align: center;'>" +"<b>Slew Distortion</b><br><br>" +"Plugin by Lost Robot<br>" +"GUI by thismoon<br>" +"</div>" +"<h3>Overview:</h3>" +"Slew Distortion is a multiband slew rate limiter and distortion effect.<br><br>" +"Slew rate limiting is something I accidentally invented while trying to make a lowpass filter for the first time.<br>" +"In short, a slew rate limiter limits how quickly the waveform can move from one point to the next.<br>" +"You'll hear that it has a similar quality to a lowpass filter, in that it does quieten the high frequencies by quite a bit.<br>" +"However, the intensity of this effect depends heavily on the input signal, and with it comes a rather unique distortion of that signal.<br><br>" +"In this plugin, the slew rate limiting is followed by waveshaping distortion.<br>" +"Every distortion type is a pure waveshaping function with no filters or delays of any kind involved.<br>" +"These distortions will generate new harmonics at exact frequency multiples of the incoming audio.<br><br>" +"Because the plugin is multiband, you can apply these effects to different frequency ranges independently.<br>" +"<br><h3>Distortion Types:</h3>" +"<b>Hard Clip</b> - Aggressively clamps the audio signal to 0 dBFS.<br>" +"This leaves the signal entirely untouched until it passes the clamping threshold, beyond which all content is clipped out entirely.<br>" +"<b>Tanh</b> - A very gentle sigmoid distortion.<br>" +"This waveshape is mathematically smooth and continuous at all derivatives.<br>" +"It can be pushed significantly harder than most other distortion shapes before it starts generating harsh high frequencies.<br>" +"<b>Fast Soft Clip 1</b> - A CPU-efficient soft clipping function.<br>" +"<b>Fast Soft Clip 2</b> - A CPU-efficient cubic soft clipping function.<br>" +"<b>Sinusoidal</b> - Incredibly smooth wavewrapping distortion.<br>" +"Unlike all the previous distortion types, loud audio information is not entirely lost or clipped away, and is instead wrapped back down to lower values.<br>" +"<b>Foldover</b> - A non-smooth wavewrapping alternative.<br>" +"This leaves the audio values untouched relative to neighboring values,<br>" +"except at the borders where the waveshape sharply changes directions, generating harsh distortion.<br>" +"<b>Full-wave Rectify</b> - Flips the bottom half of the waveform to the top half.<br>" +"The timbre of this commonly sounds similar to shifting the audio upward by one octave.<br>" +"Unlike all the previous distortion types, this one is asymmetrical by default, meaning it will generate even-multiple harmonics.<br>" +"<b>Smooth Rectify</b> - An alternative to Full-wave Rectify which has a smooth corner.<br>" +"<b>Half-wave Rectify</b> - An alternative to Full-wave Rectify which clips all negative audio samples instead of reflecting them upward.<br>" +"<b>Bitcrush</b> - Bit depth reduction. This distortion type is special-cased to have the Drive change its shape instead of its input amplitude.<br>" +"<br><h3>Slew:</h3>" +"This section controls the slew rate limit, the speed at which the incoming waveform's values can change.<br>" +"<b>Up</b> and <b>Down</b> control the slew rate limit for upward and downward movement, respectively.<br>" +"The <b>Slew Link</b> button locks the Slew Up and Slew Down parameters to the same value, for convenience.<br>" +"<br><h3>Dynamics:</h3>" +"This section uses an envelope follower to track the volume of the incoming audio signal.<br>" +"<b>Amount</b> - Restores the dynamic range lost from the distortion and slew rate limiting by matching the output volume to the input volume.<br>" +"<b>Slew</b> - Dynamically changes the slew rate, depending on the input volume.<br>" +"<b>Attack</b> - How quickly the envelope follower responds to increases in volume (e.g. transients).<br>" +"<b>Release</b> - How quickly the envelope follower responds to decreases in volume.<br>" +"<br><h3>Shape:</h3>" +"This section allows further sculpting of the distortion shape beyond what the distortion types can achieve on their own.<br>" +"<b>Warp</b> - Causes input values smaller than this value to be unimpacted by the waveshaping.<br>" +"The distortion shape is properly scaled and shifted to ensure it remains perfectly clean and continuous.<br>" +"<b>Crush</b> - Increases the volume of audio below the Warp value.<br>" +"This adds a sharp corner to the waveshaping function, resulting in much more aggressive distortion.<br>" +"<br><h3>Miscellaneous:</h3>" +"<b>Mix</b> - Blends between the wet and dry signals for the current band.<br>" +"Since both the wet and dry signal are after the crossover filter and have oversampling applied,<br>" +"this parameter is entirely immune to phase issues caused by blending signals.<br>" +"<b>Bias</b> - Adds DC offset to the input signal before the distortion, causing the waveshaping to be asymmetrical.<br>" +"This allows every distortion type to generate even-multiple harmonics, including the symmetrical types which usually only generate odd-multiple harmonics.<br>" +"<b>DC Remover</b> - Removes DC offset (0 Hz audio) from the output signal. You'll almost always want to leave this enabled.<br>" +"<b>Multiband</b> - Splits the signal into two frequency bands. If disabled, the top band's parameters are applied to the entire audio signal.<br>" +"<b>Split</b> - The crossover frequency at which the Multiband mode splits the signal into two bands.<br>" +"<br><h3>Oversampling:</h3>" +"An audio signal is only capable of storing frequencies below Nyquist, which is half of the sample rate.<br>" +"If any form of distortion generates new frequencies that are above this Nyquist frequency, they will be reflected (aliased) back downward.<br>" +"For example, if the distortion generates a harmonic that is 5000 Hz above Nyquist, that frequency will be aliased down to 5000 Hz below Nyquist.<br>" +"This aliasing is inharmonic, oftentimes sounds unpleasant, and can even contribute to auditory masking within the song.<br><br>" +"Oversampling helps to resolve this issue by temporarily increasing the sample rate of the signal,<br>" +"so significantly higher frequencies can be supported before they start aliasing back into the audible range.<br>" +"Those higher frequencies are then filtered out before decreasing the sample rate back to its original value so they don't alias.<br><br>" +"This plugin supports up to five stages of oversampling.<br>" +"Each stage provides an extra 2 octaves of headroom before frequencies alias far enough to become audible.<br>" +"The number on the button is how much the sample rate is increased by. THE PLUGIN'S CPU USAGE WILL BE INCREASED BY APPROXIMATELY THE SAME AMOUNT.<br>" +"Even just 2x oversampling can make a massive difference and is oftentimes all you need, but up to 32x oversampling is supported.<br>" +; + + + +SlewDistortionHelpView::SlewDistortionHelpView():QTextEdit(s_helpText) +{ +#if (QT_VERSION < QT_VERSION_CHECK(5,12,0)) + // Bug workaround: https://codereview.qt-project.org/c/qt/qtbase/+/225348 + using ::operator|; +#endif + setWindowTitle("Slew Distortion Help"); + setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); + getGUI()->mainWindow()->addWindowedWidget(this); + parentWidget()->setAttribute(Qt::WA_DeleteOnClose, false); + parentWidget()->setWindowIcon(PLUGIN_NAME::getIconPixmap("logo")); + + // No maximize button + Qt::WindowFlags flags = parentWidget()->windowFlags(); + flags &= ~Qt::WindowMaximizeButtonHint; + parentWidget()->setWindowFlags(flags); +} + + +} // namespace lmms::gui diff --git a/plugins/SlewDistortion/SlewDistortionControlDialog.h b/plugins/SlewDistortion/SlewDistortionControlDialog.h new file mode 100755 index 00000000000..e5f3a8afd34 --- /dev/null +++ b/plugins/SlewDistortion/SlewDistortionControlDialog.h @@ -0,0 +1,85 @@ +/* + * SlewDistortionControlDialog.h + * + * Copyright (c) 2025 Lost Robot <r94231/at/gmail/dot/com> + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef SLEW_DISTORTION_CONTROL_DIALOG_H +#define SLEW_DISTORTION_CONTROL_DIALOG_H + +#include "EffectControlDialog.h" +#include <QTextEdit> +#include <array> + +namespace lmms +{ + +class SlewDistortionControls; +class FloatModel; + +namespace gui +{ + +class Knob; + +class SlewDistortionControlDialog : public EffectControlDialog +{ + Q_OBJECT +public: + SlewDistortionControlDialog(SlewDistortionControls* controls); + ~SlewDistortionControlDialog() override = default; + + void paintEvent(QPaintEvent *event) override; +public slots: + void updateDisplay(); + void showHelpWindow(); +private: + SlewDistortionControls* m_controls; + + Knob* m_slewUp1Knob; + Knob* m_slewUp2Knob; + Knob* m_slewDown1Knob; + Knob* m_slewDown2Knob; + + std::array<float, 4> m_lastInPeaks = {0}; + std::array<float, 4> m_lastOutPeaks = {0}; +}; + +class SlewDistortionHelpView : public QTextEdit +{ + Q_OBJECT +public: + static SlewDistortionHelpView* getInstance() + { + static SlewDistortionHelpView instance; + return &instance; + } + +private: + SlewDistortionHelpView(); + static QString s_helpText; +}; + +} // namespace gui + +} // namespace lmms + +#endif diff --git a/plugins/SlewDistortion/SlewDistortionControls.cpp b/plugins/SlewDistortion/SlewDistortionControls.cpp new file mode 100755 index 00000000000..2c2f0ab1244 --- /dev/null +++ b/plugins/SlewDistortion/SlewDistortionControls.cpp @@ -0,0 +1,183 @@ +/* + * SlewDistortionControls.cpp + * + * Copyright (c) 2025 Lost Robot <r94231/at/gmail/dot/com> + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include <QDomElement> + +#include "SlewDistortionControls.h" +#include "SlewDistortion.h" + +namespace lmms +{ + +SlewDistortionControls::SlewDistortionControls(SlewDistortion* effect) : + EffectControls(effect), + m_effect(effect), + m_distType1Model(this, tr("Type 1")), + m_distType2Model(this, tr("Type 2")), + m_drive1Model(0.0f, -24.f, 24.0f, 0.0001f, this, tr("Drive 1")), + m_drive2Model(0.0f, -24.f, 24.0f, 0.0001f, this, tr("Drive 2")), + m_slewUp1Model(6.0f, -96.f, 6.0f, 0.0001f, this, tr("Slew Up 1")), + m_slewUp2Model(6.0f, -96.f, 6.0f, 0.0001f, this, tr("Slew Up 2")), + m_slewDown1Model(6.0f, -96.f, 6.0f, 0.0001f, this, tr("Slew Down 1")), + m_slewDown2Model(6.0f, -96.f, 6.0f, 0.0001f, this, tr("Slew Down 2")), + m_bias1Model(0.0f, -2.0f, 2.0f, 0.0001f, this, tr("Bias 1")), + m_bias2Model(0.0f, -2.0f, 2.0f, 0.0001f, this, tr("Bias 2")), + m_warp1Model(0.0f, 0.0f, 0.99f, 0.0001f, this, tr("Warp 1")), + m_warp2Model(0.0f, 0.0f, 0.99f, 0.0001f, this, tr("Warp 2")), + m_crush1Model(0.0f, 0.0f, 24.0f, 0.0001f, this, tr("Crush 1")), + m_crush2Model(0.0f, 0.0f, 24.0f, 0.0001f, this, tr("Crush 2")), + m_outVol1Model(0.0f, -24.0f, 24.0f, 0.0001f, this, tr("Out Vol 1")), + m_outVol2Model(0.0f, -24.0f, 24.0f, 0.0001f, this, tr("Out Vol 2")), + m_attack1Model(2.0f, 0.01f, 200.0f, 0.01f, this, tr("Attack 1")), + m_attack2Model(2.0f, 0.01f, 200.0f, 0.01f, this, tr("Attack 2")), + m_release1Model(20.0f, 0.01f, 800.0f, 0.01f, this, tr("Release 1")), + m_release2Model(20.0f, 0.01f, 800.0f, 0.01f, this, tr("Release 2")), + m_dynamics1Model(0.0f, 0.0f, 1.0f, 0.0001f, this, tr("Dynamics 1")), + m_dynamics2Model(0.0f, 0.0f, 1.0f, 0.0001f, this, tr("Dynamics 2")), + m_dynamicSlew1Model(0.0f, -8.0f, 8.0f, 0.0001f, this, tr("Dynamic Slew 1")), + m_dynamicSlew2Model(0.0f, -8.0f, 8.0f, 0.0001f, this, tr("Dynamic Slew 2")), + m_dcRemoveModel(true, this, tr("DC Offset Remover")), + m_multibandModel(false, this, tr("Multiband")), + m_oversamplingModel(0, 0, SLEWDIST_MAX_OVERSAMPLE_STAGES, this, tr("Oversample")), + m_splitModel(200.0f, 100.0f, 20000.0f, 0.1f, this, tr("Split")), + m_mix1Model(1.0f, 0.0f, 1.0f, 0.0001f, this, tr("Mix 1")), + m_mix2Model(1.0f, 0.0f, 1.0f, 0.0001f, this, tr("Mix 2")), + m_slewLink1Model(true, this, tr("Slew Link 1")), + m_slewLink2Model(true, this, tr("Slew Link 2")) +{ + m_slewUp1Model.setScaleLogarithmic(true); + m_slewUp2Model.setScaleLogarithmic(true); + m_slewDown1Model.setScaleLogarithmic(true); + m_slewDown2Model.setScaleLogarithmic(true); + m_crush1Model.setScaleLogarithmic(true); + m_crush2Model.setScaleLogarithmic(true); + m_attack1Model.setScaleLogarithmic(true); + m_attack2Model.setScaleLogarithmic(true); + m_release1Model.setScaleLogarithmic(true); + m_release2Model.setScaleLogarithmic(true); + m_dynamicSlew1Model.setScaleLogarithmic(true); + m_dynamicSlew2Model.setScaleLogarithmic(true); + m_splitModel.setScaleLogarithmic(true); + + m_distType1Model.addItem(tr("Hard Clip")); + m_distType1Model.addItem(tr("Tanh")); + m_distType1Model.addItem(tr("Fast Soft Clip 1")); + m_distType1Model.addItem(tr("Fast Soft Clip 2")); + m_distType1Model.addItem(tr("Sinusoidal")); + m_distType1Model.addItem(tr("Foldback")); + m_distType1Model.addItem(tr("Full Rectify")); + m_distType1Model.addItem(tr("Half Rectify")); + m_distType1Model.addItem(tr("Smooth Rectify")); + m_distType1Model.addItem(tr("Bitcrush")); + + m_distType2Model.addItem(tr("Hard Clip")); + m_distType2Model.addItem(tr("Tanh")); + m_distType2Model.addItem(tr("Fast Soft Clip 1")); + m_distType2Model.addItem(tr("Fast Soft Clip 2")); + m_distType2Model.addItem(tr("Sinusoidal")); + m_distType2Model.addItem(tr("Foldback")); + m_distType2Model.addItem(tr("Full Rectify")); + m_distType2Model.addItem(tr("Half Rectify")); + m_distType2Model.addItem(tr("Smooth Rectify")); + m_distType2Model.addItem(tr("Bitcrush")); +} + + +void SlewDistortionControls::loadSettings(const QDomElement& parent) +{ + m_distType1Model.loadSettings(parent, "distType1"); + m_distType2Model.loadSettings(parent, "distType2"); + m_drive1Model.loadSettings(parent, "drive1"); + m_drive2Model.loadSettings(parent, "drive2"); + m_slewUp1Model.loadSettings(parent, "slewUp1"); + m_slewUp2Model.loadSettings(parent, "slewUp2"); + m_slewDown1Model.loadSettings(parent, "slewDown1"); + m_slewDown2Model.loadSettings(parent, "slewDown2"); + m_bias1Model.loadSettings(parent, "bias1"); + m_bias2Model.loadSettings(parent, "bias2"); + m_warp1Model.loadSettings(parent, "warp1"); + m_warp2Model.loadSettings(parent, "warp2"); + m_crush1Model.loadSettings(parent, "crush1"); + m_crush2Model.loadSettings(parent, "crush2"); + m_outVol1Model.loadSettings(parent, "outVol1"); + m_outVol2Model.loadSettings(parent, "outVol2"); + m_attack1Model.loadSettings(parent, "attack1"); + m_attack2Model.loadSettings(parent, "attack2"); + m_release1Model.loadSettings(parent, "release1"); + m_release2Model.loadSettings(parent, "release2"); + m_dynamics1Model.loadSettings(parent, "dynamics1"); + m_dynamics2Model.loadSettings(parent, "dynamics2"); + m_dynamicSlew1Model.loadSettings(parent, "dynamicSlew1"); + m_dynamicSlew2Model.loadSettings(parent, "dynamicSlew2"); + m_dcRemoveModel.loadSettings(parent, "dcRemove"); + m_multibandModel.loadSettings(parent, "multiband"); + m_oversamplingModel.loadSettings(parent, "oversampling"); + m_splitModel.loadSettings(parent, "split"); + m_mix1Model.loadSettings(parent, "mix1"); + m_mix2Model.loadSettings(parent, "mix2"); + m_slewLink1Model.loadSettings(parent, "slewLink1"); + m_slewLink2Model.loadSettings(parent, "slewLink2"); +} + + + +void SlewDistortionControls::saveSettings(QDomDocument& doc, QDomElement& parent) +{ + m_distType1Model.saveSettings(doc, parent, "distType1"); + m_distType2Model.saveSettings(doc, parent, "distType2"); + m_drive1Model.saveSettings(doc, parent, "drive1"); + m_drive2Model.saveSettings(doc, parent, "drive2"); + m_slewUp1Model.saveSettings(doc, parent, "slewUp1"); + m_slewUp2Model.saveSettings(doc, parent, "slewUp2"); + m_slewDown1Model.saveSettings(doc, parent, "slewDown1"); + m_slewDown2Model.saveSettings(doc, parent, "slewDown2"); + m_bias1Model.saveSettings(doc, parent, "bias1"); + m_bias2Model.saveSettings(doc, parent, "bias2"); + m_warp1Model.saveSettings(doc, parent, "warp1"); + m_warp2Model.saveSettings(doc, parent, "warp2"); + m_crush1Model.saveSettings(doc, parent, "crush1"); + m_crush2Model.saveSettings(doc, parent, "crush2"); + m_outVol1Model.saveSettings(doc, parent, "outVol1"); + m_outVol2Model.saveSettings(doc, parent, "outVol2"); + m_attack1Model.saveSettings(doc, parent, "attack1"); + m_attack2Model.saveSettings(doc, parent, "attack2"); + m_release1Model.saveSettings(doc, parent, "release1"); + m_release2Model.saveSettings(doc, parent, "release2"); + m_dynamics1Model.saveSettings(doc, parent, "dynamics1"); + m_dynamics2Model.saveSettings(doc, parent, "dynamics2"); + m_dynamicSlew1Model.saveSettings(doc, parent, "dynamicSlew1"); + m_dynamicSlew2Model.saveSettings(doc, parent, "dynamicSlew2"); + m_dcRemoveModel.saveSettings(doc, parent, "dcRemove"); + m_multibandModel.saveSettings(doc, parent, "multiband"); + m_oversamplingModel.saveSettings(doc, parent, "oversampling"); + m_splitModel.saveSettings(doc, parent, "split"); + m_mix1Model.saveSettings(doc, parent, "mix1"); + m_mix2Model.saveSettings(doc, parent, "mix2"); + m_slewLink1Model.saveSettings(doc, parent, "slewLink1"); + m_slewLink2Model.saveSettings(doc, parent, "slewLink2"); +} + + + +} // namespace lmms diff --git a/plugins/SlewDistortion/SlewDistortionControls.h b/plugins/SlewDistortion/SlewDistortionControls.h new file mode 100755 index 00000000000..b8c716c533a --- /dev/null +++ b/plugins/SlewDistortion/SlewDistortionControls.h @@ -0,0 +1,105 @@ +/* + * SlewDistortionControls.h + * + * Copyright (c) 2025 Lost Robot <r94231/at/gmail/dot/com> + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef SLEW_DISTORTION_CONTROLS_H +#define SLEW_DISTORTION_CONTROLS_H + +#include "EffectControls.h" +#include "SlewDistortionControlDialog.h" +#include "ComboBox.h" + +namespace lmms +{ + +constexpr int SLEWDIST_MAX_OVERSAMPLE_STAGES = 5; + +class SlewDistortion; + +namespace gui +{ +class SlewDistortionControlDialog; +} + +class SlewDistortionControls : public EffectControls +{ + Q_OBJECT +public: + SlewDistortionControls(SlewDistortion* effect); + ~SlewDistortionControls() override = default; + + void saveSettings(QDomDocument& doc, QDomElement& parent) override; + void loadSettings(const QDomElement& parent) override; + inline QString nodeName() const override + { + return "SlewDistortionControls"; + } + gui::EffectControlDialog* createView() override + { + return new gui::SlewDistortionControlDialog(this); + } + int controlCount() override { return 32; } + +private: + SlewDistortion* m_effect; + + ComboBoxModel m_distType1Model; + ComboBoxModel m_distType2Model; + FloatModel m_drive1Model; + FloatModel m_drive2Model; + FloatModel m_slewUp1Model; + FloatModel m_slewUp2Model; + FloatModel m_slewDown1Model; + FloatModel m_slewDown2Model; + FloatModel m_bias1Model; + FloatModel m_bias2Model; + FloatModel m_warp1Model; + FloatModel m_warp2Model; + FloatModel m_crush1Model; + FloatModel m_crush2Model; + FloatModel m_outVol1Model; + FloatModel m_outVol2Model; + FloatModel m_attack1Model; + FloatModel m_attack2Model; + FloatModel m_release1Model; + FloatModel m_release2Model; + FloatModel m_dynamics1Model; + FloatModel m_dynamics2Model; + FloatModel m_dynamicSlew1Model; + FloatModel m_dynamicSlew2Model; + BoolModel m_dcRemoveModel; + BoolModel m_multibandModel; + IntModel m_oversamplingModel; + FloatModel m_splitModel; + FloatModel m_mix1Model; + FloatModel m_mix2Model; + BoolModel m_slewLink1Model; + BoolModel m_slewLink2Model; + + friend class gui::SlewDistortionControlDialog; + friend class SlewDistortion; +}; + +} // namespace lmms + +#endif diff --git a/plugins/SlewDistortion/artwork.png b/plugins/SlewDistortion/artwork.png new file mode 100644 index 00000000000..8f516729c82 Binary files /dev/null and b/plugins/SlewDistortion/artwork.png differ diff --git a/plugins/SlewDistortion/dc_off.png b/plugins/SlewDistortion/dc_off.png new file mode 100644 index 00000000000..d2bc7e309f2 Binary files /dev/null and b/plugins/SlewDistortion/dc_off.png differ diff --git a/plugins/SlewDistortion/dc_on.png b/plugins/SlewDistortion/dc_on.png new file mode 100644 index 00000000000..c0945bd456f Binary files /dev/null and b/plugins/SlewDistortion/dc_on.png differ diff --git a/plugins/SlewDistortion/handle.png b/plugins/SlewDistortion/handle.png new file mode 100644 index 00000000000..08e68bef3ae Binary files /dev/null and b/plugins/SlewDistortion/handle.png differ diff --git a/plugins/SlewDistortion/handle_zero.png b/plugins/SlewDistortion/handle_zero.png new file mode 100644 index 00000000000..0960a8f441e Binary files /dev/null and b/plugins/SlewDistortion/handle_zero.png differ diff --git a/plugins/SlewDistortion/help_off.png b/plugins/SlewDistortion/help_off.png new file mode 100644 index 00000000000..0a3b927f9da Binary files /dev/null and b/plugins/SlewDistortion/help_off.png differ diff --git a/plugins/SlewDistortion/help_on.png b/plugins/SlewDistortion/help_on.png new file mode 100644 index 00000000000..b9b51d6ed11 Binary files /dev/null and b/plugins/SlewDistortion/help_on.png differ diff --git a/plugins/SlewDistortion/link_off.png b/plugins/SlewDistortion/link_off.png new file mode 100644 index 00000000000..8b658abcbb2 Binary files /dev/null and b/plugins/SlewDistortion/link_off.png differ diff --git a/plugins/SlewDistortion/link_on.png b/plugins/SlewDistortion/link_on.png new file mode 100644 index 00000000000..c289f16e845 Binary files /dev/null and b/plugins/SlewDistortion/link_on.png differ diff --git a/plugins/SlewDistortion/logo.png b/plugins/SlewDistortion/logo.png new file mode 100755 index 00000000000..9340da708dd Binary files /dev/null and b/plugins/SlewDistortion/logo.png differ diff --git a/plugins/SlewDistortion/mb_off.png b/plugins/SlewDistortion/mb_off.png new file mode 100644 index 00000000000..d5f1a4fb03a Binary files /dev/null and b/plugins/SlewDistortion/mb_off.png differ diff --git a/plugins/SlewDistortion/mb_on.png b/plugins/SlewDistortion/mb_on.png new file mode 100644 index 00000000000..4f35fc1c526 Binary files /dev/null and b/plugins/SlewDistortion/mb_on.png differ diff --git a/plugins/SlewDistortion/oversample_16x_off.png b/plugins/SlewDistortion/oversample_16x_off.png new file mode 100644 index 00000000000..ad85c1cdf4b Binary files /dev/null and b/plugins/SlewDistortion/oversample_16x_off.png differ diff --git a/plugins/SlewDistortion/oversample_16x_on.png b/plugins/SlewDistortion/oversample_16x_on.png new file mode 100644 index 00000000000..18c440cb7e7 Binary files /dev/null and b/plugins/SlewDistortion/oversample_16x_on.png differ diff --git a/plugins/SlewDistortion/oversample_1x_off.png b/plugins/SlewDistortion/oversample_1x_off.png new file mode 100644 index 00000000000..3f7a8408084 Binary files /dev/null and b/plugins/SlewDistortion/oversample_1x_off.png differ diff --git a/plugins/SlewDistortion/oversample_1x_on.png b/plugins/SlewDistortion/oversample_1x_on.png new file mode 100644 index 00000000000..a7231eb407d Binary files /dev/null and b/plugins/SlewDistortion/oversample_1x_on.png differ diff --git a/plugins/SlewDistortion/oversample_2x_off.png b/plugins/SlewDistortion/oversample_2x_off.png new file mode 100644 index 00000000000..91eb6321116 Binary files /dev/null and b/plugins/SlewDistortion/oversample_2x_off.png differ diff --git a/plugins/SlewDistortion/oversample_2x_on.png b/plugins/SlewDistortion/oversample_2x_on.png new file mode 100644 index 00000000000..e66991d8436 Binary files /dev/null and b/plugins/SlewDistortion/oversample_2x_on.png differ diff --git a/plugins/SlewDistortion/oversample_32x_off.png b/plugins/SlewDistortion/oversample_32x_off.png new file mode 100644 index 00000000000..c9ec66b40e3 Binary files /dev/null and b/plugins/SlewDistortion/oversample_32x_off.png differ diff --git a/plugins/SlewDistortion/oversample_32x_on.png b/plugins/SlewDistortion/oversample_32x_on.png new file mode 100644 index 00000000000..b16e6cb40ed Binary files /dev/null and b/plugins/SlewDistortion/oversample_32x_on.png differ diff --git a/plugins/SlewDistortion/oversample_4x_off.png b/plugins/SlewDistortion/oversample_4x_off.png new file mode 100644 index 00000000000..a9ad46909f1 Binary files /dev/null and b/plugins/SlewDistortion/oversample_4x_off.png differ diff --git a/plugins/SlewDistortion/oversample_4x_on.png b/plugins/SlewDistortion/oversample_4x_on.png new file mode 100644 index 00000000000..95ce8b54e0b Binary files /dev/null and b/plugins/SlewDistortion/oversample_4x_on.png differ diff --git a/plugins/SlewDistortion/oversample_8x_off.png b/plugins/SlewDistortion/oversample_8x_off.png new file mode 100644 index 00000000000..84a91727ce8 Binary files /dev/null and b/plugins/SlewDistortion/oversample_8x_off.png differ diff --git a/plugins/SlewDistortion/oversample_8x_on.png b/plugins/SlewDistortion/oversample_8x_on.png new file mode 100644 index 00000000000..bdc12455484 Binary files /dev/null and b/plugins/SlewDistortion/oversample_8x_on.png differ diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index fe4a2c462b2..2c017116d24 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -104,6 +104,7 @@ SET(LMMS_SRCS gui/widgets/CaptionMenu.cpp gui/widgets/ComboBox.cpp gui/widgets/CustomTextKnob.cpp + gui/widgets/Draggable.cpp gui/widgets/Fader.cpp gui/widgets/FloatModelEditorBase.cpp gui/widgets/Graph.cpp diff --git a/src/gui/widgets/Draggable.cpp b/src/gui/widgets/Draggable.cpp new file mode 100644 index 00000000000..e579b94fd63 --- /dev/null +++ b/src/gui/widgets/Draggable.cpp @@ -0,0 +1,128 @@ +/* + * Draggable.h + * + * Copyright (c) 2022 Lost Robot <r94231/at/gmail/dot/com> + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Draggable.h" +#include "SimpleTextFloat.h" +#include "interpolation.h" + +#include <QPixmap> +#include <QMouseEvent> +#include <QPainter> + +namespace lmms::gui +{ + +Draggable::Draggable(FloatModelEditorBase::DirectionOfManipulation directionOfManipulation, + FloatModel* floatModel, const QPixmap &pixmap, int pointA, int pointB, QWidget* parent) + : FloatModelEditorBase(directionOfManipulation, parent), + m_pixmap(pixmap), + m_defaultValPixmap(), + m_pointA(pointA), + m_pointB(pointB), + m_defaultValue(0), + m_hasDefaultValPixmap(false) +{ + setModel(floatModel); + connect(model(), &FloatModel::dataChanged, this, &Draggable::handleMovement); + handleMovement(); +} + +QSize Draggable::sizeHint() const +{ + return m_pixmap.size(); +} + +void Draggable::setPixmap(const QPixmap &pixmap) +{ + m_pixmap = pixmap; + update(); +} + +void Draggable::setDefaultValPixmap(const QPixmap &pixmap, float value) +{ + m_defaultValPixmap = pixmap; + m_defaultValue = value; + m_hasDefaultValPixmap = true; + update(); +} + +void Draggable::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + QPainter painter(this); + + if (m_hasDefaultValPixmap && model()->value() == m_defaultValue) + { + painter.drawPixmap(rect(), m_defaultValPixmap, m_defaultValPixmap.rect()); + } + else + { + painter.drawPixmap(rect(), m_pixmap, m_pixmap.rect()); + } +} + +void Draggable::mouseMoveEvent(QMouseEvent *me) +{ + QPoint pPos = mapToParent(me->pos()); + + if (m_buttonPressed && pPos != m_lastMousePos) + { + float point = (m_directionOfManipulation == DirectionOfManipulation::Vertical) ? pPos.y() : pPos.x(); + float progress = (point - m_pointA) / (m_pointB - m_pointA); + + if (progress >= 0 && progress <= 1) + { + float newVal = progress * (model()->maxValue() - model()->minValue()) + model()->minValue(); + model()->setValue(newVal); + } + else if (progress < 0) + { + model()->setValue(model()->minValue()); + } + else + { + model()->setValue(model()->maxValue()); + } + + emit sliderMoved(model()->value()); + m_lastMousePos = pPos; + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); + } +} + +void Draggable::handleMovement() +{ + float newCoord = linearInterpolate(m_pointA, m_pointB, (model()->value() - model()->minValue()) / (model()->maxValue() - model()->minValue())); + if (m_directionOfManipulation == DirectionOfManipulation::Vertical) + { + move(x(), newCoord - m_pixmap.height() / 2.f); + } + else if (m_directionOfManipulation == DirectionOfManipulation::Horizontal) + { + move(newCoord - m_pixmap.width() / 2.f, y()); + } +} + +} // namespace lmms::gui