diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 0a4686fb2..afeca3548 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -41,6 +41,7 @@ SET(LMMS_PLUGIN_LIST HydrogenImport LadspaBrowser LadspaEffect + LOMM Lv2Effect Lv2Instrument Lb302 diff --git a/data/themes/classic/lcd_11green.png b/data/themes/classic/lcd_11green.png new file mode 100644 index 000000000..32e923fe8 Binary files /dev/null and b/data/themes/classic/lcd_11green.png differ diff --git a/data/themes/classic/lcd_11green_dot.png b/data/themes/classic/lcd_11green_dot.png new file mode 100644 index 000000000..9f5a660d3 Binary files /dev/null and b/data/themes/classic/lcd_11green_dot.png differ diff --git a/data/themes/default/lcd_11green.png b/data/themes/default/lcd_11green.png new file mode 100644 index 000000000..32e923fe8 Binary files /dev/null and b/data/themes/default/lcd_11green.png differ diff --git a/data/themes/default/lcd_11green_dot.png b/data/themes/default/lcd_11green_dot.png new file mode 100644 index 000000000..9f5a660d3 Binary files /dev/null and b/data/themes/default/lcd_11green_dot.png differ diff --git a/include/Effect.h b/include/Effect.h index 1f566e0e9..f2fb6e80f 100644 --- a/include/Effect.h +++ b/include/Effect.h @@ -157,6 +157,11 @@ public: { m_noRun = _state; } + + inline TempoSyncKnobModel* autoQuitModel() + { + return &m_autoQuitModel; + } EffectChain * effectChain() const { diff --git a/plugins/LOMM/CMakeLists.txt b/plugins/LOMM/CMakeLists.txt new file mode 100644 index 000000000..dd6178f03 --- /dev/null +++ b/plugins/LOMM/CMakeLists.txt @@ -0,0 +1,3 @@ +INCLUDE(BuildPlugin) + +BUILD_PLUGIN(lomm LOMM.cpp LOMMControls.cpp LOMMControlDialog.cpp MOCFILES LOMM.h LOMMControls.h LOMMControlDialog.h EMBEDDED_RESOURCES *.png) diff --git a/plugins/LOMM/LOMM.cpp b/plugins/LOMM/LOMM.cpp new file mode 100644 index 000000000..6dc640626 --- /dev/null +++ b/plugins/LOMM/LOMM.cpp @@ -0,0 +1,444 @@ +/* + * LOMM.cpp + * + * Copyright (c) 2023 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 "LOMM.h" + +#include "embed.h" +#include "plugin_export.h" + +namespace lmms +{ + +extern "C" +{ + Plugin::Descriptor PLUGIN_EXPORT lomm_plugin_descriptor = + { + LMMS_STRINGIFY(PLUGIN_NAME), + "LOMM", + QT_TRANSLATE_NOOP("PluginBrowser", "Upwards/downwards multiband compression plugin powered by the eldritch elder god LOMMUS."), + "Lost Robot ", + 0x0100, + Plugin::Type::Effect, + new PluginPixmapLoader("logo"), + nullptr, + nullptr + }; +} + + +LOMMEffect::LOMMEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) : + Effect(&lomm_plugin_descriptor, parent, key), + m_lommControls(this), + m_sampleRate(Engine::audioEngine()->processingSampleRate()), + m_lp1(m_sampleRate), + m_lp2(m_sampleRate), + m_hp1(m_sampleRate), + m_hp2(m_sampleRate), + m_needsUpdate(true), + m_coeffPrecalc(-0.05), + m_crestTimeConst(0.999), + m_lookWrite(0), + m_lookBufLength(2) +{ + autoQuitModel()->setValue(autoQuitModel()->maxValue()); + + m_yL[0][0] = m_yL[0][1] = LOMM_MIN_FLOOR; + m_yL[1][0] = m_yL[1][1] = LOMM_MIN_FLOOR; + m_yL[2][0] = m_yL[2][1] = LOMM_MIN_FLOOR; + + connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), this, SLOT(changeSampleRate())); + emit changeSampleRate(); +} + +void LOMMEffect::changeSampleRate() +{ + m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_lp1.setSampleRate(m_sampleRate); + m_lp2.setSampleRate(m_sampleRate); + m_hp1.setSampleRate(m_sampleRate); + m_hp2.setSampleRate(m_sampleRate); + + m_coeffPrecalc = -2.2f / (m_sampleRate * 0.001f); + m_needsUpdate = true; + + m_crestTimeConst = exp(-1.f / (0.2f * m_sampleRate)); + + m_lookBufLength = std::ceil((LOMM_MAX_LOOKAHEAD / 1000.f) * m_sampleRate) + 2; + for (int i = 0; i < 2; ++i) + { + for (int j = 0; j < 3; ++j) + { + m_inLookBuf[j][i].resize(m_lookBufLength); + m_scLookBuf[j][i].resize(m_lookBufLength, LOMM_MIN_FLOOR); + } + } +} + +void LOMMEffect::clearFilterHistories() +{ + m_lp1.clearHistory(); + m_lp2.clearHistory(); + m_hp1.clearHistory(); + m_hp2.clearHistory(); +} + + + + +bool LOMMEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) +{ + if (!isEnabled() || !isRunning()) + { + return false; + } + + if (m_needsUpdate || m_lommControls.m_split1Model.isValueChanged()) + { + m_lp1.setLowpass(m_lommControls.m_split1Model.value()); + m_hp1.setHighpass(m_lommControls.m_split1Model.value()); + } + if (m_needsUpdate || m_lommControls.m_split2Model.isValueChanged()) + { + m_lp2.setLowpass(m_lommControls.m_split2Model.value()); + m_hp2.setHighpass(m_lommControls.m_split2Model.value()); + } + m_needsUpdate = false; + + float outSum = 0.f; + const float d = dryLevel(); + const float w = wetLevel(); + + const float depth = m_lommControls.m_depthModel.value(); + const float time = m_lommControls.m_timeModel.value(); + const float inVol = dbfsToAmp(m_lommControls.m_inVolModel.value()); + const float outVol = dbfsToAmp(m_lommControls.m_outVolModel.value()); + const float upward = m_lommControls.m_upwardModel.value(); + const float downward = m_lommControls.m_downwardModel.value(); + const bool split1Enabled = m_lommControls.m_split1EnabledModel.value(); + const bool split2Enabled = m_lommControls.m_split2EnabledModel.value(); + const bool band1Enabled = m_lommControls.m_band1EnabledModel.value(); + const bool band2Enabled = m_lommControls.m_band2EnabledModel.value(); + const bool band3Enabled = m_lommControls.m_band3EnabledModel.value(); + const float inHigh = dbfsToAmp(m_lommControls.m_inHighModel.value()); + const float inMid = dbfsToAmp(m_lommControls.m_inMidModel.value()); + const float inLow = dbfsToAmp(m_lommControls.m_inLowModel.value()); + float inBandVol[3] = {inHigh, inMid, inLow}; + const float outHigh = dbfsToAmp(m_lommControls.m_outHighModel.value()); + const float outMid = dbfsToAmp(m_lommControls.m_outMidModel.value()); + const float outLow = dbfsToAmp(m_lommControls.m_outLowModel.value()); + float outBandVol[3] = {outHigh, outMid, outLow}; + const float aThreshH = m_lommControls.m_aThreshHModel.value(); + const float aThreshM = m_lommControls.m_aThreshMModel.value(); + const float aThreshL = m_lommControls.m_aThreshLModel.value(); + float aThresh[3] = {aThreshH, aThreshM, aThreshL}; + const float aRatioH = m_lommControls.m_aRatioHModel.value(); + const float aRatioM = m_lommControls.m_aRatioMModel.value(); + const float aRatioL = m_lommControls.m_aRatioLModel.value(); + float aRatio[3] = {1.f / aRatioH, 1.f / aRatioM, 1.f / aRatioL}; + const float bThreshH = m_lommControls.m_bThreshHModel.value(); + const float bThreshM = m_lommControls.m_bThreshMModel.value(); + const float bThreshL = m_lommControls.m_bThreshLModel.value(); + float bThresh[3] = {bThreshH, bThreshM, bThreshL}; + const float bRatioH = m_lommControls.m_bRatioHModel.value(); + const float bRatioM = m_lommControls.m_bRatioMModel.value(); + const float bRatioL = m_lommControls.m_bRatioLModel.value(); + float bRatio[3] = {1.f / bRatioH, 1.f / bRatioM, 1.f / bRatioL}; + const float atkH = m_lommControls.m_atkHModel.value() * time; + const float atkM = m_lommControls.m_atkMModel.value() * time; + const float atkL = m_lommControls.m_atkLModel.value() * time; + const float atkCoefH = msToCoeff(atkH); + const float atkCoefM = msToCoeff(atkM); + const float atkCoefL = msToCoeff(atkL); + float atk[3] = {atkH, atkM, atkL}; + float atkCoef[3] = {atkCoefH, atkCoefM, atkCoefL}; + const float relH = m_lommControls.m_relHModel.value() * time; + const float relM = m_lommControls.m_relMModel.value() * time; + const float relL = m_lommControls.m_relLModel.value() * time; + const float relCoefH = msToCoeff(relH); + const float relCoefM = msToCoeff(relM); + const float relCoefL = msToCoeff(relL); + float rel[3] = {relH, relM, relL}; + float relCoef[3] = {relCoefH, relCoefM, relCoefL}; + const float rmsTime = m_lommControls.m_rmsTimeModel.value(); + const float rmsTimeConst = (rmsTime == 0) ? 0 : exp(-1.f / (rmsTime * 0.001f * m_sampleRate)); + const float knee = m_lommControls.m_kneeModel.value() * 0.5f; + const float range = m_lommControls.m_rangeModel.value(); + const float rangeAmp = dbfsToAmp(range); + const float balance = m_lommControls.m_balanceModel.value(); + const float balanceAmpTemp = dbfsToAmp(balance); + const float balanceAmp[2] = {1.f / balanceAmpTemp, balanceAmpTemp}; + const bool depthScaling = m_lommControls.m_depthScalingModel.value(); + const bool stereoLink = m_lommControls.m_stereoLinkModel.value(); + const float autoTime = m_lommControls.m_autoTimeModel.value() * m_lommControls.m_autoTimeModel.value(); + const float mix = m_lommControls.m_mixModel.value(); + const bool midside = m_lommControls.m_midsideModel.value(); + const bool lookaheadEnable = m_lommControls.m_lookaheadEnableModel.value(); + const int lookahead = std::ceil((m_lommControls.m_lookaheadModel.value() / 1000.f) * m_sampleRate); + const bool feedback = m_lommControls.m_feedbackModel.value() && !lookaheadEnable; + const bool lowSideUpwardSuppress = m_lommControls.m_lowSideUpwardSuppressModel.value() && midside; + + for (fpp_t f = 0; f < frames; ++f) + { + std::array s = {buf[f][0], buf[f][1]}; + + // Convert left/right to mid/side. Side channel is intentionally made + // to be 6 dB louder to bring it into volume ranges comparable to the mid channel. + if (midside) + { + float tempS0 = s[0]; + s[0] = (s[0] + s[1]) * 0.5f; + s[1] = tempS0 - s[1]; + } + + std::array, 3> bands = {{}}; + std::array, 3> bandsDry = {{}}; + + for (int i = 0; i < 2; ++i)// Channels + { + // These values are for the Auto time knob. Higher crest factor allows for faster attack/release. + float inSquared = s[i] * s[i]; + m_crestPeakVal[i] = std::max(std::max(LOMM_MIN_FLOOR, inSquared), m_crestTimeConst * m_crestPeakVal[i] + (1 - m_crestTimeConst) * (inSquared)); + m_crestRmsVal[i] = std::max(LOMM_MIN_FLOOR, m_crestTimeConst * m_crestRmsVal[i] + ((1 - m_crestTimeConst) * (inSquared))); + m_crestFactorVal[i] = m_crestPeakVal[i] / m_crestRmsVal[i]; + float crestFactorValTemp = ((m_crestFactorVal[i] - LOMM_AUTO_TIME_ADJUST) * autoTime) + LOMM_AUTO_TIME_ADJUST; + + // Crossover filters + bands[0][i] = m_hp1.update(s[i], i); + bands[1][i] = m_hp2.update(m_lp1.update(s[i], i), i); + bands[2][i] = m_lp2.update(s[i], i); + + if (!split1Enabled) + { + bands[1][i] += bands[0][i]; + bands[0][i] = 0; + } + if (!split2Enabled) + { + bands[1][i] += bands[2][i]; + bands[2][i] = 0; + } + + // Mute disabled bands + bands[0][i] *= band1Enabled; + bands[1][i] *= band2Enabled; + bands[2][i] *= band3Enabled; + + std::array detect = {0, 0, 0}; + for (int j = 0; j < 3; ++j)// Bands + { + bandsDry[j][i] = bands[j][i]; + + if (feedback && !lookaheadEnable) + { + bands[j][i] = m_prevOut[j][i]; + } + + bands[j][i] *= inBandVol[j] * inVol * balanceAmp[i]; + + if (rmsTime > 0)// RMS + { + m_rms[j][i] = rmsTimeConst * m_rms[j][i] + ((1 - rmsTimeConst) * (bands[j][i] * bands[j][i])); + detect[j] = std::max(LOMM_MIN_FLOOR, std::sqrt(m_rms[j][i])); + } + else// Peak + { + detect[j] = std::max(LOMM_MIN_FLOOR, std::abs(bands[j][i])); + } + + if (detect[j] > m_yL[j][i])// Attack phase + { + // Calculate attack value depending on crest factor + const float currentAttack = autoTime + ? msToCoeff(LOMM_AUTO_TIME_ADJUST * atk[j] / crestFactorValTemp) + : atkCoef[j]; + + m_yL[j][i] = m_yL[j][i] * currentAttack + (1 - currentAttack) * detect[j]; + } + else// Release phase + { + // Calculate release value depending on crest factor + const float currentRelease = autoTime + ? msToCoeff(LOMM_AUTO_TIME_ADJUST * rel[j] / crestFactorValTemp) + : relCoef[j]; + + m_yL[j][i] = m_yL[j][i] * currentRelease + (1 - currentRelease) * detect[j]; + } + + m_yL[j][i] = std::max(LOMM_MIN_FLOOR, m_yL[j][i]); + + float yAmp = m_yL[j][i]; + if (lookaheadEnable) + { + float temp = yAmp; + // Lookahead is calculated by picking the largest value between + // the current sidechain signal and the delayed sidechain signal. + yAmp = std::max(m_scLookBuf[j][i][m_lookWrite], m_scLookBuf[j][i][(m_lookWrite + m_lookBufLength - lookahead) % m_lookBufLength]); + m_scLookBuf[j][i][m_lookWrite] = temp; + } + + const float yDbfs = ampToDbfs(yAmp); + + float aboveGain = 0; + float belowGain = 0; + + // Downward compression + if (yDbfs - aThresh[j] < -knee)// Below knee + { + aboveGain = yDbfs; + } + else if (yDbfs - aThresh[j] < knee)// Within knee + { + const float temp = yDbfs - aThresh[j] + knee; + aboveGain = yDbfs + (aRatio[j] - 1) * temp * temp / (4 * knee); + } + else// Above knee + { + aboveGain = aThresh[j] + (yDbfs - aThresh[j]) * aRatio[j]; + } + if (aboveGain < yDbfs) + { + if (downward * depth <= 1) + { + aboveGain = linearInterpolate(yDbfs, aboveGain, downward * depth); + } + else + { + aboveGain = linearInterpolate(aboveGain, aThresh[j], downward * depth - 1); + } + } + + // Upward compression + if (yDbfs - bThresh[j] > knee)// Above knee + { + belowGain = yDbfs; + } + else if (bThresh[j] - yDbfs < knee)// Within knee + { + const float temp = bThresh[j] - yDbfs + knee; + belowGain = yDbfs + (1 - bRatio[j]) * temp * temp / (4 * knee); + } + else// Below knee + { + belowGain = bThresh[j] + (yDbfs - bThresh[j]) * bRatio[j]; + } + if (belowGain > yDbfs) + { + if (upward * depth <= 1) + { + belowGain = linearInterpolate(yDbfs, belowGain, upward * depth); + } + else + { + belowGain = linearInterpolate(belowGain, bThresh[j], upward * depth - 1); + } + } + + m_displayIn[j][i] = yDbfs; + m_gainResult[j][i] = (dbfsToAmp(aboveGain) / yAmp) * (dbfsToAmp(belowGain) / yAmp); + if (lowSideUpwardSuppress && m_gainResult[j][i] > 1 && j == 2 && i == 1) //undo upward compression if low side band + { + m_gainResult[j][i] = 1; + } + m_gainResult[j][i] = std::min(m_gainResult[j][i], rangeAmp); + m_displayOut[j][i] = ampToDbfs(std::max(LOMM_MIN_FLOOR, yAmp * m_gainResult[j][i])); + + // Apply the same gain reduction to both channels if stereo link is enabled. + if (stereoLink && i == 1) + { + if (m_gainResult[j][1] < m_gainResult[j][0]) + { + m_gainResult[j][0] = m_gainResult[j][1]; + m_displayOut[j][0] = m_displayIn[j][0] - (m_displayIn[j][1] - m_displayOut[j][1]); + } + else + { + m_gainResult[j][1] = m_gainResult[j][0]; + m_displayOut[j][1] = m_displayIn[j][1] - (m_displayIn[j][0] - m_displayOut[j][0]); + } + } + } + } + + for (int i = 0; i < 2; ++i)// Channels + { + for (int j = 0; j < 3; ++j)// Bands + { + if (lookaheadEnable) + { + float temp = bands[j][i]; + bands[j][i] = m_inLookBuf[j][i][m_lookWrite]; + m_inLookBuf[j][i][m_lookWrite] = temp; + bandsDry[j][i] = bands[j][i]; + } + else if (feedback) + { + bands[j][i] = bandsDry[j][i] * inBandVol[j] * inVol * balanceAmp[i]; + } + + // Apply gain reduction + bands[j][i] *= m_gainResult[j][i]; + + // Store for Feedback + m_prevOut[j][i] = bands[j][i]; + + bands[j][i] *= outBandVol[j]; + + bands[j][i] = linearInterpolate(bandsDry[j][i], bands[j][i], mix); + } + + s[i] = bands[0][i] + bands[1][i] + bands[2][i]; + + s[i] *= linearInterpolate(1.f, outVol, mix * (depthScaling ? depth : 1)); + } + + // Convert mid/side back to left/right. + // Note that the side channel was intentionally made to be 6 dB louder prior to compression. + if (midside) + { + float tempS0 = s[0]; + s[0] = s[0] + (s[1] * 0.5f); + s[1] = tempS0 - (s[1] * 0.5f); + } + + if (--m_lookWrite < 0) { m_lookWrite = m_lookBufLength - 1; } + + buf[f][0] = d * buf[f][0] + w * s[0]; + buf[f][1] = d * buf[f][1] + w * s[1]; + outSum += buf[f][0] + buf[f][1]; + } + + checkGate(outSum / frames); + return isRunning(); +} + +extern "C" +{ + // necessary for getting instance out of shared lib + PLUGIN_EXPORT Plugin * lmms_plugin_main(Model* parent, void* data) + { + return new LOMMEffect(parent, static_cast(data)); + } +} + +} // namespace lmms diff --git a/plugins/LOMM/LOMM.h b/plugins/LOMM/LOMM.h new file mode 100644 index 000000000..039f80b6a --- /dev/null +++ b/plugins/LOMM/LOMM.h @@ -0,0 +1,106 @@ +/* + * LOMM.h + * + * Copyright (c) 2023 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. + * + */ + + +#ifndef LMMS_LOMM_H +#define LMMS_LOMM_H + +#include "LOMMControls.h" +#include "Effect.h" + +#include "BasicFilters.h" +#include "lmms_math.h" + +namespace lmms +{ + +constexpr inline float LOMM_MIN_FLOOR = 0.00012589;// -72 dBFS +constexpr inline float LOMM_MAX_LOOKAHEAD = 20.f; +constexpr inline float LOMM_AUTO_TIME_ADJUST = 5.f; + +class LOMMEffect : public Effect +{ + Q_OBJECT +public: + LOMMEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key); + ~LOMMEffect() override = default; + bool processAudioBuffer(sampleFrame* buf, const fpp_t frames) override; + + EffectControls* controls() override + { + return &m_lommControls; + } + + void clearFilterHistories(); + + inline float msToCoeff(float ms) + { + return (ms == 0) ? 0 : exp(m_coeffPrecalc / ms); + } + +private slots: + void changeSampleRate(); + +private: + LOMMControls m_lommControls; + + float m_sampleRate; + + StereoLinkwitzRiley m_lp1; + StereoLinkwitzRiley m_lp2; + + StereoLinkwitzRiley m_hp1; + StereoLinkwitzRiley m_hp2; + + bool m_needsUpdate; + float m_coeffPrecalc; + + std::array, 3> m_yL; + std::array, 3> m_rms; + std::array, 3> m_gainResult; + + std::array, 3> m_displayIn; + std::array, 3> m_displayOut; + + std::array m_crestPeakVal; + std::array m_crestRmsVal; + std::array m_crestFactorVal; + float m_crestTimeConst = 0.0f; + + std::array, 3> m_prevOut; + + std::array, 2>, 3> m_inLookBuf; + std::array, 2>, 3> m_scLookBuf; + + int m_lookWrite = 0; + int m_lookBufLength = 0; + + friend class LOMMControls; + friend class gui::LOMMControlDialog; +}; + + +} // namespace lmms + +#endif // LMMS_LOMM_H diff --git a/plugins/LOMM/LOMMControlDialog.cpp b/plugins/LOMM/LOMMControlDialog.cpp new file mode 100644 index 000000000..e53987a05 --- /dev/null +++ b/plugins/LOMM/LOMMControlDialog.cpp @@ -0,0 +1,275 @@ +/* + * LOMMControlDialog.cpp + * + * Copyright (c) 2023 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 "LOMM.h" +#include "LOMMControlDialog.h" +#include "LOMMControls.h" + + +namespace lmms::gui +{ + +LOMMControlDialog::LOMMControlDialog(LOMMControls* controls) : + EffectControlDialog(controls), + m_controls(controls) +{ + setAutoFillBackground(true); + QPalette pal; + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); + setPalette(pal); + setFixedSize(400, 256); + + createKnob(KnobType::Bright26, this, 10, 4, &controls->m_depthModel, tr("Depth:"), "", tr("Compression amount for all bands")); + createKnob(KnobType::Bright26, this, 10, 41, &controls->m_timeModel, tr("Time:"), "", tr("Attack/release scaling for all bands")); + createKnob(KnobType::Bright26, this, 10, 220, &controls->m_inVolModel, tr("Input Volume:"), " dB", tr("Input volume")); + createKnob(KnobType::Bright26, this, 363, 220, &controls->m_outVolModel, tr("Output Volume:"), " dB", tr("Output volume")); + createKnob(KnobType::Bright26, this, 10, 179, &controls->m_upwardModel, tr("Upward Depth:"), "", tr("Upward compression amount for all bands")); + createKnob(KnobType::Bright26, this, 363, 179, &controls->m_downwardModel, tr("Downward Depth:"), "", tr("Downward compression amount for all bands")); + + createLcdFloatSpinBox(5, 2, "11green", tr("High/Mid Crossover"), this, 352, 76, &controls->m_split1Model, tr("High/Mid Crossover")); + createLcdFloatSpinBox(5, 2, "11green", tr("Mid/Low Crossover"), this, 352, 156, &controls->m_split2Model, tr("Mid/Low Crossover")); + + createPixmapButton(tr("High/mid band split"), this, 369, 104, &controls->m_split1EnabledModel, "crossover_led_green", "crossover_led_off", tr("High/mid band split")); + createPixmapButton(tr("Mid/low band split"), this, 369, 126, &controls->m_split2EnabledModel, "crossover_led_green", "crossover_led_off", tr("Mid/low band split")); + + createPixmapButton(tr("Enable High Band"), this, 143, 66, &controls->m_band1EnabledModel, "high_band_active", "high_band_inactive", tr("Enable High Band")); + createPixmapButton(tr("Enable Mid Band"), this, 143, 146, &controls->m_band2EnabledModel, "mid_band_active", "mid_band_inactive", tr("Enable Mid Band")); + createPixmapButton(tr("Enable Low Band"), this, 143, 226, &controls->m_band3EnabledModel, "low_band_active", "low_band_inactive", tr("Enable Low Band")); + + createKnob(KnobType::Bright26, this, 53, 43, &controls->m_inHighModel, tr("High Input Volume:"), " dB", tr("Input volume for high band")); + createKnob(KnobType::Bright26, this, 53, 123, &controls->m_inMidModel, tr("Mid Input Volume:"), " dB", tr("Input volume for mid band")); + createKnob(KnobType::Bright26, this, 53, 203, &controls->m_inLowModel, tr("Low Input Volume:"), " dB", tr("Input volume for low band")); + createKnob(KnobType::Bright26, this, 320, 43, &controls->m_outHighModel, tr("High Output Volume:"), " dB", tr("Output volume for high band")); + createKnob(KnobType::Bright26, this, 320, 123, &controls->m_outMidModel, tr("Mid Output Volume:"), " dB", tr("Output volume for mid band")); + createKnob(KnobType::Bright26, this, 320, 203, &controls->m_outLowModel, tr("Low Output Volume:"), " dB", tr("Output volume for low band")); + + createLcdFloatSpinBox(3, 3, "11green", tr("Above Threshold High"), this, 300, 13, &controls->m_aThreshHModel, tr("Downward compression threshold for high band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Above Threshold Mid"), this, 300, 93, &controls->m_aThreshMModel, tr("Downward compression threshold for mid band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Above Threshold Low"), this, 300, 173, &controls->m_aThreshLModel, tr("Downward compression threshold for low band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Above Ratio High"), this, 284, 44, &controls->m_aRatioHModel, tr("Downward compression ratio for high band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Above Ratio Mid"), this, 284, 124, &controls->m_aRatioMModel, tr("Downward compression ratio for mid band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Above Ratio Low"), this, 284, 204, &controls->m_aRatioLModel, tr("Downward compression ratio for low band")); + + createLcdFloatSpinBox(3, 3, "11green", tr("Below Threshold High"), this, 59, 13, &controls->m_bThreshHModel, tr("Upward compression threshold for high band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Below Threshold Mid"), this, 59, 93, &controls->m_bThreshMModel, tr("Upward compression threshold for mid band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Below Threshold Low"), this, 59, 173, &controls->m_bThreshLModel, tr("Upward compression threshold for low band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Below Ratio High"), this, 87, 44, &controls->m_bRatioHModel, tr("Upward compression ratio for high band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Below Ratio Mid"), this, 87, 124, &controls->m_bRatioMModel, tr("Upward compression ratio for mid band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Below Ratio Low"), this, 87, 204, &controls->m_bRatioLModel, tr("Upward compression ratio for low band")); + + createKnob(KnobType::Small17, this, 120, 61, &controls->m_atkHModel, tr("Attack High:"), " ms", tr("Attack time for high band")); + createKnob(KnobType::Small17, this, 120, 141, &controls->m_atkMModel, tr("Attack Mid:"), " ms", tr("Attack time for mid band")); + createKnob(KnobType::Small17, this, 120, 221, &controls->m_atkLModel, tr("Attack Low:"), " ms", tr("Attack time for low band")); + createKnob(KnobType::Small17, this, 261, 61, &controls->m_relHModel, tr("Release High:"), " ms", tr("Release time for high band")); + createKnob(KnobType::Small17, this, 261, 141, &controls->m_relMModel, tr("Release Mid:"), " ms", tr("Release time for mid band")); + createKnob(KnobType::Small17, this, 261, 221, &controls->m_relLModel, tr("Release Low:"), " ms", tr("Release time for low band")); + + createKnob(KnobType::Small17, this, 380, 42, &controls->m_rmsTimeModel, tr("RMS Time:"), " ms", tr("RMS size for sidechain signal (set to 0 for Peak mode)")); + createKnob(KnobType::Small17, this, 356, 42, &controls->m_kneeModel, tr("Knee:"), " dB", tr("Knee size for all compressors")); + createKnob(KnobType::Small17, this, 24, 146, &controls->m_rangeModel, tr("Range:"), " dB", tr("Maximum gain increase for all bands")); + createKnob(KnobType::Small17, this, 13, 114, &controls->m_balanceModel, tr("Balance:"), " dB", tr("Bias input volume towards one channel")); + + createPixmapButton(tr("Scale output volume with Depth"), this, 51, 0, &controls->m_depthScalingModel, "depthScaling_active", "depthScaling_inactive", + tr("Scale output volume with Depth parameter")); + createPixmapButton(tr("Stereo Link"), this, 52, 237, &controls->m_stereoLinkModel, "stereoLink_active", "stereoLink_inactive", + tr("Apply same gain change to both channels")); + + createKnob(KnobType::Small17, this, 24, 80, &controls->m_autoTimeModel, tr("Auto Time:"), "", tr("Speed up attack and release times when transients occur")); + createKnob(KnobType::Bright26, this, 363, 4, &controls->m_mixModel, tr("Mix:"), "", tr("Wet/Dry of all bands")); + + m_feedbackButton = createPixmapButton(tr("Feedback"), this, 317, 238, &controls->m_feedbackModel, "feedback_active", "feedback_inactive", + tr("Use output as sidechain signal instead of input")); + createPixmapButton(tr("Mid/Side"), this, 285, 238, &controls->m_midsideModel, "midside_active", "midside_inactive", tr("Compress mid/side channels instead of left/right")); + m_lowSideUpwardSuppressButton = createPixmapButton(tr("Suppress upward compression for side band"), this, 106, 180, &controls->m_lowSideUpwardSuppressModel, + "lowSideUpwardSuppress_active", "lowSideUpwardSuppress_inactive", tr("Suppress upward compression for side band")); + createPixmapButton(tr("Lookahead"), this, 147, 0, &controls->m_lookaheadEnableModel, "lookahead_active", "lookahead_inactive", + tr(("Enable lookahead with fixed " + std::to_string(int(LOMM_MAX_LOOKAHEAD)) + " ms latency").c_str())); + createLcdFloatSpinBox(2, 2, "11green", tr("Lookahead"), this, 214, 2, &controls->m_lookaheadModel, tr("Lookahead length")); + + PixmapButton* initButton = createPixmapButton(tr("Clear all parameters"), this, 84, 237, nullptr, "init_active", "init_inactive", tr("Clear all parameters")); + + connect(initButton, SIGNAL(clicked()), m_controls, SLOT(resetAllParameters())); + connect(&controls->m_lookaheadEnableModel, SIGNAL(dataChanged()), this, SLOT(updateFeedbackVisibility())); + connect(&controls->m_midsideModel, SIGNAL(dataChanged()), this, SLOT(updateLowSideUpwardSuppressVisibility())); + connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(updateDisplay())); + + emit updateFeedbackVisibility(); + emit updateLowSideUpwardSuppressVisibility(); +} + +void LOMMControlDialog::updateFeedbackVisibility() +{ + m_feedbackButton->setVisible(!m_controls->m_lookaheadEnableModel.value()); +} + +void LOMMControlDialog::updateLowSideUpwardSuppressVisibility() +{ + m_lowSideUpwardSuppressButton->setVisible(m_controls->m_midsideModel.value()); +} + +void LOMMControlDialog::updateDisplay() +{ + update(); +} + +void LOMMControlDialog::paintEvent(QPaintEvent *event) +{ + if (!isVisible()) { return; } + + QPainter p; + p.begin(this); + + // Draw threshold lines + QColor aColor(255, 255, 0, 31); + QColor bColor(255, 0, 0, 31); + QPen aPen(QColor(255, 255, 0, 255), 1); + QPen bPen(QColor(255, 0, 0, 255), 1); + int thresholdsX[] = {dbfsToX(m_controls->m_aThreshHModel.value()), + dbfsToX(m_controls->m_aThreshMModel.value()), + dbfsToX(m_controls->m_aThreshLModel.value()), + dbfsToX(m_controls->m_bThreshHModel.value()), + dbfsToX(m_controls->m_bThreshMModel.value()), + dbfsToX(m_controls->m_bThreshLModel.value())}; + for (int i = 0; i < 3; ++i) { + p.setPen(aPen); + p.fillRect(thresholdsX[i], LOMM_DISPLAY_Y[2 * i], LOMM_DISPLAY_X + LOMM_DISPLAY_WIDTH - thresholdsX[i], LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT - LOMM_DISPLAY_Y[2 * i], aColor); + p.drawLine(thresholdsX[i], LOMM_DISPLAY_Y[2 * i], thresholdsX[i], LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT); + + p.setPen(bPen); + p.fillRect(LOMM_DISPLAY_X, LOMM_DISPLAY_Y[2 * i], thresholdsX[i + 3] - LOMM_DISPLAY_X, LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT - LOMM_DISPLAY_Y[2 * i], bColor); + p.drawLine(thresholdsX[i + 3], LOMM_DISPLAY_Y[2 * i], thresholdsX[i + 3], LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT); + } + + QPen inputPen(QColor(200, 200, 200, 80), 1); + QPen outputPen(QColor(255, 255, 255, 255), 1); + for (int i = 0; i < 3; ++i) { + // Draw input lines + p.setPen(inputPen); + int inL = dbfsToX(m_controls->m_effect->m_displayIn[i][0]); + p.drawLine(inL, LOMM_DISPLAY_Y[2 * i] + 4, inL, LOMM_DISPLAY_Y[2 * i] + LOMM_DISPLAY_HEIGHT); + int inR = dbfsToX(m_controls->m_effect->m_displayIn[i][1]); + p.drawLine(inR, LOMM_DISPLAY_Y[2 * i + 1], inR, LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT - 4); + + // Draw output lines + p.setPen(outputPen); + int outL = dbfsToX(m_controls->m_effect->m_displayOut[i][0]); + p.drawLine(outL, LOMM_DISPLAY_Y[2 * i], outL, LOMM_DISPLAY_Y[2 * i] + LOMM_DISPLAY_HEIGHT); + int outR = dbfsToX(m_controls->m_effect->m_displayOut[i][1]); + p.drawLine(outR, LOMM_DISPLAY_Y[2 * i + 1], outR, LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT); + } + + p.end(); +} + +int LOMMControlDialog::dbfsToX(float dbfs) +{ + float returnX = (dbfs - LOMM_DISPLAY_MIN) / (LOMM_DISPLAY_MAX - LOMM_DISPLAY_MIN); + returnX = qBound(LOMM_DISPLAY_X, LOMM_DISPLAY_X + returnX * LOMM_DISPLAY_WIDTH, LOMM_DISPLAY_X + LOMM_DISPLAY_WIDTH); + return returnX; +} + +float LOMMControlDialog::xToDbfs(int x) +{ + float xNorm = static_cast(x - LOMM_DISPLAY_X) / LOMM_DISPLAY_WIDTH; + float dbfs = xNorm * (LOMM_DISPLAY_MAX - LOMM_DISPLAY_MIN) + LOMM_DISPLAY_MIN; + return dbfs; +} + +void LOMMControlDialog::mousePressEvent(QMouseEvent* event) +{ + if ((event->button() == Qt::LeftButton || event->button() == Qt::MiddleButton) && !(event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier))) + { + const QPoint& p = event->pos(); + + if (LOMM_DISPLAY_X - 10 <= p.x() && p.x() <= LOMM_DISPLAY_X + LOMM_DISPLAY_WIDTH + 10) + { + FloatModel* aThresh[] = {&m_controls->m_aThreshHModel, &m_controls->m_aThreshMModel, &m_controls->m_aThreshLModel}; + FloatModel* bThresh[] = {&m_controls->m_bThreshHModel, &m_controls->m_bThreshMModel, &m_controls->m_bThreshLModel}; + + for (int i = 0; i < 3; ++i) + { + if (LOMM_DISPLAY_Y[i * 2] <= p.y() && p.y() <= LOMM_DISPLAY_Y[i * 2 + 1] + LOMM_DISPLAY_HEIGHT) + { + int behavior = (p.x() < dbfsToX(bThresh[i]->value())) ? 0 : (p.x() > dbfsToX(aThresh[i]->value())) ? 1 : 2; + if (event->button() == Qt::MiddleButton) + { + if (behavior == 0 || behavior == 2) {bThresh[i]->reset();} + if (behavior == 1 || behavior == 2) {aThresh[i]->reset();} + return; + } + + m_bandDrag = i; + m_lastMousePos = p; + m_buttonPressed = true; + + m_dragType = behavior; + return; + } + } + } + } +} + +void LOMMControlDialog::mouseMoveEvent(QMouseEvent * event) +{ + if (m_buttonPressed && event->pos() != m_lastMousePos) + { + const float distance = event->pos().x() - m_lastMousePos.x(); + float dbDistance = distance * LOMM_DISPLAY_DB_PER_PIXEL; + m_lastMousePos = event->pos(); + + FloatModel* aModel[] = {&m_controls->m_aThreshHModel, &m_controls->m_aThreshMModel, &m_controls->m_aThreshLModel}; + FloatModel* bModel[] = {&m_controls->m_bThreshHModel, &m_controls->m_bThreshMModel, &m_controls->m_bThreshLModel}; + + float bVal = bModel[m_bandDrag]->value(); + float aVal = aModel[m_bandDrag]->value(); + if (m_dragType == 0) + { + bModel[m_bandDrag]->setValue(bVal + dbDistance); + } + else if (m_dragType == 1) + { + aModel[m_bandDrag]->setValue(aVal + dbDistance); + } + else + { + dbDistance = qBound(bModel[m_bandDrag]->minValue(), bVal + dbDistance, bModel[m_bandDrag]->maxValue()) - bVal; + dbDistance = qBound(aModel[m_bandDrag]->minValue(), aVal + dbDistance, aModel[m_bandDrag]->maxValue()) - aVal; + bModel[m_bandDrag]->setValue(bVal + dbDistance); + aModel[m_bandDrag]->setValue(aVal + dbDistance); + } + } +} + +void LOMMControlDialog::mouseReleaseEvent(QMouseEvent* event) +{ + if (event && event->button() == Qt::LeftButton) + { + m_buttonPressed = false; + } +} + + +} // namespace lmms::gui diff --git a/plugins/LOMM/LOMMControlDialog.h b/plugins/LOMM/LOMMControlDialog.h new file mode 100644 index 000000000..bf7e67c4c --- /dev/null +++ b/plugins/LOMM/LOMMControlDialog.h @@ -0,0 +1,129 @@ +/* + * LOMMControlDialog.h + * + * Copyright (c) 2023 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. + * + */ + +#ifndef LMMS_GUI_LOMM_CONTROL_DIALOG_H +#define LMMS_GUI_LOMM_CONTROL_DIALOG_H + +#include "EffectControlDialog.h" + +#include +#include + +#include "embed.h" +#include "GuiApplication.h" +#include "Knob.h" +#include "LcdFloatSpinBox.h" +#include "LcdSpinBox.h" +#include "LedCheckBox.h" +#include "MainWindow.h" +#include "PixmapButton.h" + +namespace lmms +{ + +inline constexpr float LOMM_DISPLAY_MIN = -72; +inline constexpr float LOMM_DISPLAY_MAX = 0; +inline constexpr float LOMM_DISPLAY_X = 125; +inline constexpr float LOMM_DISPLAY_Y[6] = {24, 41, 106, 123, 186, 203}; +inline constexpr float LOMM_DISPLAY_WIDTH = 150; +inline constexpr float LOMM_DISPLAY_HEIGHT = 13; +inline constexpr float LOMM_DISPLAY_DB_PER_PIXEL = (LOMM_DISPLAY_MAX - LOMM_DISPLAY_MIN) / LOMM_DISPLAY_WIDTH; + +class LOMMControls; + + +namespace gui +{ + +class LOMMControlDialog : public EffectControlDialog +{ + Q_OBJECT +public: + LOMMControlDialog(LOMMControls* controls); + ~LOMMControlDialog() override = default; + + int dbfsToX(float dbfs); + float xToDbfs(int x); + + Knob* createKnob(KnobType knobType, QWidget* parent, int x, int y, FloatModel* model, const QString& hintText, const QString& unit, const QString& toolTip) + { + Knob* knob = new Knob(knobType, parent); + knob->move(x, y); + knob->setModel(model); + knob->setHintText(hintText, unit); + knob->setToolTip(toolTip); + return knob; + } + + LcdFloatSpinBox* createLcdFloatSpinBox(int integerDigits, int decimalDigits, const QString& color, const QString& unit, QWidget* parent, int x, int y, FloatModel* model, const QString& toolTip) + { + LcdFloatSpinBox* spinBox = new LcdFloatSpinBox(integerDigits, decimalDigits, color, unit, parent); + spinBox->move(x, y); + spinBox->setModel(model); + spinBox->setSeamless(true, true); + spinBox->setToolTip(toolTip); + return spinBox; + } + + PixmapButton* createPixmapButton(const QString& text, QWidget* parent, int x, int y, BoolModel* model, const QString& activeIcon, const QString& inactiveIcon, const QString& tooltip) + { + PixmapButton* button = new PixmapButton(parent, text); + button->move(x, y); + button->setCheckable(true); + if (model) { button->setModel(model); } + button->setActiveGraphic(PLUGIN_NAME::getIconPixmap(activeIcon)); + button->setInactiveGraphic(PLUGIN_NAME::getIconPixmap(inactiveIcon)); + button->setToolTip(tooltip); + return button; + } + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + +private: + LOMMControls* m_controls; + + QPoint m_lastMousePos; + bool m_buttonPressed = false; + int m_bandDrag = 0; + int m_dragType = -1; + + PixmapButton* m_feedbackButton; + PixmapButton* m_lowSideUpwardSuppressButton; + +private slots: + void updateFeedbackVisibility(); + void updateLowSideUpwardSuppressVisibility(); + void updateDisplay(); +}; + + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_LOMM_CONTROL_DIALOG_H diff --git a/plugins/LOMM/LOMMControls.cpp b/plugins/LOMM/LOMMControls.cpp new file mode 100644 index 000000000..d695cf483 --- /dev/null +++ b/plugins/LOMM/LOMMControls.cpp @@ -0,0 +1,277 @@ +/* + * LOMMControls.cpp + * + * Copyright (c) 2023 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 "LOMMControls.h" +#include "LOMM.h" + +#include +#include + +namespace lmms +{ + +LOMMControls::LOMMControls(LOMMEffect* effect) : + EffectControls(effect), + m_effect(effect), + m_depthModel(0.4, 0, 1, 0.00001, this, tr("Depth")), + m_timeModel(1, 0, 10, 0.00001, this, tr("Time")), + m_inVolModel(0, -48, 48, 0.00001, this, tr("Input Volume")), + m_outVolModel(8, -48, 48, 0.00001, this, tr("Output Volume")), + m_upwardModel(1, 0, 2, 0.00001, this, tr("Upward Depth")), + m_downwardModel(1, 0, 2, 0.00001, this, tr("Downward Depth")), + m_split1Model(2500, 20, 20000, 0.01, this, tr("High/Mid Split")), + m_split2Model(88.3, 20, 20000, 0.01, this, tr("Mid/Low Split")), + m_split1EnabledModel(true, this, tr("Enable High/Mid Split")), + m_split2EnabledModel(true, this, tr("Enable Mid/Low Split")), + m_band1EnabledModel(true, this, tr("Enable High Band")), + m_band2EnabledModel(true, this, tr("Enable Mid Band")), + m_band3EnabledModel(true, this, tr("Enable Low Band")), + m_inHighModel(0, -48, 48, 0.00001, this, tr("High Input Volume")), + m_inMidModel(0, -48, 48, 0.00001, this, tr("Mid Input Volume")), + m_inLowModel(0, -48, 48, 0.00001, this, tr("Low Input Volume")), + m_outHighModel(4.6, -48, 48, 0.00001, this, tr("High Output Volume")), + m_outMidModel(0.0, -48, 48, 0.00001, this, tr("Mid Output Volume")), + m_outLowModel(4.6, -48, 48, 0.00001, this, tr("Low Output Volume")), + m_aThreshHModel(-30.3, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Above Threshold High")), + m_aThreshMModel(-25.0, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Above Threshold Mid")), + m_aThreshLModel(-28.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Above Threshold Low")), + m_aRatioHModel(99.99, 1, 99.99, 0.01, this, tr("Above Ratio High")), + m_aRatioMModel(66.7, 1, 99.99, 0.01, this, tr("Above Ratio Mid")), + m_aRatioLModel(66.7, 1, 99.99, 0.01, this, tr("Above Ratio Low")), + m_bThreshHModel(-35.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Below Threshold High")), + m_bThreshMModel(-36.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Below Threshold Mid")), + m_bThreshLModel(-35.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Below Threshold Low")), + m_bRatioHModel(4.17, 1, 99.99, 0.01, this, tr("Below Ratio High")), + m_bRatioMModel(4.17, 1, 99.99, 0.01, this, tr("Below Ratio Mid")), + m_bRatioLModel(4.17, 1, 99.99, 0.01, this, tr("Below Ratio Low")), + m_atkHModel(13.5, 0, 1000, 0.001, this, tr("Attack High")), + m_atkMModel(22.4, 0, 1000, 0.001, this, tr("Attack Mid")), + m_atkLModel(47.8, 0, 1000, 0.001, this, tr("Attack Low")), + m_relHModel(132, 0, 1000, 0.001, this, tr("Release High")), + m_relMModel(282, 0, 1000, 0.001, this, tr("Release Mid")), + m_relLModel(282, 0, 1000, 0.001, this, tr("Release Low")), + m_rmsTimeModel(10, 0, 500, 0.001, this, tr("RMS Time")), + m_kneeModel(6, 0, 36, 0.00001, this, tr("Knee")), + m_rangeModel(36, 0, 96, 0.00001, this, tr("Range")), + m_balanceModel(0, -18, 18, 0.00001, this, tr("Balance")), + m_depthScalingModel(true, this, tr("Scale output volume with Depth")), + m_stereoLinkModel(false, this, tr("Stereo Link")), + m_autoTimeModel(0, 0, 1, 0.00001, this, tr("Auto Time")), + m_mixModel(1, 0, 1, 0.00001, this, tr("Mix")), + m_feedbackModel(false, this, tr("Feedback")), + m_midsideModel(false, this, tr("Mid/Side")), + m_lookaheadEnableModel(false, this, tr("Lookahead")), + m_lookaheadModel(0.f, 0.f, LOMM_MAX_LOOKAHEAD, 0.01, this, tr("Lookahead Length")), + m_lowSideUpwardSuppressModel(false, this, tr("Suppress upward compression for side band")) +{ + auto models = {&m_timeModel, &m_inVolModel, &m_outVolModel, &m_inHighModel, &m_inMidModel, + &m_inLowModel, &m_outHighModel, &m_outMidModel, &m_outLowModel, &m_aRatioHModel, + &m_aRatioMModel, &m_aRatioLModel, &m_bRatioHModel, &m_bRatioMModel, &m_bRatioLModel, + &m_atkHModel, &m_atkMModel, &m_atkLModel, &m_relHModel, &m_relMModel, &m_relLModel, + &m_rmsTimeModel, &m_balanceModel}; + for (auto model : models) { model->setScaleLogarithmic(true); } +} + + +void LOMMControls::resetAllParameters() +{ + int choice = QMessageBox::question(m_view, "Clear Plugin Settings", "Are you sure you want to clear all parameters?\n(This wipes LOMM to a clean slate, not the default preset.)", QMessageBox::Yes | QMessageBox::No); + if (choice != QMessageBox::Yes) { return; } + + // give the user a chance to beg LMMS for forgiveness + addJournalCheckPoint(); + + // This plugin's normal default values are fairly close to what they'd want in most applications. + // The Init button is there so the user can start from a clean slate instead. + // These are those values. + setInitAndReset(m_depthModel, 1); + setInitAndReset(m_timeModel, 1); + setInitAndReset(m_inVolModel, 0); + setInitAndReset(m_outVolModel, 0); + setInitAndReset(m_upwardModel, 1); + setInitAndReset(m_downwardModel, 1); + setInitAndReset(m_split1Model, 2500); + setInitAndReset(m_split2Model, 88); + setInitAndReset(m_split1EnabledModel, true); + setInitAndReset(m_split2EnabledModel, true); + setInitAndReset(m_band1EnabledModel, true); + setInitAndReset(m_band2EnabledModel, true); + setInitAndReset(m_band3EnabledModel, true); + setInitAndReset(m_inHighModel, 0); + setInitAndReset(m_inMidModel, 0); + setInitAndReset(m_inLowModel, 0); + setInitAndReset(m_outHighModel, 0); + setInitAndReset(m_outMidModel, 0); + setInitAndReset(m_outLowModel, 0); + setInitAndReset(m_aThreshHModel, m_aThreshHModel.maxValue()); + setInitAndReset(m_aThreshMModel, m_aThreshMModel.maxValue()); + setInitAndReset(m_aThreshLModel, m_aThreshLModel.maxValue()); + setInitAndReset(m_aRatioHModel, 1); + setInitAndReset(m_aRatioMModel, 1); + setInitAndReset(m_aRatioLModel, 1); + setInitAndReset(m_bThreshHModel, m_bThreshHModel.minValue()); + setInitAndReset(m_bThreshMModel, m_bThreshMModel.minValue()); + setInitAndReset(m_bThreshLModel, m_bThreshLModel.minValue()); + setInitAndReset(m_bRatioHModel, 1); + setInitAndReset(m_bRatioMModel, 1); + setInitAndReset(m_bRatioLModel, 1); + setInitAndReset(m_atkHModel, 13.5); + setInitAndReset(m_atkMModel, 22.4); + setInitAndReset(m_atkLModel, 47.8); + setInitAndReset(m_relHModel, 132); + setInitAndReset(m_relMModel, 282); + setInitAndReset(m_relLModel, 282); + setInitAndReset(m_rmsTimeModel, 10); + setInitAndReset(m_kneeModel, 6); + setInitAndReset(m_rangeModel, 36); + setInitAndReset(m_balanceModel, 0); + setInitAndReset(m_depthScalingModel, true); + setInitAndReset(m_stereoLinkModel, false); + setInitAndReset(m_autoTimeModel, 0); + setInitAndReset(m_mixModel, 1); + setInitAndReset(m_feedbackModel, false); + setInitAndReset(m_midsideModel, false); + setInitAndReset(m_lookaheadEnableModel, false); + setInitAndReset(m_lookaheadModel, 0.f); + setInitAndReset(m_lowSideUpwardSuppressModel, false); +} + + + +void LOMMControls::loadSettings(const QDomElement& parent) +{ + m_depthModel.loadSettings(parent, "depth"); + m_timeModel.loadSettings(parent, "time"); + m_inVolModel.loadSettings(parent, "inVol"); + m_outVolModel.loadSettings(parent, "outVol"); + m_upwardModel.loadSettings(parent, "upward"); + m_downwardModel.loadSettings(parent, "downward"); + m_split1Model.loadSettings(parent, "split1"); + m_split2Model.loadSettings(parent, "split2"); + m_split1EnabledModel.loadSettings(parent, "split1Enabled"); + m_split2EnabledModel.loadSettings(parent, "split2Enabled"); + m_band1EnabledModel.loadSettings(parent, "band1Enabled"); + m_band2EnabledModel.loadSettings(parent, "band2Enabled"); + m_band3EnabledModel.loadSettings(parent, "band3Enabled"); + m_inHighModel.loadSettings(parent, "inHigh"); + m_inMidModel.loadSettings(parent, "inMid"); + m_inLowModel.loadSettings(parent, "inLow"); + m_outHighModel.loadSettings(parent, "outHigh"); + m_outMidModel.loadSettings(parent, "outMid"); + m_outLowModel.loadSettings(parent, "outLow"); + m_aThreshHModel.loadSettings(parent, "aThreshH"); + m_aThreshMModel.loadSettings(parent, "aThreshM"); + m_aThreshLModel.loadSettings(parent, "aThreshL"); + m_aRatioHModel.loadSettings(parent, "aRatioH"); + m_aRatioMModel.loadSettings(parent, "aRatioM"); + m_aRatioLModel.loadSettings(parent, "aRatioL"); + m_bThreshHModel.loadSettings(parent, "bThreshH"); + m_bThreshMModel.loadSettings(parent, "bThreshM"); + m_bThreshLModel.loadSettings(parent, "bThreshL"); + m_bRatioHModel.loadSettings(parent, "bRatioH"); + m_bRatioMModel.loadSettings(parent, "bRatioM"); + m_bRatioLModel.loadSettings(parent, "bRatioL"); + m_atkHModel.loadSettings(parent, "atkH"); + m_atkMModel.loadSettings(parent, "atkM"); + m_atkLModel.loadSettings(parent, "atkL"); + m_relHModel.loadSettings(parent, "relH"); + m_relMModel.loadSettings(parent, "relM"); + m_relLModel.loadSettings(parent, "relL"); + m_rmsTimeModel.loadSettings(parent, "rmsTime"); + m_kneeModel.loadSettings(parent, "knee"); + m_rangeModel.loadSettings(parent, "range"); + m_balanceModel.loadSettings(parent, "balance"); + m_depthScalingModel.loadSettings(parent, "depthScaling"); + m_stereoLinkModel.loadSettings(parent, "stereoLink"); + m_autoTimeModel.loadSettings(parent, "autoTime"); + m_mixModel.loadSettings(parent, "mix"); + m_feedbackModel.loadSettings(parent, "feedback"); + m_midsideModel.loadSettings(parent, "midside"); + m_lookaheadEnableModel.loadSettings(parent, "lookaheadEnable"); + m_lookaheadModel.loadSettings(parent, "lookahead"); + m_lowSideUpwardSuppressModel.loadSettings(parent, "lowSideUpwardSuppress"); +} + + + + +void LOMMControls::saveSettings(QDomDocument& doc, QDomElement& parent) +{ + m_depthModel.saveSettings(doc, parent, "depth"); + m_timeModel.saveSettings(doc, parent, "time"); + m_inVolModel.saveSettings(doc, parent, "inVol"); + m_outVolModel.saveSettings(doc, parent, "outVol"); + m_upwardModel.saveSettings(doc, parent, "upward"); + m_downwardModel.saveSettings(doc, parent, "downward"); + m_split1Model.saveSettings(doc, parent, "split1"); + m_split2Model.saveSettings(doc, parent, "split2"); + m_split1EnabledModel.saveSettings(doc, parent, "split1Enabled"); + m_split2EnabledModel.saveSettings(doc, parent, "split2Enabled"); + m_band1EnabledModel.saveSettings(doc, parent, "band1Enabled"); + m_band2EnabledModel.saveSettings(doc, parent, "band2Enabled"); + m_band3EnabledModel.saveSettings(doc, parent, "band3Enabled"); + m_inHighModel.saveSettings(doc, parent, "inHigh"); + m_inMidModel.saveSettings(doc, parent, "inMid"); + m_inLowModel.saveSettings(doc, parent, "inLow"); + m_outHighModel.saveSettings(doc, parent, "outHigh"); + m_outMidModel.saveSettings(doc, parent, "outMid"); + m_outLowModel.saveSettings(doc, parent, "outLow"); + m_aThreshHModel.saveSettings(doc, parent, "aThreshH"); + m_aThreshMModel.saveSettings(doc, parent, "aThreshM"); + m_aThreshLModel.saveSettings(doc, parent, "aThreshL"); + m_aRatioHModel.saveSettings(doc, parent, "aRatioH"); + m_aRatioMModel.saveSettings(doc, parent, "aRatioM"); + m_aRatioLModel.saveSettings(doc, parent, "aRatioL"); + m_bThreshHModel.saveSettings(doc, parent, "bThreshH"); + m_bThreshMModel.saveSettings(doc, parent, "bThreshM"); + m_bThreshLModel.saveSettings(doc, parent, "bThreshL"); + m_bRatioHModel.saveSettings(doc, parent, "bRatioH"); + m_bRatioMModel.saveSettings(doc, parent, "bRatioM"); + m_bRatioLModel.saveSettings(doc, parent, "bRatioL"); + m_atkHModel.saveSettings(doc, parent, "atkH"); + m_atkMModel.saveSettings(doc, parent, "atkM"); + m_atkLModel.saveSettings(doc, parent, "atkL"); + m_relHModel.saveSettings(doc, parent, "relH"); + m_relMModel.saveSettings(doc, parent, "relM"); + m_relLModel.saveSettings(doc, parent, "relL"); + m_rmsTimeModel.saveSettings(doc, parent, "rmsTime"); + m_kneeModel.saveSettings(doc, parent, "knee"); + m_rangeModel.saveSettings(doc, parent, "range"); + m_balanceModel.saveSettings(doc, parent, "balance"); + m_depthScalingModel.saveSettings(doc, parent, "depthScaling"); + m_stereoLinkModel.saveSettings(doc, parent, "stereoLink"); + m_autoTimeModel.saveSettings(doc, parent, "autoTime"); + m_mixModel.saveSettings(doc, parent, "mix"); + m_feedbackModel.saveSettings(doc, parent, "feedback"); + m_midsideModel.saveSettings(doc, parent, "midside"); + m_lookaheadEnableModel.saveSettings(doc, parent, "lookaheadEnable"); + m_lookaheadModel.saveSettings(doc, parent, "lookahead"); + m_lowSideUpwardSuppressModel.saveSettings(doc, parent, "lowSideUpwardSuppress"); +} + + +} // namespace lmms + + diff --git a/plugins/LOMM/LOMMControls.h b/plugins/LOMM/LOMMControls.h new file mode 100644 index 000000000..3e5325426 --- /dev/null +++ b/plugins/LOMM/LOMMControls.h @@ -0,0 +1,136 @@ +/* + * LOMMControls.h + * + * Copyright (c) 2023 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. + * + */ + +#ifndef LMMS_LOMM_CONTROLS_H +#define LMMS_LOMM_CONTROLS_H + +#include "LOMMControlDialog.h" +#include "EffectControls.h" + +namespace lmms +{ +class LOMMEffect; + +namespace gui +{ +class LOMMControlDialog; +} + +class LOMMControls : public EffectControls +{ + Q_OBJECT +public: + LOMMControls(LOMMEffect* effect); + ~LOMMControls() override = default; + + void saveSettings(QDomDocument & doc, QDomElement & parent) override; + void loadSettings(const QDomElement & parent) override; + inline QString nodeName() const override + { + return "LOMMControls"; + } + + int controlCount() override + { + return 49; + } + + gui::EffectControlDialog* createView() override + { + m_view = new gui::LOMMControlDialog(this); + return m_view; + } + + template + void setInitAndReset(AutomatableModel& model, T initValue) + { + model.setInitValue(initValue); + model.reset(); + } + +public slots: + void resetAllParameters(); + +private: + LOMMEffect* m_effect; + gui::LOMMControlDialog* m_view; + + FloatModel m_depthModel; + FloatModel m_timeModel; + FloatModel m_inVolModel; + FloatModel m_outVolModel; + FloatModel m_upwardModel; + FloatModel m_downwardModel; + FloatModel m_split1Model; + FloatModel m_split2Model; + BoolModel m_split1EnabledModel; + BoolModel m_split2EnabledModel; + BoolModel m_band1EnabledModel; + BoolModel m_band2EnabledModel; + BoolModel m_band3EnabledModel; + FloatModel m_inHighModel; + FloatModel m_inMidModel; + FloatModel m_inLowModel; + FloatModel m_outHighModel; + FloatModel m_outMidModel; + FloatModel m_outLowModel; + FloatModel m_aThreshHModel; + FloatModel m_aThreshMModel; + FloatModel m_aThreshLModel; + FloatModel m_aRatioHModel; + FloatModel m_aRatioMModel; + FloatModel m_aRatioLModel; + FloatModel m_bThreshHModel; + FloatModel m_bThreshMModel; + FloatModel m_bThreshLModel; + FloatModel m_bRatioHModel; + FloatModel m_bRatioMModel; + FloatModel m_bRatioLModel; + FloatModel m_atkHModel; + FloatModel m_atkMModel; + FloatModel m_atkLModel; + FloatModel m_relHModel; + FloatModel m_relMModel; + FloatModel m_relLModel; + FloatModel m_rmsTimeModel; + FloatModel m_kneeModel; + FloatModel m_rangeModel; + FloatModel m_balanceModel; + BoolModel m_depthScalingModel; + BoolModel m_stereoLinkModel; + FloatModel m_autoTimeModel; + FloatModel m_mixModel; + BoolModel m_feedbackModel; + BoolModel m_midsideModel; + BoolModel m_lookaheadEnableModel; + FloatModel m_lookaheadModel; + BoolModel m_lowSideUpwardSuppressModel; + + friend class gui::LOMMControlDialog; + friend class LOMMEffect; +}; + +} // namespace lmms + +#endif // LMMS_LOMM_CONTROLS_H diff --git a/plugins/LOMM/artwork.png b/plugins/LOMM/artwork.png new file mode 100644 index 000000000..cfc65908f Binary files /dev/null and b/plugins/LOMM/artwork.png differ diff --git a/plugins/LOMM/crossover_led_green.png b/plugins/LOMM/crossover_led_green.png new file mode 100644 index 000000000..440eb82dd Binary files /dev/null and b/plugins/LOMM/crossover_led_green.png differ diff --git a/plugins/LOMM/crossover_led_off.png b/plugins/LOMM/crossover_led_off.png new file mode 100644 index 000000000..2fd7f721c Binary files /dev/null and b/plugins/LOMM/crossover_led_off.png differ diff --git a/plugins/LOMM/depthScaling_active.png b/plugins/LOMM/depthScaling_active.png new file mode 100644 index 000000000..ff53a77e0 Binary files /dev/null and b/plugins/LOMM/depthScaling_active.png differ diff --git a/plugins/LOMM/depthScaling_inactive.png b/plugins/LOMM/depthScaling_inactive.png new file mode 100644 index 000000000..ba806b6c3 Binary files /dev/null and b/plugins/LOMM/depthScaling_inactive.png differ diff --git a/plugins/LOMM/feedback_active.png b/plugins/LOMM/feedback_active.png new file mode 100644 index 000000000..426abb506 Binary files /dev/null and b/plugins/LOMM/feedback_active.png differ diff --git a/plugins/LOMM/feedback_inactive.png b/plugins/LOMM/feedback_inactive.png new file mode 100644 index 000000000..b3e7d635a Binary files /dev/null and b/plugins/LOMM/feedback_inactive.png differ diff --git a/plugins/LOMM/high_band_active.png b/plugins/LOMM/high_band_active.png new file mode 100644 index 000000000..e3c225e2d Binary files /dev/null and b/plugins/LOMM/high_band_active.png differ diff --git a/plugins/LOMM/high_band_inactive.png b/plugins/LOMM/high_band_inactive.png new file mode 100644 index 000000000..43a24cc8f Binary files /dev/null and b/plugins/LOMM/high_band_inactive.png differ diff --git a/plugins/LOMM/init_active.png b/plugins/LOMM/init_active.png new file mode 100644 index 000000000..3401a74c1 Binary files /dev/null and b/plugins/LOMM/init_active.png differ diff --git a/plugins/LOMM/init_inactive.png b/plugins/LOMM/init_inactive.png new file mode 100644 index 000000000..dfd847c32 Binary files /dev/null and b/plugins/LOMM/init_inactive.png differ diff --git a/plugins/LOMM/logo.png b/plugins/LOMM/logo.png new file mode 100644 index 000000000..9340da708 Binary files /dev/null and b/plugins/LOMM/logo.png differ diff --git a/plugins/LOMM/lookahead_active.png b/plugins/LOMM/lookahead_active.png new file mode 100644 index 000000000..78fc1ba03 Binary files /dev/null and b/plugins/LOMM/lookahead_active.png differ diff --git a/plugins/LOMM/lookahead_inactive.png b/plugins/LOMM/lookahead_inactive.png new file mode 100644 index 000000000..8e4e2ddb3 Binary files /dev/null and b/plugins/LOMM/lookahead_inactive.png differ diff --git a/plugins/LOMM/lowSideUpwardSuppress_active.png b/plugins/LOMM/lowSideUpwardSuppress_active.png new file mode 100644 index 000000000..39e6d746c Binary files /dev/null and b/plugins/LOMM/lowSideUpwardSuppress_active.png differ diff --git a/plugins/LOMM/lowSideUpwardSuppress_inactive.png b/plugins/LOMM/lowSideUpwardSuppress_inactive.png new file mode 100644 index 000000000..8e61e129d Binary files /dev/null and b/plugins/LOMM/lowSideUpwardSuppress_inactive.png differ diff --git a/plugins/LOMM/low_band_active.png b/plugins/LOMM/low_band_active.png new file mode 100644 index 000000000..fc3ca34c1 Binary files /dev/null and b/plugins/LOMM/low_band_active.png differ diff --git a/plugins/LOMM/low_band_inactive.png b/plugins/LOMM/low_band_inactive.png new file mode 100644 index 000000000..48041495e Binary files /dev/null and b/plugins/LOMM/low_band_inactive.png differ diff --git a/plugins/LOMM/mid_band_active.png b/plugins/LOMM/mid_band_active.png new file mode 100644 index 000000000..5a714a019 Binary files /dev/null and b/plugins/LOMM/mid_band_active.png differ diff --git a/plugins/LOMM/mid_band_inactive.png b/plugins/LOMM/mid_band_inactive.png new file mode 100644 index 000000000..6ccabc0ad Binary files /dev/null and b/plugins/LOMM/mid_band_inactive.png differ diff --git a/plugins/LOMM/midside_active.png b/plugins/LOMM/midside_active.png new file mode 100644 index 000000000..9caf8b691 Binary files /dev/null and b/plugins/LOMM/midside_active.png differ diff --git a/plugins/LOMM/midside_inactive.png b/plugins/LOMM/midside_inactive.png new file mode 100644 index 000000000..7b14b75df Binary files /dev/null and b/plugins/LOMM/midside_inactive.png differ diff --git a/plugins/LOMM/split1Enabled_active.png b/plugins/LOMM/split1Enabled_active.png new file mode 100644 index 000000000..6b3f8ec62 Binary files /dev/null and b/plugins/LOMM/split1Enabled_active.png differ diff --git a/plugins/LOMM/split1Enabled_inactive.png b/plugins/LOMM/split1Enabled_inactive.png new file mode 100644 index 000000000..7577806a0 Binary files /dev/null and b/plugins/LOMM/split1Enabled_inactive.png differ diff --git a/plugins/LOMM/split2Enabled_active.png b/plugins/LOMM/split2Enabled_active.png new file mode 100644 index 000000000..b7b162ceb Binary files /dev/null and b/plugins/LOMM/split2Enabled_active.png differ diff --git a/plugins/LOMM/split2Enabled_inactive.png b/plugins/LOMM/split2Enabled_inactive.png new file mode 100644 index 000000000..678d0565b Binary files /dev/null and b/plugins/LOMM/split2Enabled_inactive.png differ diff --git a/plugins/LOMM/stereoLink_active.png b/plugins/LOMM/stereoLink_active.png new file mode 100644 index 000000000..2ba9c4b88 Binary files /dev/null and b/plugins/LOMM/stereoLink_active.png differ diff --git a/plugins/LOMM/stereoLink_inactive.png b/plugins/LOMM/stereoLink_inactive.png new file mode 100644 index 000000000..9e5d5b884 Binary files /dev/null and b/plugins/LOMM/stereoLink_inactive.png differ