/* * SlewDistortionControlDialog.cpp * * Copyright (c) 2025 Lost Robot * * 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 #include "GuiApplication.h" #include "PixmapButton.h" #include "Draggable.h" #include "lmms_math.h" #include 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(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 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(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 * 0.5f, 2.f * std::numbers::pi_v); if (modInput < 0) {modInput += 2.f * std::numbers::pi_v;} const float x = std::abs(modInput - std::numbers::pi_v) - std::numbers::pi_v * 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( "
" "Slew Distortion

" "Plugin by Lost Robot
" "GUI by thismoon
" "
" "

Overview:

" "Slew Distortion is a multiband slew rate limiter and distortion effect.

" "Slew rate limiting is something I accidentally invented while trying to make a lowpass filter for the first time.
" "In short, a slew rate limiter limits how quickly the waveform can move from one point to the next.
" "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.
" "However, the intensity of this effect depends heavily on the input signal, and with it comes a rather unique distortion of that signal.

" "In this plugin, the slew rate limiting is followed by waveshaping distortion.
" "Every distortion type is a pure waveshaping function with no filters or delays of any kind involved.
" "These distortions will generate new harmonics at exact frequency multiples of the incoming audio.

" "Because the plugin is multiband, you can apply these effects to different frequency ranges independently.
" "

Distortion Types:

" "Hard Clip - Aggressively clamps the audio signal to 0 dBFS.
" "This leaves the signal entirely untouched until it passes the clamping threshold, beyond which all content is clipped out entirely.
" "Tanh - A very gentle sigmoid distortion.
" "This waveshape is mathematically smooth and continuous at all derivatives.
" "It can be pushed significantly harder than most other distortion shapes before it starts generating harsh high frequencies.
" "Fast Soft Clip 1 - A CPU-efficient soft clipping function.
" "Fast Soft Clip 2 - A CPU-efficient cubic soft clipping function.
" "Sinusoidal - Incredibly smooth wavewrapping distortion.
" "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.
" "Foldover - A non-smooth wavewrapping alternative.
" "This leaves the audio values untouched relative to neighboring values,
" "except at the borders where the waveshape sharply changes directions, generating harsh distortion.
" "Full-wave Rectify - Flips the bottom half of the waveform to the top half.
" "The timbre of this commonly sounds similar to shifting the audio upward by one octave.
" "Unlike all the previous distortion types, this one is asymmetrical by default, meaning it will generate even-multiple harmonics.
" "Smooth Rectify - An alternative to Full-wave Rectify which has a smooth corner.
" "Half-wave Rectify - An alternative to Full-wave Rectify which clips all negative audio samples instead of reflecting them upward.
" "Bitcrush - Bit depth reduction. This distortion type is special-cased to have the Drive change its shape instead of its input amplitude.
" "

Slew:

" "This section controls the slew rate limit, the speed at which the incoming waveform's values can change.
" "Up and Down control the slew rate limit for upward and downward movement, respectively.
" "The Slew Link button locks the Slew Up and Slew Down parameters to the same value, for convenience.
" "

Dynamics:

" "This section uses an envelope follower to track the volume of the incoming audio signal.
" "Amount - Restores the dynamic range lost from the distortion and slew rate limiting by matching the output volume to the input volume.
" "Slew - Dynamically changes the slew rate, depending on the input volume.
" "Attack - How quickly the envelope follower responds to increases in volume (e.g. transients).
" "Release - How quickly the envelope follower responds to decreases in volume.
" "

Shape:

" "This section allows further sculpting of the distortion shape beyond what the distortion types can achieve on their own.
" "Warp - Causes input values smaller than this value to be unimpacted by the waveshaping.
" "The distortion shape is properly scaled and shifted to ensure it remains perfectly clean and continuous.
" "Crush - Increases the volume of audio below the Warp value.
" "This adds a sharp corner to the waveshaping function, resulting in much more aggressive distortion.
" "

Miscellaneous:

" "Mix - Blends between the wet and dry signals for the current band.
" "Since both the wet and dry signal are after the crossover filter and have oversampling applied,
" "this parameter is entirely immune to phase issues caused by blending signals.
" "Bias - Adds DC offset to the input signal before the distortion, causing the waveshaping to be asymmetrical.
" "This allows every distortion type to generate even-multiple harmonics, including the symmetrical types which usually only generate odd-multiple harmonics.
" "DC Remover - Removes DC offset (0 Hz audio) from the output signal. You'll almost always want to leave this enabled.
" "Multiband - Splits the signal into two frequency bands. If disabled, the top band's parameters are applied to the entire audio signal.
" "Split - The crossover frequency at which the Multiband mode splits the signal into two bands.
" "

Oversampling:

" "An audio signal is only capable of storing frequencies below Nyquist, which is half of the sample rate.
" "If any form of distortion generates new frequencies that are above this Nyquist frequency, they will be reflected (aliased) back downward.
" "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.
" "This aliasing is inharmonic, oftentimes sounds unpleasant, and can even contribute to auditory masking within the song.

" "Oversampling helps to resolve this issue by temporarily increasing the sample rate of the signal,
" "so significantly higher frequencies can be supported before they start aliasing back into the audible range.
" "Those higher frequencies are then filtered out before decreasing the sample rate back to its original value so they don't alias.

" "This plugin supports up to five stages of oversampling.
" "Each stage provides an extra 2 octaves of headroom before frequencies alias far enough to become audible.
" "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.
" "Even just 2x oversampling can make a massive difference and is oftentimes all you need, but up to 32x oversampling is supported.
" ); SlewDistortionHelpView::SlewDistortionHelpView() : QTextEdit(s_helpText) { setWindowTitle("Slew Distortion Help"); setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); getGUI()->mainWindow()->addWindowedWidget(this); parentWidget()->setAttribute(Qt::WA_DeleteOnClose, false); parentWidget()->setWindowIcon(QIcon(PixmapLoader("lmms-plugin-logo").pixmap())); // No maximize button Qt::WindowFlags flags = parentWidget()->windowFlags(); flags &= ~Qt::WindowMaximizeButtonHint; parentWidget()->setWindowFlags(flags); } } // namespace lmms::gui