Add SlewDistortion effect and oversampling support (#7641)

This commit is contained in:
Lost Robot
2025-10-13 14:18:34 -05:00
committed by GitHub
parent 807751dc4d
commit a809c03f87
38 changed files with 2232 additions and 7 deletions

View File

@@ -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)

View File

@@ -0,0 +1,720 @@
/*
* 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(), &AudioEngine::sampleRateChanged, this, &SlewDistortion::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 SlewDistortionType distType1 = static_cast<SlewDistortionType>(m_slewdistortionControls.m_distType1Model.value());
const SlewDistortionType distType2 = static_cast<SlewDistortionType>(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].processSample(m_overOuts[0].data(), buf[f][0]);
m_upsampler[1].processSample(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 = sse2Abs(in);
// 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(&m_inEnv[0]);
__m128 slewOut = _mm_load_ps(&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, fastLog(inEnv));
// clamp to [-80.0f, 80.0f] since float std::exp breaks outside of those bounds
__m128 clampedScaledLog = _mm_max_ps(_mm_min_ps(scaledLog, _mm_set1_ps(80.0f)), _mm_set1_ps(-80.0f));
__m128 slewMult = fastExp(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 - std::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)
{
SlewDistortionType 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 SlewDistortionType::HardClip:// 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 SlewDistortionType::Tanh: // Tanh => 2 / (1 + exp(-2x)) - 1
{
// clamp to [-80.0f, 80.0f] since float std::exp 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(80.0f)), _mm_set1_ps(-80.0f));
__m128 expResult = fastExp(clampedInput);
distOutFull = _mm_sub_ps(_mm_div_ps(_mm_set1_ps(2.0f), _mm_add_ps(one, expResult)), one);
break;
}
case SlewDistortionType::FastSoftClip1: // 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 SlewDistortionType::FastSoftClip2: // 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 SlewDistortionType::Sinusoidal: // Sinusoidal => sin(x)
{
// SSE2 sine approximation I created
__m128 pi = _mm_set1_ps(std::numbers::pi_v<float>);
__m128 piOverTwo = _mm_set1_ps(std::numbers::pi_v<float> * 0.5f);
__m128 tau = _mm_set1_ps(std::numbers::pi_v<float> * 2.f);
__m128 distMinusPiOverTwo = _mm_sub_ps(distInFull, piOverTwo);
__m128 divByTwoPi = _mm_div_ps(distMinusPiOverTwo, tau);
// SSE2 floor replacement
__m128 floorDivByTwoPi = sse2Floor(divByTwoPi);
// 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(sse2Abs(_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 SlewDistortionType::Foldback: // 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 floorOverFour = sse2Floor(divByFour);
distOutFull = _mm_sub_ps(sse2Abs(_mm_sub_ps(_mm_sub_ps(
distInMinusOne, _mm_mul_ps(floorOverFour, four)), _mm_set1_ps(2.0f))), one);
break;
}
case SlewDistortionType::FullRectify: // |x|
{
distOutFull = sse2Abs(distInFull);
break;
}
case SlewDistortionType::SmoothRectify: // 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 SlewDistortionType::HalfRectify: // max(0, x)
{
distOutFull = _mm_max_ps(_mm_setzero_ps(), distInFull);
break;
}
case SlewDistortionType::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, half away from zero
__m128 rounded = sse2Round(scaledVal);
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(reinterpret_cast<__m64*>(&distOutArr[0]), distOutFull);
}
else
{
// for elements 2 and 3
_mm_storeh_pi(reinterpret_cast<__m64*>(&distOutArr[2]), distOutFull);
}
}
}
__m128 distOut = _mm_load_ps(&distOutArr[0]);
// (1 - warp) * distOut + std::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 = sse2Abs(biasedIn);
__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(&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(&m_outEnv[0]);
__m128 absOut = sse2Abs(distOutMinusDC);
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 = sse2Abs(outFinal);
_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(&m_inEnv[0], inEnv);
_mm_store_ps(&m_slewOut[0], slewOut);
_mm_store_ps(&m_dcOffset[0], dcOffset);
_mm_store_ps(&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].processSample(m_overOuts[0].data());
s[1] = m_downsampler[1].processSample(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 SlewDistortionType distType1 = static_cast<SlewDistortionType>(m_slewdistortionControls.m_distType1Model.value());
const SlewDistortionType distType2 = static_cast<SlewDistortionType>(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<SlewDistortionType, 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].processSample(m_overOuts[0].data(), buf[f][0]);
m_upsampler[1].processSample(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 - std::copysign(warp[i] / crush[i], biasedIn)) / (1.f - warp[i]);
float distOut;
switch (static_cast<SlewDistortionType>(distType[i]))
{
case SlewDistortionType::HardClip: {
distOut = std::clamp(distIn, -1.f, 1.f);
break;
}
case SlewDistortionType::Tanh: {
const float temp = std::clamp(distIn, -40.f, 40.f);
distOut = 2.f / (1.f + std::exp(-2.f * temp)) - 1;
break;
}
case SlewDistortionType::FastSoftClip1: {
const float temp = std::clamp(distIn, -2.f, 2.f);
distOut = temp / (1 + 0.25f * temp * temp);
break;
}
case SlewDistortionType::FastSoftClip2: {
const float temp = std::clamp(distIn, -1.5f, 1.5f);
distOut = temp - (4.f / 27.f) * temp * temp * temp;
break;
}
case SlewDistortionType::Sinusoidal: {
// using a polynomial approximation so it matches with the SSE2 code
// x - x^3 / 6 + x^5 / 120
float modInput = std::fmod(distIn - std::numbers::pi_v<float> * 0.5f, 2.f * std::numbers::pi_v<float>);
if (modInput < 0) {modInput += 2.f * std::numbers::pi_v<float>;}
const float x = std::abs(modInput - std::numbers::pi_v<float>) - std::numbers::pi_v<float> * 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 SlewDistortionType::Foldback: {
distOut = std::abs(std::abs(std::fmod(distIn - 1.f, 4.f)) - 2.f) - 1.f;
break;
}
case SlewDistortionType::FullRectify: {
distOut = std::abs(distIn);
break;
}
case SlewDistortionType::SmoothRectify:
{
distOut = std::sqrt(distIn * distIn + 0.04f) - 0.2f;
break;
}
case SlewDistortionType::HalfRectify:
{
distOut = std::max(0.0f, distIn);
break;
}
case SlewDistortionType::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]) + std::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 *= std::lerp(1.f, m_inEnv[i] / m_outEnv[i], dynamics[i]);
out[i] = std::lerp(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].processSample(m_overOuts[0].data());
s[1] = m_downsampler[1].processSample(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 * std::numbers::pi_v<float> * 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

View File

@@ -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 : std::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

View File

@@ -0,0 +1,494 @@
/*
* 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)
{
using DirectionOfManipulation = FloatModelEditorBase::DirectionOfManipulation;
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(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(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(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(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(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(DirectionOfManipulation::Vertical,
&controls->m_outVol2Model, PLUGIN_NAME::getIconPixmap("handle"), 229, 155, this);
outVol2Draggable->move(594, outVol2Draggable->y());
outVol2Draggable->setDefaultValPixmap(PLUGIN_NAME::getIconPixmap("handle_zero"));
makeToggleButton(132, 70, tr("Slew Link 1"), "link_on", "link_off", &controls->m_slewLink1Model);
connect(&controls->m_slewLink1Model, &BoolModel::dataChanged, this, [this, controls]{
if (controls->m_slewLink1Model.value())
{
controls->m_slewDown1Model.setValue(controls->m_slewUp1Model.value());
m_slewDown1Knob->setModel(&controls->m_slewUp1Model);
}
else
{
m_slewDown1Knob->setModel(&controls->m_slewDown1Model);
}
});
makeToggleButton(132, 191, tr("Slew Link 2"), "link_on", "link_off", &controls->m_slewLink2Model);
connect(&controls->m_slewLink2Model, &BoolModel::dataChanged, this, [this, controls]{
if (controls->m_slewLink2Model.value())
{
controls->m_slewDown2Model.setValue(controls->m_slewUp2Model.value());
m_slewDown2Knob->setModel(&controls->m_slewUp2Model);
}
else
{
m_slewDown2Knob->setModel(&controls->m_slewDown2Model);
}
});
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, &PixmapButton::clicked, this, &SlewDistortionControlDialog::showHelpWindow);
connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(update()));
}
void SlewDistortionControlDialog::paintEvent(QPaintEvent* event)
{
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 (static_cast<SlewDistortionType>(distType))
{
case SlewDistortionType::HardClip: {
distOut = std::clamp(distIn, -1.f, 1.f);
break;
}
case SlewDistortionType::Tanh: {
const float temp = std::clamp(distIn, -40.f, 40.f);
distOut = 2.f / (1.f + std::exp(-2.f * temp)) - 1;
break;
}
case SlewDistortionType::FastSoftClip1: {
const float temp = std::clamp(distIn, -2.f, 2.f);
distOut = temp / (1 + 0.25f * temp * temp);
break;
}
case SlewDistortionType::FastSoftClip2: {
const float temp = std::clamp(distIn, -1.5f, 1.5f);
distOut = temp - (4.f / 27.f) * temp * temp * temp;
break;
}
case SlewDistortionType::Sinusoidal: {
// using a polynomial approximation so it matches with the SSE2 code
// x - x^3 / 6 + x^5 / 120
float modInput = std::fmod(distIn - std::numbers::pi_v<float> * 0.5f, 2.f * std::numbers::pi_v<float>);
if (modInput < 0) {modInput += 2.f * std::numbers::pi_v<float>;}
const float x = std::abs(modInput - std::numbers::pi_v<float>) - std::numbers::pi_v<float> * 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 SlewDistortionType::Foldback: {
distOut = std::abs(std::abs(std::fmod(distIn - 1.f, 4.f)) - 2.f) - 1.f;
break;
}
case SlewDistortionType::FullRectify: {
distOut = std::abs(distIn);
break;
}
case SlewDistortionType::SmoothRectify:
{
distOut = std::sqrt(distIn * distIn + 0.04f) - 0.2f;
break;
}
case SlewDistortionType::HalfRectify:
{
distOut = std::max(0.0f, distIn);
break;
}
case SlewDistortionType::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 = tr(
"<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

View File

@@ -0,0 +1,84 @@
/*
* 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 LMMS_GUI_SLEW_DISTORTION_CONTROL_DIALOG_H
#define LMMS_GUI_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 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 // LMMS_GUI_SLEW_DISTORTION_CONTROL_DIALOG_H

View File

@@ -0,0 +1,184 @@
/*
* 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 "SlewDistortionControls.h"
#include <QDomElement>
#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

View File

@@ -0,0 +1,120 @@
/*
* 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 LMMS_SLEW_DISTORTION_CONTROLS_H
#define LMMS_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;
}
enum class SlewDistortionType : int
{
HardClip = 0,
Tanh,
FastSoftClip1,
FastSoftClip2,
Sinusoidal,
Foldback,
FullRectify,
HalfRectify,
SmoothRectify,
Bitcrush,
Count
};
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 // LMMS_SLEW_DISTORTION_CONTROLS_H

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

BIN
plugins/SlewDistortion/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B