diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index a2871bf99ec..263cb6fe6dc 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -30,6 +30,7 @@ SET(LMMS_PLUGIN_LIST carlabase carlapatchbay carlarack + Compressor CrossoverEQ Delay DualFilter diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 65b496617f1..50c063f6bb4 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -902,6 +902,26 @@ NesInstrumentView Knob { qproperty-lineWidth: 2; } +CompressorControlDialog { + qproperty-inVolAreaColor: rgba(209, 216, 228, 17); + qproperty-inVolColor: rgba(209, 216, 228, 100); + qproperty-outVolAreaColor: rgba(209, 216, 228, 30); + qproperty-outVolColor: rgba(209, 216, 228, 240); + qproperty-gainReductionColor: rgba(180, 100, 100, 210); + qproperty-kneeColor: rgba(39, 171, 95, 255); + qproperty-kneeColor2: rgba(9, 171, 160, 255); + qproperty-threshColor: rgba(39, 171, 95, 100); + qproperty-textColor: rgba(209, 216, 228, 50); + qproperty-graphColor: rgba(209, 216, 228, 50); + qproperty-resetColor: rgba(200, 100, 15, 200); +} + +CompressorControlDialog Knob { + color: #2fcc71; + qproperty-outerColor: #2fcc71; + qproperty-lineWidth: 2; +} + /* palette information */ LmmsPalette { diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 832da176f28..f78c14f8b33 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -938,6 +938,26 @@ NesInstrumentView Knob { qproperty-lineWidth: 2; } +CompressorControlDialog { + qproperty-inVolAreaColor: rgba(209, 216, 228, 17); + qproperty-inVolColor: rgba(209, 216, 228, 100); + qproperty-outVolAreaColor: rgba(209, 216, 228, 30); + qproperty-outVolColor: rgba(209, 216, 228, 240); + qproperty-gainReductionColor: rgba(180, 100, 100, 210); + qproperty-kneeColor: rgba(39, 171, 95, 255); + qproperty-kneeColor2: rgba(9, 171, 160, 255); + qproperty-threshColor: rgba(39, 171, 95, 100); + qproperty-textColor: rgba(209, 216, 228, 50); + qproperty-graphColor: rgba(209, 216, 228, 50); + qproperty-resetColor: rgba(200, 100, 15, 200); +} + +CompressorControlDialog Knob { + color: #2fcc71; + qproperty-outerColor: #2fcc71; + qproperty-lineWidth: 2; +} + /* palette information */ LmmsPalette { diff --git a/plugins/Compressor/CMakeLists.txt b/plugins/Compressor/CMakeLists.txt new file mode 100755 index 00000000000..111d0e2ec87 --- /dev/null +++ b/plugins/Compressor/CMakeLists.txt @@ -0,0 +1,3 @@ +INCLUDE(BuildPlugin) + +BUILD_PLUGIN(compressor Compressor.cpp CompressorControls.cpp CompressorControlDialog.cpp MOCFILES Compressor.h CompressorControls.h CompressorControlDialog.h ../Eq/EqFader.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") diff --git a/plugins/Compressor/Compressor.cpp b/plugins/Compressor/Compressor.cpp new file mode 100755 index 00000000000..0847eacdbcc --- /dev/null +++ b/plugins/Compressor/Compressor.cpp @@ -0,0 +1,664 @@ +/* + * Compressor.cpp + * + * Copyright (c) 2020 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 "Compressor.h" + +#include "embed.h" +#include "interpolation.h" +#include "lmms_math.h" +#include "plugin_export.h" + +extern "C" +{ + +Plugin::Descriptor PLUGIN_EXPORT compressor_plugin_descriptor = +{ + STRINGIFY(PLUGIN_NAME), + "Compressor", + QT_TRANSLATE_NOOP("PluginBrowser", "A dynamic range compressor."), + "Lost Robot ", + 0x0100, + Plugin::Effect, + new PluginPixmapLoader("logo"), + NULL, + NULL +} ; + +} + + +CompressorEffect::CompressorEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) : + Effect(&compressor_plugin_descriptor, parent, key), + m_compressorControls(this) +{ + m_sampleRate = Engine::mixer()->processingSampleRate(); + + m_yL[0] = m_yL[1] = COMP_NOISE_FLOOR; + + m_maxLookaheadVal[0] = 0; + m_maxLookaheadVal[1] = 0; + + // 200 ms + m_crestTimeConst = exp(-1.f / (0.2f * m_sampleRate)); + + connect(&m_compressorControls.m_attackModel, SIGNAL(dataChanged()), this, SLOT(calcAttack()), Qt::DirectConnection); + connect(&m_compressorControls.m_releaseModel, SIGNAL(dataChanged()), this, SLOT(calcRelease()), Qt::DirectConnection); + connect(&m_compressorControls.m_holdModel, SIGNAL(dataChanged()), this, SLOT(calcHold()), Qt::DirectConnection); + connect(&m_compressorControls.m_ratioModel, SIGNAL(dataChanged()), this, SLOT(calcRatio()), Qt::DirectConnection); + connect(&m_compressorControls.m_rangeModel, SIGNAL(dataChanged()), this, SLOT(calcRange()), Qt::DirectConnection); + connect(&m_compressorControls.m_rmsModel, SIGNAL(dataChanged()), this, SLOT(resizeRMS()), Qt::DirectConnection); + connect(&m_compressorControls.m_lookaheadLengthModel, SIGNAL(dataChanged()), this, SLOT(calcLookaheadLength()), Qt::DirectConnection); + connect(&m_compressorControls.m_thresholdModel, SIGNAL(dataChanged()), this, SLOT(calcThreshold()), Qt::DirectConnection); + connect(&m_compressorControls.m_kneeModel, SIGNAL(dataChanged()), this, SLOT(calcKnee()), Qt::DirectConnection); + connect(&m_compressorControls.m_outGainModel, SIGNAL(dataChanged()), this, SLOT(calcOutGain()), Qt::DirectConnection); + connect(&m_compressorControls.m_inGainModel, SIGNAL(dataChanged()), this, SLOT(calcInGain()), Qt::DirectConnection); + connect(&m_compressorControls.m_tiltModel, SIGNAL(dataChanged()), this, SLOT(calcTiltCoeffs()), Qt::DirectConnection); + connect(&m_compressorControls.m_tiltFreqModel, SIGNAL(dataChanged()), this, SLOT(calcTiltCoeffs()), Qt::DirectConnection); + connect(&m_compressorControls.m_limiterModel, SIGNAL(dataChanged()), this, SLOT(redrawKnee()), Qt::DirectConnection); + connect(&m_compressorControls.m_mixModel, SIGNAL(dataChanged()), this, SLOT(calcMix()), Qt::DirectConnection); + + connect(&m_compressorControls.m_autoAttackModel, SIGNAL(dataChanged()), this, SLOT(calcAutoAttack()), Qt::DirectConnection); + connect(&m_compressorControls.m_autoReleaseModel, SIGNAL(dataChanged()), this, SLOT(calcAutoRelease()), Qt::DirectConnection); + + connect(&m_compressorControls.m_thresholdModel, SIGNAL(dataChanged()), this, SLOT(calcAutoMakeup()), Qt::DirectConnection); + connect(&m_compressorControls.m_ratioModel, SIGNAL(dataChanged()), this, SLOT(calcAutoMakeup()), Qt::DirectConnection); + connect(&m_compressorControls.m_kneeModel, SIGNAL(dataChanged()), this, SLOT(calcAutoMakeup()), Qt::DirectConnection); + connect(&m_compressorControls.m_autoMakeupModel, SIGNAL(dataChanged()), this, SLOT(calcAutoMakeup()), Qt::DirectConnection); + + connect(Engine::mixer(), SIGNAL(sampleRateChanged()), this, SLOT(changeSampleRate())); + changeSampleRate(); +} + + + + +CompressorEffect::~CompressorEffect() +{ +} + + +float CompressorEffect::msToCoeff(float ms) +{ + // Convert time in milliseconds to applicable lowpass coefficient + return exp(m_coeffPrecalc / ms); +} + + + +void CompressorEffect::calcAutoMakeup() +{ + // Formulas using the compressor's Threshold, Ratio, and Knee values to estimate a good makeup gain value + + float tempGainResult; + if (-m_thresholdVal < m_kneeVal) + { + const float temp = -m_thresholdVal + m_kneeVal; + tempGainResult = ((m_compressorControls.m_limiterModel.value() ? 0 : m_ratioVal) - 1) * temp * temp / (4 * m_kneeVal); + } + else// Above knee + { + tempGainResult = m_compressorControls.m_limiterModel.value() + ? m_thresholdVal + : m_thresholdVal - m_thresholdVal * m_ratioVal; + } + + m_autoMakeupVal = 1.f / dbfsToAmp(tempGainResult); +} + + + +void CompressorEffect::calcAttack() +{ + m_attCoeff = msToCoeff(m_compressorControls.m_attackModel.value()); +} + +void CompressorEffect::calcRelease() +{ + m_relCoeff = msToCoeff(m_compressorControls.m_releaseModel.value()); +} + +void CompressorEffect::calcAutoAttack() +{ + m_autoAttVal = m_compressorControls.m_autoAttackModel.value() * 0.01f; +} + +void CompressorEffect::calcAutoRelease() +{ + m_autoRelVal = m_compressorControls.m_autoReleaseModel.value() * 0.01f; +} + +void CompressorEffect::calcHold() +{ + m_holdLength = m_compressorControls.m_holdModel.value() * 0.001f * m_sampleRate; + m_holdTimer[0] = 0; + m_holdTimer[1] = 0; +} + +void CompressorEffect::calcOutGain() +{ + // 0.999 is needed to keep the values from crossing the threshold all the time + // (most commonly for limiters specifically), and is kept across all modes for consistency. + m_outGainVal = dbfsToAmp(m_compressorControls.m_outGainModel.value()) * 0.999; +} + +void CompressorEffect::calcRatio() +{ + m_ratioVal = 1.f / m_compressorControls.m_ratioModel.value(); + m_redrawKnee = true; +} + +void CompressorEffect::calcRange() +{ + // Range is inactive when turned all the way down + m_rangeVal = (m_compressorControls.m_rangeModel.value() > m_compressorControls.m_rangeModel.minValue()) + ? dbfsToAmp(m_compressorControls.m_rangeModel.value()) + : 0; +} + +void CompressorEffect::resizeRMS() +{ + m_rmsTimeConst = exp(-1.f / (m_compressorControls.m_rmsModel.value() * 0.001f * m_sampleRate)); +} + +void CompressorEffect::calcLookaheadLength() +{ + m_lookaheadLength = qMax(m_compressorControls.m_lookaheadLengthModel.value() * 0.001f * m_sampleRate, 1.f); + + m_preLookaheadLength = ceil(m_lookaheadDelayLength - m_lookaheadLength); +} + +void CompressorEffect::calcThreshold() +{ + m_thresholdVal = m_compressorControls.m_thresholdModel.value(); + m_thresholdAmpVal = dbfsToAmp(m_thresholdVal); + m_redrawKnee = true; + m_redrawThreshold = true; +} + +void CompressorEffect::calcKnee() +{ + m_kneeVal = m_compressorControls.m_kneeModel.value() * 0.5f; + m_redrawKnee = true; +} + +void CompressorEffect::calcInGain() +{ + m_inGainVal = dbfsToAmp(m_compressorControls.m_inGainModel.value()); +} + +void CompressorEffect::redrawKnee() +{ + m_redrawKnee = true; +} + +void CompressorEffect::calcTiltCoeffs() +{ + m_tiltVal = m_compressorControls.m_tiltModel.value(); + + const float amp = 6 / log(2); + + const float gfactor = 5; + const float g1 = m_tiltVal > 0 ? -gfactor * m_tiltVal : -m_tiltVal; + const float g2 = m_tiltVal > 0 ? m_tiltVal : gfactor * m_tiltVal; + + m_lgain = exp(g1 / amp) - 1; + m_hgain = exp(g2 / amp) - 1; + + const float omega = 2 * F_PI * m_compressorControls.m_tiltFreqModel.value(); + const float n = 1 / (m_sampleRate * 3 + omega); + m_a0 = 2 * omega * n; + m_b1 = (m_sampleRate * 3 - omega) * n; +} + +void CompressorEffect::calcMix() +{ + m_mixVal = m_compressorControls.m_mixModel.value() * 0.01; +} + + + +bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) +{ + if (!isEnabled() || !isRunning()) + { + // Clear lookahead buffers and other values when needed + if (!m_cleanedBuffers) + { + m_yL[0] = m_yL[1] = COMP_NOISE_FLOOR; + m_gainResult[0] = m_gainResult[1] = 1; + m_displayPeak[0] = m_displayPeak[1] = COMP_NOISE_FLOOR; + m_displayGain[0] = m_displayGain[1] = COMP_NOISE_FLOOR; + std::fill(std::begin(m_lookaheadBuf[0]), std::end(m_lookaheadBuf[0]), 0); + std::fill(std::begin(m_lookaheadBuf[1]), std::end(m_lookaheadBuf[1]), 0); + m_lookaheadBufLoc[0] = 0; + m_lookaheadBufLoc[1] = 0; + std::fill(std::begin(m_preLookaheadBuf[0]), std::end(m_preLookaheadBuf[0]), 0); + std::fill(std::begin(m_preLookaheadBuf[1]), std::end(m_preLookaheadBuf[1]), 0); + m_preLookaheadBufLoc[0] = 0; + m_preLookaheadBufLoc[1] = 0; + std::fill(std::begin(m_inputBuf[0]), std::end(m_inputBuf[0]), 0); + std::fill(std::begin(m_inputBuf[1]), std::end(m_inputBuf[1]), 0); + m_inputBufLoc = 0; + m_cleanedBuffers = true; + } + return false; + } + else + { + m_cleanedBuffers = false; + } + + float outSum = 0.0; + const float d = dryLevel(); + const float w = wetLevel(); + + float lOutPeak = 0.0; + float rOutPeak = 0.0; + float lInPeak = 0.0; + float rInPeak = 0.0; + + const bool midside = m_compressorControls.m_midsideModel.value(); + const bool peakmode = m_compressorControls.m_peakmodeModel.value(); + const float inBalance = m_compressorControls.m_inBalanceModel.value(); + const float outBalance = m_compressorControls.m_outBalanceModel.value(); + const bool limiter = m_compressorControls.m_limiterModel.value(); + const float blend = m_compressorControls.m_blendModel.value(); + const float stereoBalance = m_compressorControls.m_stereoBalanceModel.value(); + const bool autoMakeup = m_compressorControls.m_autoMakeupModel.value(); + const int stereoLink = m_compressorControls.m_stereoLinkModel.value(); + const bool audition = m_compressorControls.m_auditionModel.value(); + const bool feedback = m_compressorControls.m_feedbackModel.value(); + const bool lookahead = m_compressorControls.m_lookaheadModel.value(); + + for(fpp_t f = 0; f < frames; ++f) + { + sample_t drySignal[2] = {buf[f][0], buf[f][1]}; + sample_t s[2] = {drySignal[0] * m_inGainVal, drySignal[1] * m_inGainVal}; + + // Calculate tilt filters, to bias the sidechain to the low or high frequencies + if (m_tiltVal) + { + calcTiltFilter(s[0], s[0], 0); + calcTiltFilter(s[1], s[1], 1); + } + + if (midside)// Convert left/right to mid/side + { + const float temp = s[0]; + s[0] = (temp + s[1]) * 0.5; + s[1] = temp - s[1]; + } + + s[0] *= inBalance > 0 ? 1 - inBalance : 1; + s[1] *= inBalance < 0 ? 1 + inBalance : 1; + + m_gainResult[0] = 0; + m_gainResult[1] = 0; + + for (int i = 0; i < 2; i++) + { + float inputValue = feedback ? m_prevOut[i] : s[i]; + + // Calculate the crest factor of the audio by diving the peak by the RMS + m_crestPeakVal[i] = qMax(inputValue * inputValue, m_crestTimeConst * m_crestPeakVal[i] + (1 - m_crestTimeConst) * (inputValue * inputValue)); + m_crestRmsVal[i] = m_crestTimeConst * m_crestRmsVal[i] + ((1 - m_crestTimeConst) * (inputValue * inputValue)); + m_crestFactorVal[i] = m_crestPeakVal[i] / m_crestRmsVal[i]; + + m_rmsVal[i] = m_rmsTimeConst * m_rmsVal[i] + ((1 - m_rmsTimeConst) * (inputValue * inputValue)); + + // Grab the peak or RMS value + inputValue = qMax(COMP_NOISE_FLOOR, peakmode ? abs(inputValue) : sqrt(m_rmsVal[i])); + + // The following code uses math magic to semi-efficiently + // find the largest value in the lookahead buffer. + // This can probably be improved. + if (lookahead) + { + // Pre-lookahead delay, so the total delay always matches 20 ms + ++m_preLookaheadBufLoc[i]; + if (m_preLookaheadBufLoc[i] >= m_preLookaheadLength) + { + m_preLookaheadBufLoc[i] = 0; + } + const float tempInputValue = inputValue; + inputValue = m_preLookaheadBuf[i][m_preLookaheadBufLoc[i]]; + m_preLookaheadBuf[i][m_preLookaheadBufLoc[i]] = tempInputValue; + + + // Increment ring buffer location + ++m_lookaheadBufLoc[i]; + if (m_lookaheadBufLoc[i] >= m_lookaheadLength) + { + m_lookaheadBufLoc[i] = 0; + } + + m_lookaheadBuf[i][m_lookaheadBufLoc[i]] = inputValue; + + // If the new input value is larger than the stored maximum, + // store that as the maximum + if (inputValue >= m_maxLookaheadVal[i]) + { + m_maxLookaheadVal[i] = inputValue; + m_maxLookaheadTimer[i] = m_lookaheadLength; + } + + // Decrement timer. When the timer reaches 0, that means the + // stored maximum value has left the buffer and a new + // maximum value must be found. + if (--m_maxLookaheadTimer[i] <= 0) + { + m_maxLookaheadTimer[i] = std::distance(std::begin(m_lookaheadBuf[i]), + std::max_element(std::begin(m_lookaheadBuf[i]), std::begin(m_lookaheadBuf[i]) + m_lookaheadLength)); + m_maxLookaheadVal[i] = m_lookaheadBuf[i][m_maxLookaheadTimer[i]]; + m_maxLookaheadTimer[i] = realmod(m_maxLookaheadTimer[i] - m_lookaheadBufLoc[i], m_lookaheadLength); + } + + inputValue = m_maxLookaheadVal[i]; + } + + float t = inputValue; + + if (t > m_yL[i])// Attack phase + { + // We want the "resting value" of our crest factor to be with a sine wave, + // which with this variable has a value of 2. + // So, we pull this value down to 0, and multiply it by the percentage of + // automatic attack control that is applied. We then add 2 back to it. + float crestFactorValTemp = ((m_crestFactorVal[i] - 2.f) * m_autoAttVal) + 2.f; + + // Calculate attack value depending on crest factor + const float att = m_autoAttVal + ? msToCoeff(2.f * m_compressorControls.m_attackModel.value() / (crestFactorValTemp)) + : m_attCoeff; + + m_yL[i] = m_yL[i] * att + (1 - att) * t; + m_holdTimer[i] = m_holdLength;// Reset hold timer + } + else// Release phase + { + float crestFactorValTemp = ((m_crestFactorVal[i] - 2.f) * m_autoRelVal) + 2.f; + + const float rel = m_autoRelVal + ? msToCoeff(2.f * m_compressorControls.m_releaseModel.value() / (crestFactorValTemp)) + : m_relCoeff; + + if (m_holdTimer[i])// Don't change peak if hold is being applied + { + --m_holdTimer[i]; + } + else + { + m_yL[i] = m_yL[i] * rel + (1 - rel) * t; + } + } + + // Keep it above the noise floor + m_yL[i] = qMax(COMP_NOISE_FLOOR, m_yL[i]); + + // For the visualizer + m_displayPeak[i] = qMax(m_yL[i], m_displayPeak[i]); + + const float currentPeakDbfs = ampToDbfs(m_yL[i]); + + // Now find the gain change that should be applied, + // depending on the measured input value. + if (currentPeakDbfs - m_thresholdVal < -m_kneeVal)// Below knee + { + m_gainResult[i] = currentPeakDbfs; + } + else if (currentPeakDbfs - m_thresholdVal < m_kneeVal)// Within knee + { + const float temp = currentPeakDbfs - m_thresholdVal + m_kneeVal; + m_gainResult[i] = currentPeakDbfs + ((limiter ? 0 : m_ratioVal) - 1) * temp * temp / (4 * m_kneeVal); + } + else// Above knee + { + m_gainResult[i] = limiter + ? m_thresholdVal + : m_thresholdVal + (currentPeakDbfs - m_thresholdVal) * m_ratioVal; + } + + m_gainResult[i] = dbfsToAmp(m_gainResult[i]) / m_yL[i]; + m_gainResult[i] = qMax(m_rangeVal, m_gainResult[i]); + } + + switch (stereoLink) + { + case Unlinked: + { + break; + } + case Maximum: + { + m_gainResult[0] = m_gainResult[1] = qMin(m_gainResult[0], m_gainResult[1]); + break; + } + case Average: + { + m_gainResult[0] = m_gainResult[1] = (m_gainResult[0] + m_gainResult[1]) * 0.5f; + break; + } + case Minimum: + { + m_gainResult[0] = m_gainResult[1] = qMax(m_gainResult[0], m_gainResult[1]); + break; + } + case Blend: + { + if (blend > 0)// 0 is unlinked + { + if (blend <= 1)// Blend to minimum volume + { + const float temp1 = qMin(m_gainResult[0], m_gainResult[1]); + m_gainResult[0] = linearInterpolate(m_gainResult[0], temp1, blend); + m_gainResult[1] = linearInterpolate(m_gainResult[1], temp1, blend); + } + else if (blend <= 2)// Blend to average volume + { + const float temp1 = qMin(m_gainResult[0], m_gainResult[1]); + const float temp2 = (m_gainResult[0] + m_gainResult[1]) * 0.5f; + m_gainResult[0] = linearInterpolate(temp1, temp2, blend - 1); + m_gainResult[1] = m_gainResult[0]; + } + else// Blend to maximum volume + { + const float temp1 = (m_gainResult[0] + m_gainResult[1]) * 0.5f; + const float temp2 = qMax(m_gainResult[0], m_gainResult[1]); + m_gainResult[0] = linearInterpolate(temp1, temp2, blend - 2); + m_gainResult[1] = m_gainResult[0]; + } + } + break; + } + } + + // Bias compression to the left or right (or mid or side) + if (stereoBalance != 0) + { + m_gainResult[0] = 1 - ((1 - m_gainResult[0]) * (stereoBalance > 0 ? 1 - stereoBalance : 1)); + m_gainResult[1] = 1 - ((1 - m_gainResult[1]) * (stereoBalance < 0 ? 1 + stereoBalance : 1)); + } + + // For visualizer + m_displayGain[0] = qMax(m_gainResult[0], m_displayGain[0]); + m_displayGain[1] = qMax(m_gainResult[1], m_displayGain[1]); + + // Delay the signal by 20 ms via ring buffer if lookahead is enabled + if (lookahead) + { + ++m_inputBufLoc; + if (m_inputBufLoc >= m_lookaheadDelayLength) + { + m_inputBufLoc = 0; + } + + const float temp[2] = {drySignal[0], drySignal[1]}; + s[0] = m_inputBuf[0][m_inputBufLoc]; + s[1] = m_inputBuf[1][m_inputBufLoc]; + + m_inputBuf[0][m_inputBufLoc] = temp[0]; + m_inputBuf[1][m_inputBufLoc] = temp[1]; + } + else + { + s[0] = drySignal[0]; + s[1] = drySignal[1]; + } + + float delayedDrySignal[2] = {s[0], s[1]}; + + if (midside)// Convert left/right to mid/side + { + const float temp = s[0]; + s[0] = (temp + s[1]) * 0.5; + s[1] = temp - s[1]; + } + + s[0] *= inBalance > 0 ? 1 - inBalance : 1; + s[1] *= inBalance < 0 ? 1 + inBalance : 1; + + s[0] *= m_gainResult[0] * m_inGainVal * m_outGainVal * (outBalance > 0 ? 1 - outBalance : 1); + s[1] *= m_gainResult[1] * m_inGainVal * m_outGainVal * (outBalance < 0 ? 1 + outBalance : 1); + + if (midside)// Convert mid/side back to left/right + { + const float temp1 = s[0]; + const float temp2 = s[1] * 0.5; + s[0] = temp1 + temp2; + s[1] = temp1 - temp2; + } + + m_prevOut[0] = s[0]; + m_prevOut[1] = s[1]; + + // Negate wet signal from dry signal + if (audition) + { + s[0] = (-s[0] + delayedDrySignal[0] * m_outGainVal); + s[1] = (-s[1] + delayedDrySignal[1] * m_outGainVal); + } + else if (autoMakeup) + { + s[0] *= m_autoMakeupVal; + s[1] *= m_autoMakeupVal; + } + + // Calculate wet/dry value results + const float temp1 = delayedDrySignal[0]; + const float temp2 = delayedDrySignal[1]; + buf[f][0] = d * temp1 + w * s[0]; + buf[f][1] = d * temp2 + w * s[1]; + buf[f][0] = (1 - m_mixVal) * temp1 + m_mixVal * buf[f][0]; + buf[f][1] = (1 - m_mixVal) * temp2 + m_mixVal * buf[f][1]; + + outSum += buf[f][0] * buf[f][0] + buf[f][1] * buf[f][1]; + + lInPeak = drySignal[0] > lInPeak ? drySignal[0] : lInPeak; + rInPeak = drySignal[1] > rInPeak ? drySignal[1] : rInPeak; + lOutPeak = s[0] > lOutPeak ? s[0] : lOutPeak; + rOutPeak = s[1] > rOutPeak ? s[1] : rOutPeak; + } + + checkGate(outSum / frames); + m_compressorControls.m_outPeakL = lOutPeak; + m_compressorControls.m_outPeakR = rOutPeak; + m_compressorControls.m_inPeakL = lInPeak; + m_compressorControls.m_inPeakR = rInPeak; + + return isRunning(); +} + + +// Regular modulo doesn't handle negative numbers correctly. This does. +inline int CompressorEffect::realmod(int k, int n) +{ + return (k %= n) < 0 ? k+n : k; +} + +// Regular fmod doesn't handle negative numbers correctly. This does. +inline float CompressorEffect::realfmod(float k, float n) +{ + return (k = fmod(k, n)) < 0 ? k+n : k; +} + + + +inline void CompressorEffect::calcTiltFilter(sample_t inputSample, sample_t &outputSample, int filtNum) +{ + m_tiltOut[filtNum] = m_a0 * inputSample + m_b1 * m_tiltOut[filtNum]; + outputSample = inputSample + m_lgain * m_tiltOut[filtNum] + m_hgain * (inputSample - m_tiltOut[filtNum]); +} + + + +void CompressorEffect::changeSampleRate() +{ + m_sampleRate = Engine::mixer()->processingSampleRate(); + + m_coeffPrecalc = COMP_LOG / (m_sampleRate * 0.001f); + + // 200 ms + m_crestTimeConst = exp(-1.f / (0.2f * m_sampleRate)); + + // 20 ms + m_lookaheadDelayLength = 0.02 * m_sampleRate; + m_inputBuf[0].resize(m_lookaheadDelayLength); + m_inputBuf[1].resize(m_lookaheadDelayLength); + + m_lookaheadBuf[0].resize(m_lookaheadDelayLength); + m_lookaheadBuf[1].resize(m_lookaheadDelayLength); + + m_preLookaheadBuf[0].resize(m_lookaheadDelayLength); + m_preLookaheadBuf[1].resize(m_lookaheadDelayLength); + + calcThreshold(); + calcKnee(); + calcRatio(); + calcAutoMakeup();// This should be after Threshold, Knee, and Ratio + + calcAttack(); + calcRelease(); + calcRange(); + calcLookaheadLength(); + calcHold(); + resizeRMS(); + calcOutGain(); + calcInGain(); + calcTiltCoeffs(); + calcMix(); +} + + + +extern "C" +{ + +// necessary for getting instance out of shared lib +PLUGIN_EXPORT Plugin * lmms_plugin_main(Model* parent, void* data) +{ + return new CompressorEffect(parent, static_cast(data)); +} + +} + diff --git a/plugins/Compressor/Compressor.h b/plugins/Compressor/Compressor.h new file mode 100755 index 00000000000..be9cd066264 --- /dev/null +++ b/plugins/Compressor/Compressor.h @@ -0,0 +1,153 @@ +/* + * Compressor.h + * + * Copyright (c) 2020 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 COMPRESSOR_H +#define COMPRESSOR_H + +#include "CompressorControls.h" + +#include "Effect.h" +#include "ValueBuffer.h" +#include "RmsHelper.h" + + +constexpr float COMP_LOG = -2.2; + +class CompressorEffect : public Effect +{ + Q_OBJECT +public: + CompressorEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key); + ~CompressorEffect() override; + bool processAudioBuffer(sampleFrame* buf, const fpp_t frames) override; + + EffectControls* controls() override + { + return &m_compressorControls; + } + +private slots: + void calcAutoMakeup(); + void calcAttack(); + void calcRelease(); + void calcAutoAttack(); + void calcAutoRelease(); + void calcHold(); + void calcOutGain(); + void calcRatio(); + void calcRange(); + void resizeRMS(); + void calcLookaheadLength(); + void calcThreshold(); + void calcKnee(); + void calcInGain(); + void calcTiltCoeffs(); + void calcMix(); + void changeSampleRate(); + void redrawKnee(); + +private: + CompressorControls m_compressorControls; + + float msToCoeff(float ms); + + inline void calcTiltFilter(sample_t inputSample, sample_t &outputSample, int filtNum); + inline int realmod(int k, int n); + inline float realfmod(float k, float n); + + enum StereoLinkModes { Unlinked, Maximum, Average, Minimum, Blend }; + + std::vector m_preLookaheadBuf[2]; + int m_preLookaheadBufLoc[2] = {0}; + + std::vector m_lookaheadBuf[2]; + int m_lookaheadBufLoc[2] = {0}; + + std::vector m_inputBuf[2]; + int m_inputBufLoc = 0; + + float m_attCoeff; + float m_relCoeff; + float m_autoAttVal; + float m_autoRelVal; + + int m_holdLength = 0; + int m_holdTimer[2] = {0, 0}; + + int m_lookaheadLength; + int m_lookaheadDelayLength; + int m_preLookaheadLength; + float m_thresholdAmpVal; + float m_autoMakeupVal; + float m_outGainVal; + float m_inGainVal; + float m_rangeVal; + float m_tiltVal; + float m_mixVal; + + float m_coeffPrecalc; + + sampleFrame m_maxLookaheadVal; + + int m_maxLookaheadTimer[2] = {1, 1}; + + float m_rmsTimeConst; + float m_rmsVal[2] = {0, 0}; + + float m_crestPeakVal[2] = {0, 0}; + float m_crestRmsVal[2] = {0, 0}; + float m_crestFactorVal[2] = {0, 0}; + float m_crestTimeConst; + + float m_tiltOut[2] = {0}; + + bool m_cleanedBuffers = false; + + float m_sampleRate; + + float m_lgain; + float m_hgain; + float m_a0; + float m_b1; + + float m_prevOut[2] = {0}; + + float m_yL[2]; + float m_gainResult[2]; + float m_displayPeak[2]; + float m_displayGain[2]; + + float m_kneeVal; + float m_thresholdVal; + float m_ratioVal; + + bool m_redrawKnee = true; + bool m_redrawThreshold = true; + + friend class CompressorControls; + friend class CompressorControlDialog; +} ; + +#endif diff --git a/plugins/Compressor/CompressorControlDialog.cpp b/plugins/Compressor/CompressorControlDialog.cpp new file mode 100755 index 00000000000..363098eec0f --- /dev/null +++ b/plugins/Compressor/CompressorControlDialog.cpp @@ -0,0 +1,762 @@ +/* + * CompressorControlDialog.cpp + * + * Copyright (c) 2020 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 "Compressor.h" +#include "CompressorControlDialog.h" +#include "CompressorControls.h" + +#include +#include +#include + +#include "embed.h" +#include "GuiApplication.h" +#include "gui_templates.h" +#include "interpolation.h" +#include "MainWindow.h" +#include "ToolTip.h" + +CompressorControlDialog::CompressorControlDialog(CompressorControls* controls) : + EffectControlDialog(controls), + m_controls(controls), + m_inVolAreaColor(209, 216, 228, 17), + m_inVolColor(209, 216, 228, 100), + m_outVolAreaColor(209, 216, 228, 30), + m_outVolColor(209, 216, 228, 240), + m_gainReductionColor(180, 100, 100, 210), + m_kneeColor(39, 171, 95, 255), + m_kneeColor2(9, 171, 160, 255), + m_threshColor(39, 171, 95, 100), + m_textColor(209, 216, 228, 50), + m_graphColor(209, 216, 228, 50), + m_resetColor(200, 100, 15, 200) +{ + setAutoFillBackground(true); + QPalette pal; + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); + setPalette(pal); + setMinimumSize(MIN_COMP_SCREEN_X, MIN_COMP_SCREEN_Y); + resize(COMP_SCREEN_X, COMP_SCREEN_Y); + + m_graphPixmap.fill(QColor("transparent")); + + m_visPixmap.fill(QColor("transparent")); + + m_kneePixmap.fill(QColor("transparent")); + + m_kneePixmap2.fill(QColor("transparent")); + + m_miscPixmap.fill(QColor("transparent")); + + m_controlsBoxLabel = new QLabel(this); + m_controlsBoxLabel->setPixmap(PLUGIN_NAME::getIconPixmap("controlsBox")); + m_controlsBoxLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + + m_rmsEnabledLabel = new QLabel(this); + m_rmsEnabledLabel->setPixmap(PLUGIN_NAME::getIconPixmap("knob_enabled")); + m_rmsEnabledLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + + m_blendEnabledLabel = new QLabel(this); + m_blendEnabledLabel->setPixmap(PLUGIN_NAME::getIconPixmap("knob_enabled")); + m_blendEnabledLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + + m_lookaheadEnabledLabel = new QLabel(this); + m_lookaheadEnabledLabel->setPixmap(PLUGIN_NAME::getIconPixmap("knob_enabled")); + m_lookaheadEnabledLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + + m_ratioEnabledLabel = new QLabel(this); + m_ratioEnabledLabel->setPixmap(PLUGIN_NAME::getIconPixmap("knob_enabled_large")); + m_ratioEnabledLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + + m_thresholdKnob = new Knob(knobStyled, this); + makeLargeKnob(m_thresholdKnob, tr("Threshold:") , " dBFS"); + m_thresholdKnob->setModel(&controls->m_thresholdModel); + ToolTip::add(m_thresholdKnob, tr("Volume at which the compression begins to take place")); + + m_ratioKnob = new Knob(knobStyled, this); + makeLargeKnob(m_ratioKnob, tr("Ratio:") , ":1"); + m_ratioKnob->setModel(&controls->m_ratioModel); + ToolTip::add(m_ratioKnob, tr("How far the compressor must turn the volume down after crossing the threshold")); + + m_attackKnob = new Knob(knobStyled, this); + makeLargeKnob(m_attackKnob, tr("Attack:") , " ms"); + m_attackKnob->setModel(&controls->m_attackModel); + ToolTip::add(m_attackKnob, tr("Speed at which the compressor starts to compress the audio")); + + m_releaseKnob = new Knob(knobStyled, this); + makeLargeKnob(m_releaseKnob, tr("Release:") , " ms"); + m_releaseKnob->setModel(&controls->m_releaseModel); + ToolTip::add(m_releaseKnob, tr("Speed at which the compressor ceases to compress the audio")); + + m_kneeKnob = new Knob(knobStyled, this); + makeSmallKnob(m_kneeKnob, tr("Knee:") , " dB"); + m_kneeKnob->setModel(&controls->m_kneeModel); + ToolTip::add(m_kneeKnob, tr("Smooth out the gain reduction curve around the threshold")); + + m_rangeKnob = new Knob(knobStyled, this); + makeSmallKnob(m_rangeKnob, tr("Range:") , " dBFS"); + m_rangeKnob->setModel(&controls->m_rangeModel); + ToolTip::add(m_rangeKnob, tr("Maximum gain reduction")); + + m_lookaheadLengthKnob = new Knob(knobStyled, this); + makeSmallKnob(m_lookaheadLengthKnob, tr("Lookahead Length:") , " ms"); + m_lookaheadLengthKnob->setModel(&controls->m_lookaheadLengthModel); + ToolTip::add(m_lookaheadLengthKnob, tr("How long the compressor has to react to the sidechain signal ahead of time")); + + m_holdKnob = new Knob(knobStyled, this); + makeSmallKnob(m_holdKnob, tr("Hold:") , " ms"); + m_holdKnob->setModel(&controls->m_holdModel); + ToolTip::add(m_holdKnob, tr("Delay between attack and release stages")); + + m_rmsKnob = new Knob(knobStyled, this); + makeSmallKnob(m_rmsKnob, tr("RMS Size:") , ""); + m_rmsKnob->setModel(&controls->m_rmsModel); + ToolTip::add(m_rmsKnob, tr("Size of the RMS buffer")); + + m_inBalanceKnob = new Knob(knobStyled, this); + makeSmallKnob(m_inBalanceKnob, tr("Input Balance:") , ""); + m_inBalanceKnob->setModel(&controls->m_inBalanceModel); + ToolTip::add(m_inBalanceKnob, tr("Bias the input audio to the left/right or mid/side")); + + m_outBalanceKnob = new Knob(knobStyled, this); + makeSmallKnob(m_outBalanceKnob, tr("Output Balance:") , ""); + m_outBalanceKnob->setModel(&controls->m_outBalanceModel); + ToolTip::add(m_outBalanceKnob, tr("Bias the output audio to the left/right or mid/side")); + + m_stereoBalanceKnob = new Knob(knobStyled, this); + makeSmallKnob(m_stereoBalanceKnob, tr("Stereo Balance:") , ""); + m_stereoBalanceKnob->setModel(&controls->m_stereoBalanceModel); + ToolTip::add(m_stereoBalanceKnob, tr("Bias the sidechain signal to the left/right or mid/side")); + + m_blendKnob = new Knob(knobStyled, this); + makeSmallKnob(m_blendKnob, tr("Stereo Link Blend:") , ""); + m_blendKnob->setModel(&controls->m_blendModel); + ToolTip::add(m_blendKnob, tr("Blend between unlinked/maximum/average/minimum stereo linking modes")); + + m_tiltKnob = new Knob(knobStyled, this); + makeSmallKnob(m_tiltKnob, tr("Tilt Gain:") , " dB"); + m_tiltKnob->setModel(&controls->m_tiltModel); + ToolTip::add(m_tiltKnob, tr("Bias the sidechain signal to the low or high frequencies. -6 db is lowpass, 6 db is highpass.")); + + m_tiltFreqKnob = new Knob(knobStyled, this); + makeSmallKnob(m_tiltFreqKnob, tr("Tilt Frequency:") , " Hz"); + m_tiltFreqKnob->setModel(&controls->m_tiltFreqModel); + ToolTip::add(m_tiltFreqKnob, tr("Center frequency of sidechain tilt filter")); + + m_mixKnob = new Knob(knobStyled, this); + makeSmallKnob(m_mixKnob, tr("Mix:") , "%"); + m_mixKnob->setModel(&controls->m_mixModel); + ToolTip::add(m_mixKnob, tr("Balance between wet and dry signals")); + + m_autoAttackKnob = new Knob(knobStyled, this); + makeSmallKnob(m_autoAttackKnob, tr("Auto Attack:") , "%"); + m_autoAttackKnob->setModel(&controls->m_autoAttackModel); + ToolTip::add(m_autoAttackKnob, tr("Automatically control attack value depending on crest factor")); + + m_autoReleaseKnob = new Knob(knobStyled, this); + makeSmallKnob(m_autoReleaseKnob, tr("Auto Release:") , "%"); + m_autoReleaseKnob->setModel(&controls->m_autoReleaseModel); + ToolTip::add(m_autoReleaseKnob, tr("Automatically control release value depending on crest factor")); + + m_outFader = new EqFader(&controls->m_outGainModel,tr("Output gain"), + this, &controls->m_outPeakL, &controls->m_outPeakR); + m_outFader->setDisplayConversion(false); + m_outFader->setHintText(tr("Gain"), "dBFS"); + ToolTip::add(m_outFader, tr("Output volume")); + + m_inFader = new EqFader(&controls->m_inGainModel,tr("Input gain"), + this, &controls->m_inPeakL, &controls->m_inPeakR); + m_inFader->setDisplayConversion(false); + m_inFader->setHintText(tr("Gain"), "dBFS"); + ToolTip::add(m_inFader, tr("Input volume")); + + rmsButton = new PixmapButton(this, tr("Root Mean Square")); + rmsButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("rms_sel")); + rmsButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("rms_unsel")); + ToolTip::add(rmsButton, tr("Use RMS of the input")); + + peakButton = new PixmapButton(this, tr("Peak")); + peakButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("peak_sel")); + peakButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("peak_unsel")); + ToolTip::add(peakButton, tr("Use absolute value of the input")); + + rmsPeakGroup = new automatableButtonGroup(this); + rmsPeakGroup->addButton(rmsButton); + rmsPeakGroup->addButton(peakButton); + rmsPeakGroup->setModel(&controls->m_peakmodeModel); + + leftRightButton = new PixmapButton(this, tr("Left/Right")); + leftRightButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("leftright_sel")); + leftRightButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("leftright_unsel")); + ToolTip::add(leftRightButton, tr("Compress left and right audio")); + + midSideButton = new PixmapButton(this, tr("Mid/Side")); + midSideButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("midside_sel")); + midSideButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("midside_unsel")); + ToolTip::add(midSideButton, tr("Compress mid and side audio")); + + leftRightMidSideGroup = new automatableButtonGroup(this); + leftRightMidSideGroup->addButton(leftRightButton); + leftRightMidSideGroup->addButton(midSideButton); + leftRightMidSideGroup->setModel(&controls->m_midsideModel); + + compressButton = new PixmapButton(this, tr("Compressor")); + compressButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("compressor_sel")); + compressButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("compressor_unsel")); + ToolTip::add(compressButton, tr("Compress the audio")); + + limitButton = new PixmapButton(this, tr("Limiter")); + limitButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("limiter_sel")); + limitButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("limiter_unsel")); + ToolTip::add(limitButton, tr("Set Ratio to infinity (is not guaranteed to limit audio volume)")); + + compressLimitGroup = new automatableButtonGroup(this); + compressLimitGroup->addButton(compressButton); + compressLimitGroup->addButton(limitButton); + compressLimitGroup->setModel(&controls->m_limiterModel); + + unlinkedButton = new PixmapButton(this, tr("Unlinked")); + unlinkedButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("unlinked_sel")); + unlinkedButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("unlinked_unsel")); + ToolTip::add(unlinkedButton, tr("Compress each channel separately")); + + maximumButton = new PixmapButton(this, tr("Maximum")); + maximumButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("maximum_sel")); + maximumButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("maximum_unsel")); + ToolTip::add(maximumButton, tr("Compress based on the loudest channel")); + + averageButton = new PixmapButton(this, tr("Average")); + averageButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("average_sel")); + averageButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("average_unsel")); + ToolTip::add(averageButton, tr("Compress based on the averaged channel volume")); + + minimumButton = new PixmapButton(this, tr("Minimum")); + minimumButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("minimum_sel")); + minimumButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("minimum_unsel")); + ToolTip::add(minimumButton, tr("Compress based on the quietest channel")); + + blendButton = new PixmapButton(this, tr("Blend")); + blendButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("blend_sel")); + blendButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("blend_unsel")); + ToolTip::add(blendButton, tr("Blend between stereo linking modes")); + + stereoLinkGroup = new automatableButtonGroup(this); + stereoLinkGroup->addButton(unlinkedButton); + stereoLinkGroup->addButton(maximumButton); + stereoLinkGroup->addButton(averageButton); + stereoLinkGroup->addButton(minimumButton); + stereoLinkGroup->addButton(blendButton); + stereoLinkGroup->setModel(&controls->m_stereoLinkModel); + + autoMakeupButton = new PixmapButton(this, tr("Auto Makeup Gain")); + autoMakeupButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("autogain_sel")); + autoMakeupButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("autogain_unsel")); + ToolTip::add(autoMakeupButton, tr("Automatically change makeup gain depending on threshold, knee, and ratio settings")); + autoMakeupButton->setCheckable(true); + autoMakeupButton->setModel(&controls->m_autoMakeupModel); + + auditionButton = new PixmapButton(this, tr("Soft Clip")); + auditionButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("audition_sel")); + auditionButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("audition_unsel")); + ToolTip::add(auditionButton, tr("Play the delta signal")); + auditionButton->setCheckable(true); + auditionButton->setModel(&controls->m_auditionModel); + + feedbackButton = new PixmapButton(this, tr("Soft Clip")); + feedbackButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("feedback_sel")); + feedbackButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("feedback_unsel")); + ToolTip::add(feedbackButton, tr("Use the compressor's output as the sidechain input")); + feedbackButton->setCheckable(true); + feedbackButton->setModel(&controls->m_feedbackModel); + + lookaheadButton = new PixmapButton(this, tr("Lookahead Enabled")); + lookaheadButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("lookahead_sel")); + lookaheadButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("lookahead_unsel")); + ToolTip::add(lookaheadButton, tr("Enable Lookahead, which introduces 20 milliseconds of latency")); + lookaheadButton->setCheckable(true); + lookaheadButton->setModel(&controls->m_lookaheadModel); + + connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(updateDisplay())); + + connect(&m_controls->m_peakmodeModel, SIGNAL(dataChanged()), this, SLOT(peakmodeChanged())); + connect(&m_controls->m_stereoLinkModel, SIGNAL(dataChanged()), this, SLOT(stereoLinkChanged())); + connect(&m_controls->m_lookaheadModel, SIGNAL(dataChanged()), this, SLOT(lookaheadChanged())); + connect(&m_controls->m_limiterModel, SIGNAL(dataChanged()), this, SLOT(limiterChanged())); + + m_timeElapsed.start(); + + peakmodeChanged(); + stereoLinkChanged(); + lookaheadChanged(); + limiterChanged(); +} + + +void CompressorControlDialog::makeLargeKnob(Knob * knob, QString hint, QString unit) +{ + knob->setHintText(hint, unit); + knob->setFixedSize(56, 56); + knob->setOuterRadius(23); + knob->setInnerRadius(15); + knob->setCenterPointX(28); + knob->setCenterPointY(28); +} + +void CompressorControlDialog::makeSmallKnob(Knob * knob, QString hint, QString unit) +{ + knob->setHintText(hint, unit); + knob->setFixedSize(30, 30); + knob->setOuterRadius(10); + knob->setInnerRadius(4); + knob->setCenterPointX(15); + knob->setCenterPointY(15); +} + + +void CompressorControlDialog::peakmodeChanged() +{ + m_rmsKnob->setVisible(!m_controls->m_peakmodeModel.value()); + m_rmsEnabledLabel->setVisible(!m_controls->m_peakmodeModel.value()); +} + + +void CompressorControlDialog::stereoLinkChanged() +{ + m_blendKnob->setVisible(m_controls->m_stereoLinkModel.value() == 4); + m_blendEnabledLabel->setVisible(m_controls->m_stereoLinkModel.value() == 4); +} + + +void CompressorControlDialog::lookaheadChanged() +{ + m_lookaheadLengthKnob->setVisible(m_controls->m_lookaheadModel.value()); + m_lookaheadEnabledLabel->setVisible(m_controls->m_lookaheadModel.value()); +} + + +void CompressorControlDialog::limiterChanged() +{ + m_ratioKnob->setVisible(!m_controls->m_limiterModel.value()); + m_ratioEnabledLabel->setVisible(!m_controls->m_limiterModel.value()); +} + + +void CompressorControlDialog::updateDisplay() +{ + if (!isVisible()) + { + m_timeElapsed.restart(); + return; + } + + int elapsedMil = m_timeElapsed.elapsed(); + m_timeElapsed.restart(); + m_timeSinceLastUpdate += elapsedMil; + m_compPixelMovement = int(m_timeSinceLastUpdate / COMP_MILLI_PER_PIXEL); + m_timeSinceLastUpdate %= COMP_MILLI_PER_PIXEL; + + // Time Change / Daylight Savings Time protection + if (!m_compPixelMovement || m_compPixelMovement <= 0) + { + return; + } + + if (!m_controls->m_effect->isEnabled() || !m_controls->m_effect->isRunning()) + { + m_controls->m_effect->m_displayPeak[0] = COMP_NOISE_FLOOR; + m_controls->m_effect->m_displayPeak[1] = COMP_NOISE_FLOOR; + m_controls->m_effect->m_displayGain[0] = 1; + m_controls->m_effect->m_displayGain[1] = 1; + } + + m_peakAvg = (m_controls->m_effect->m_displayPeak[0] + m_controls->m_effect->m_displayPeak[1]) * 0.5f; + m_gainAvg = (m_controls->m_effect->m_displayGain[0] + m_controls->m_effect->m_displayGain[1]) * 0.5f; + + m_controls->m_effect->m_displayPeak[0] = m_controls->m_effect->m_yL[0]; + m_controls->m_effect->m_displayPeak[1] = m_controls->m_effect->m_yL[1]; + m_controls->m_effect->m_displayGain[0] = m_controls->m_effect->m_gainResult[0]; + m_controls->m_effect->m_displayGain[1] = m_controls->m_effect->m_gainResult[1]; + + m_yPoint = dbfsToYPoint(ampToDbfs(m_peakAvg)); + m_yGainPoint = dbfsToYPoint(ampToDbfs(m_gainAvg)); + + m_threshYPoint = dbfsToYPoint(m_controls->m_effect->m_thresholdVal); + m_threshXPoint = m_kneeWindowSizeY - m_threshYPoint; + + drawVisPixmap(); + + if (m_controls->m_effect->m_redrawKnee) + { + redrawKnee(); + } + + drawKneePixmap2(); + + if (m_controls->m_effect->m_redrawThreshold) + { + drawMiscPixmap(); + } + + m_lastPoint = m_yPoint; + m_lastGainPoint = m_yGainPoint; + + update(); +} + + +void CompressorControlDialog::drawVisPixmap() +{ + m_p.begin(&m_visPixmap); + + // Move entire display to the left + m_p.setCompositionMode(QPainter::CompositionMode_Source); + m_p.drawPixmap(-m_compPixelMovement, 0, m_visPixmap); + m_p.fillRect(m_windowSizeX-m_compPixelMovement, 0, m_windowSizeX, m_windowSizeY, QColor("transparent")); + m_p.setCompositionMode(QPainter::CompositionMode_SourceOver); + + m_p.setRenderHint(QPainter::Antialiasing, true); + + // Draw translucent portion of input volume line + m_p.setPen(QPen(m_inVolAreaColor, 1)); + for (int i = 0; i < m_compPixelMovement; ++i) + { + const int temp = linearInterpolate(m_lastPoint, m_yPoint, float(i) / float(m_compPixelMovement)); + m_p.drawLine(m_windowSizeX-m_compPixelMovement+i, temp, m_windowSizeX-m_compPixelMovement+i, m_windowSizeY); + } + + // Draw input volume line + m_p.setPen(QPen(m_inVolColor, 1)); + m_p.drawLine(m_windowSizeX-m_compPixelMovement-1, m_lastPoint, m_windowSizeX, m_yPoint); + + // Draw translucent portion of output volume line + m_p.setPen(QPen(m_outVolAreaColor, 1)); + for (int i = 0; i < m_compPixelMovement; ++i) + { + const int temp = linearInterpolate(m_lastPoint+m_lastGainPoint, m_yPoint+m_yGainPoint, float(i) / float(m_compPixelMovement)); + m_p.drawLine(m_windowSizeX-m_compPixelMovement+i, temp, m_windowSizeX-m_compPixelMovement+i, m_windowSizeY); + } + + // Draw output volume line + m_p.setPen(QPen(m_outVolColor, 1)); + m_p.drawLine(m_windowSizeX-m_compPixelMovement-1, m_lastPoint+m_lastGainPoint, m_windowSizeX, m_yPoint+m_yGainPoint); + + // Draw gain reduction line + m_p.setPen(QPen(m_gainReductionColor, 2)); + m_p.drawLine(m_windowSizeX-m_compPixelMovement-1, m_lastGainPoint, m_windowSizeX, m_yGainPoint); + + m_p.end(); +} + + +void CompressorControlDialog::redrawKnee() +{ + m_controls->m_effect->m_redrawKnee = false; + + // Start drawing knee visualizer + m_p.begin(&m_kneePixmap); + + m_p.setRenderHint(QPainter::Antialiasing, false); + + // Clear display + m_p.setCompositionMode(QPainter::CompositionMode_Source); + m_p.fillRect(0, 0, m_windowSizeX, m_kneeWindowSizeY, QColor("transparent")); + m_p.setCompositionMode(QPainter::CompositionMode_SourceOver); + + m_p.setRenderHint(QPainter::Antialiasing, true); + + m_p.setPen(QPen(m_kneeColor, 3)); + + // Limiter = infinite ratio + float actualRatio = m_controls->m_limiterModel.value() ? 0 : m_controls->m_effect->m_ratioVal; + + // Calculate endpoints for the two straight lines + float kneePoint1 = m_controls->m_effect->m_thresholdVal - m_controls->m_effect->m_kneeVal; + float kneePoint2X = m_controls->m_effect->m_thresholdVal + m_controls->m_effect->m_kneeVal; + float kneePoint2Y = (m_controls->m_effect->m_thresholdVal + (-m_controls->m_effect->m_thresholdVal * (actualRatio * (m_controls->m_effect->m_kneeVal / -m_controls->m_effect->m_thresholdVal)))); + float ratioPoint = m_controls->m_effect->m_thresholdVal + (-m_controls->m_effect->m_thresholdVal * actualRatio); + + // Draw two straight lines + m_p.drawLine(0, m_kneeWindowSizeY, dbfsToXPoint(kneePoint1), dbfsToYPoint(kneePoint1)); + if (dbfsToXPoint(kneePoint2X) < m_kneeWindowSizeY) + { + m_p.drawLine(dbfsToXPoint(kneePoint2X), dbfsToYPoint(kneePoint2Y), m_kneeWindowSizeY, dbfsToYPoint(ratioPoint)); + } + + // Draw knee section + if (m_controls->m_effect->m_kneeVal) + { + m_p.setPen(QPen(m_kneeColor2, 3)); + + float prevPoint[2] = {kneePoint1, kneePoint1}; + float newPoint[2] = {0, 0}; + + // Draw knee curve using many straight lines. + for (int i = 0; i < COMP_KNEE_LINES; ++i) + { + newPoint[0] = linearInterpolate(kneePoint1, kneePoint2X, (i + 1) / (float)COMP_KNEE_LINES); + + const float temp = newPoint[0] - m_controls->m_effect->m_thresholdVal + m_controls->m_effect->m_kneeVal; + newPoint[1] = (newPoint[0] + (actualRatio - 1) * temp * temp / (4 * m_controls->m_effect->m_kneeVal)); + + m_p.drawLine(dbfsToXPoint(prevPoint[0]), dbfsToYPoint(prevPoint[1]), dbfsToXPoint(newPoint[0]), dbfsToYPoint(newPoint[1])); + + prevPoint[0] = newPoint[0]; + prevPoint[1] = newPoint[1]; + } + } + + m_p.setRenderHint(QPainter::Antialiasing, false); + + // Erase right portion + m_p.setCompositionMode(QPainter::CompositionMode_Source); + m_p.fillRect(m_kneeWindowSizeX + 1, 0, m_windowSizeX, m_kneeWindowSizeY, QColor("transparent")); + m_p.setCompositionMode(QPainter::CompositionMode_SourceOver); + + m_p.end(); + + m_p.begin(&m_kneePixmap2); + + m_p.setCompositionMode(QPainter::CompositionMode_Source); + m_p.fillRect(0, 0, m_windowSizeX, m_kneeWindowSizeY, QColor("transparent")); + m_p.setCompositionMode(QPainter::CompositionMode_SourceOver); + + m_p.end(); + + m_lastKneePoint = 0; +} + + +void CompressorControlDialog::drawKneePixmap2() +{ + m_p.begin(&m_kneePixmap2); + + m_p.setRenderHint(QPainter::Antialiasing, false); + + int kneePoint = dbfsToXPoint(ampToDbfs(m_peakAvg)); + if (kneePoint > m_lastKneePoint) + { + QRectF knee2Rect = QRect(m_lastKneePoint, 0, kneePoint - m_lastKneePoint, m_kneeWindowSizeY); + m_p.drawPixmap(knee2Rect, m_kneePixmap, knee2Rect); + } + else + { + m_p.setCompositionMode(QPainter::CompositionMode_Source); + m_p.fillRect(kneePoint, 0, m_lastKneePoint, m_kneeWindowSizeY, QColor("transparent")); + m_p.setCompositionMode(QPainter::CompositionMode_SourceOver); + } + m_lastKneePoint = kneePoint; + + m_p.end(); +} + + +void CompressorControlDialog::drawMiscPixmap() +{ + m_p.begin(&m_miscPixmap); + + m_p.setCompositionMode(QPainter::CompositionMode_Source); + m_p.fillRect(0, 0, m_windowSizeX, m_windowSizeY, QColor("transparent")); + m_p.setCompositionMode(QPainter::CompositionMode_SourceOver); + + m_p.setRenderHint(QPainter::Antialiasing, true); + + // Draw threshold lines + m_p.setPen(QPen(m_threshColor, 2, Qt::DotLine)); + m_p.drawLine(0, m_threshYPoint, m_windowSizeX, m_threshYPoint); + m_p.drawLine(m_threshXPoint, 0, m_threshXPoint, m_kneeWindowSizeY); + + m_p.end(); + + m_controls->m_effect->m_redrawThreshold = false; +} + + + +void CompressorControlDialog::paintEvent(QPaintEvent *event) +{ + if (!isVisible()) + { + return; + } + + m_p.begin(this); + + m_p.setCompositionMode(QPainter::CompositionMode_Source); + m_p.fillRect(0, 0, m_windowSizeX, m_windowSizeY, QColor("transparent")); + m_p.setCompositionMode(QPainter::CompositionMode_SourceOver); + + m_p.drawPixmap(0, 0, m_graphPixmap); + m_p.drawPixmap(0, 0, m_visPixmap); + m_p.setOpacity(0.25); + m_p.drawPixmap(0, 0, m_kneePixmap); + m_p.setOpacity(1); + if (m_controls->m_effect->isEnabled() && m_controls->m_effect->isRunning()) + { + m_p.drawPixmap(0, 0, m_kneePixmap2); + } + m_p.drawPixmap(0, 0, m_miscPixmap); + + m_p.end(); +} + + +inline int CompressorControlDialog::dbfsToYPoint(float inDbfs) +{ + return (-((inDbfs + m_dbRange) / m_dbRange) + 1) * m_windowSizeY; +} + +inline int CompressorControlDialog::dbfsToXPoint(float inDbfs) +{ + return m_kneeWindowSizeY - dbfsToYPoint(inDbfs); +} + + +void CompressorControlDialog::resizeEvent(QResizeEvent *event) +{ + resetCompressorView(); +} + + +void CompressorControlDialog::wheelEvent(QWheelEvent * event) +{ + const float temp = m_dbRange; + const float dbRangeNew = m_dbRange - copysignf(COMP_GRID_SPACING, event->delta()); + m_dbRange = round(qBound(COMP_GRID_SPACING, dbRangeNew, COMP_GRID_MAX) / COMP_GRID_SPACING) * COMP_GRID_SPACING; + + // Only reset view if the scolling had an effect + if (m_dbRange != temp) + { + drawGraph(); + m_controls->m_effect->m_redrawKnee = true; + m_controls->m_effect->m_redrawThreshold = true; + } +} + + +void CompressorControlDialog::drawGraph() +{ + m_p.begin(&m_graphPixmap); + + m_p.setRenderHint(QPainter::Antialiasing, false); + + m_p.setCompositionMode(QPainter::CompositionMode_Source); + m_p.fillRect(0, 0, m_windowSizeX, m_windowSizeY, QColor("transparent")); + m_p.setCompositionMode(QPainter::CompositionMode_SourceOver); + + m_p.setPen(QPen(m_textColor, 1)); + + // Arbitrary formula for increasing font size when window size increases + m_p.setFont(QFont("Arial", qMax(int(m_windowSizeY / 1080.f * 24), 12))); + + // Redraw graph + m_p.setPen(QPen(m_graphColor, 1)); + for (int i = 1; i < m_dbRange / COMP_GRID_SPACING + 1; ++i) + { + m_p.drawLine(0, dbfsToYPoint(-COMP_GRID_SPACING * i), m_windowSizeX, dbfsToYPoint(-COMP_GRID_SPACING * i)); + m_p.drawLine(dbfsToXPoint(-COMP_GRID_SPACING * i), 0, dbfsToXPoint(-COMP_GRID_SPACING * i), m_kneeWindowSizeY); + m_p.drawText(QRectF(m_windowSizeX - 50, dbfsToYPoint(-COMP_GRID_SPACING * i), 50, 50), Qt::AlignRight | Qt::AlignTop, QString::number(i * -COMP_GRID_SPACING)); + } + + m_p.end(); +} + + +void CompressorControlDialog::resetCompressorView() +{ + m_windowSizeX = size().width(); + m_windowSizeY = size().height(); + m_kneeWindowSizeX = m_windowSizeY; + m_kneeWindowSizeY = m_windowSizeY; + m_controlsBoxX = (m_windowSizeX - COMP_BOX_X) * 0.5; + m_controlsBoxY = m_windowSizeY - 40 - COMP_BOX_Y; + + m_controls->m_effect->m_redrawKnee = true; + m_controls->m_effect->m_redrawThreshold = true; + m_lastKneePoint = 0; + + drawGraph(); + + m_p.begin(&m_visPixmap); + + m_p.setCompositionMode(QPainter::CompositionMode_Source); + m_p.fillRect(0, 0, m_windowSizeX, m_windowSizeY, QColor("transparent")); + m_p.setCompositionMode(QPainter::CompositionMode_SourceOver); + + // Draw line at right side, so the sudden + // content that the visualizer will display + // later on won't look too ugly + m_p.setPen(QPen(m_resetColor, 3)); + m_p.drawLine(m_windowSizeX, 0, m_windowSizeX, m_windowSizeY); + + m_p.end(); + + m_controlsBoxLabel->move(m_controlsBoxX, m_controlsBoxY); + m_rmsEnabledLabel->move(m_controlsBoxX + 429, m_controlsBoxY + 209); + m_blendEnabledLabel->move(m_controlsBoxX + 587, m_controlsBoxY + 197); + m_lookaheadEnabledLabel->move(m_controlsBoxX + 221, m_controlsBoxY + 135); + m_ratioEnabledLabel->move(m_controlsBoxX + 267, m_controlsBoxY + 21); + + m_thresholdKnob->move(m_controlsBoxX + 137, m_controlsBoxY + 21); + m_ratioKnob->move(m_controlsBoxX + 267, m_controlsBoxY + 21); + m_attackKnob->move(m_controlsBoxX + 397, m_controlsBoxY + 21); + m_releaseKnob->move(m_controlsBoxX + 527, m_controlsBoxY + 21); + m_kneeKnob->move(m_controlsBoxX + 97, m_controlsBoxY + 135); + m_rangeKnob->move(m_controlsBoxX + 159, m_controlsBoxY + 135); + m_lookaheadLengthKnob->move(m_controlsBoxX + 221, m_controlsBoxY + 135); + m_holdKnob->move(m_controlsBoxX + 283, m_controlsBoxY + 135); + + m_rmsKnob->move(m_controlsBoxX + 429, m_controlsBoxY + 209); + m_inBalanceKnob->move(m_controlsBoxX + 27, m_controlsBoxY + 219); + m_outBalanceKnob->move(m_controlsBoxX + 662, m_controlsBoxY + 219); + m_stereoBalanceKnob->move(m_controlsBoxX + 522, m_controlsBoxY + 137); + m_blendKnob->move(m_controlsBoxX + 587, m_controlsBoxY + 197); + m_tiltKnob->move(m_controlsBoxX + 364, m_controlsBoxY + 138); + m_tiltFreqKnob->move(m_controlsBoxX + 415, m_controlsBoxY + 138); + m_mixKnob->move(m_controlsBoxX + 27, m_controlsBoxY + 13); + + m_outFader->move(m_controlsBoxX + 666, m_controlsBoxY + 91); + m_inFader->move(m_controlsBoxX + 31, m_controlsBoxY + 91); + + rmsButton->move(m_controlsBoxX + 337, m_controlsBoxY + 231); + peakButton->move(m_controlsBoxX + 337, m_controlsBoxY + 248); + + leftRightButton->move(m_controlsBoxX + 220, m_controlsBoxY + 231); + midSideButton->move(m_controlsBoxX + 220, m_controlsBoxY + 248); + + compressButton->move(m_controlsBoxX + 98, m_controlsBoxY + 231); + limitButton->move(m_controlsBoxX + 98, m_controlsBoxY + 248); + + unlinkedButton->move(m_controlsBoxX + 495, m_controlsBoxY + 180); + maximumButton->move(m_controlsBoxX + 495, m_controlsBoxY + 197); + averageButton->move(m_controlsBoxX + 495, m_controlsBoxY + 214); + minimumButton->move(m_controlsBoxX + 495, m_controlsBoxY + 231); + blendButton->move(m_controlsBoxX + 495, m_controlsBoxY + 248); + + autoMakeupButton->move(m_controlsBoxX + 220, m_controlsBoxY + 206); + auditionButton->move(m_controlsBoxX + 658, m_controlsBoxY + 14); + feedbackButton->move(m_controlsBoxX + 98, m_controlsBoxY + 206); + m_autoAttackKnob->move(m_controlsBoxX + 460, m_controlsBoxY + 38); + m_autoReleaseKnob->move(m_controlsBoxX + 590, m_controlsBoxY + 38); + lookaheadButton->move(m_controlsBoxX + 202, m_controlsBoxY + 171); +} diff --git a/plugins/Compressor/CompressorControlDialog.h b/plugins/Compressor/CompressorControlDialog.h new file mode 100755 index 00000000000..7eb19c5b9f1 --- /dev/null +++ b/plugins/Compressor/CompressorControlDialog.h @@ -0,0 +1,218 @@ +/* + * CompressorControlDialog.h + * + * Copyright (c) 2020 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 COMPRESSOR_CONTROL_DIALOG_H +#define COMPRESSOR_CONTROL_DIALOG_H + +#include +#include +#include +#include +#include + +#include "../Eq/EqFader.h" +#include "EffectControlDialog.h" +#include "GuiApplication.h" +#include "Knob.h" +#include "MainWindow.h" +#include "PixmapButton.h" + + +constexpr int COMP_MILLI_PER_PIXEL = 6; +constexpr int MIN_COMP_SCREEN_X = 800; +constexpr int MIN_COMP_SCREEN_Y = 360; +constexpr int MAX_COMP_SCREEN_X = 1920; +constexpr int MAX_COMP_SCREEN_Y = 1080; +constexpr int COMP_SCREEN_X = 800; +constexpr int COMP_SCREEN_Y = 560; +constexpr int KNEE_SCREEN_X = COMP_SCREEN_Y; +constexpr int KNEE_SCREEN_Y = COMP_SCREEN_Y; +constexpr int COMP_KNEE_LINES = 20; +constexpr int COMP_BOX_X = 720; +constexpr int COMP_BOX_Y = 280; +constexpr float COMP_GRID_SPACING = 3.f;// 3 db per grid line +constexpr float COMP_GRID_MAX = 96.f;// Can't zoom out past 96 db + +constexpr float COMP_NOISE_FLOOR = 0.000001;// -120 dbFs + + + +class CompressorControls; + +class CompressorControlDialog : public EffectControlDialog +{ + Q_OBJECT +public: + CompressorControlDialog(CompressorControls* controls); + + bool isResizable() const override {return true;} + QSize sizeHint() const override {return QSize(COMP_SCREEN_X, COMP_SCREEN_Y);} + + // For theming purposes + Q_PROPERTY(QColor inVolAreaColor MEMBER m_inVolAreaColor) + Q_PROPERTY(QColor inVolColor MEMBER m_inVolColor) + Q_PROPERTY(QColor outVolAreaColor MEMBER m_outVolAreaColor) + Q_PROPERTY(QColor outVolColor MEMBER m_outVolColor) + Q_PROPERTY(QColor gainReductionColor MEMBER m_gainReductionColor) + Q_PROPERTY(QColor kneeColor MEMBER m_kneeColor) + Q_PROPERTY(QColor kneeColor2 MEMBER m_kneeColor2) + Q_PROPERTY(QColor threshColor MEMBER m_threshColor) + Q_PROPERTY(QColor textColor MEMBER m_textColor) + Q_PROPERTY(QColor graphColor MEMBER m_graphColor) + Q_PROPERTY(QColor resetColor MEMBER m_resetColor) + +protected: + void resizeEvent(QResizeEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + +private slots: + void updateDisplay(); + void peakmodeChanged(); + void stereoLinkChanged(); + void lookaheadChanged(); + void limiterChanged(); + +private: + void makeLargeKnob(Knob * knob, QString hint, QString unit); + void makeSmallKnob(Knob * knob, QString hint, QString unit); + void resetCompressorView(); + void drawVisPixmap(); + void redrawKnee(); + void drawKneePixmap2(); + void drawMiscPixmap(); + void drawGraph(); + + QPainter m_p; + + QBasicTimer m_updateTimer; + + CompressorControls * m_controls; + + inline int dbfsToYPoint(float inDbfs); + inline int dbfsToXPoint(float inDbfs); + + QPixmap m_visPixmap = QPixmap(MAX_COMP_SCREEN_X, MAX_COMP_SCREEN_Y); + QPixmap m_kneePixmap = QPixmap(MAX_COMP_SCREEN_X, MAX_COMP_SCREEN_Y); + QPixmap m_kneePixmap2 = QPixmap(MAX_COMP_SCREEN_X, MAX_COMP_SCREEN_Y); + QPixmap m_miscPixmap = QPixmap(MAX_COMP_SCREEN_X, MAX_COMP_SCREEN_Y); + QPixmap m_graphPixmap = QPixmap(MAX_COMP_SCREEN_X, MAX_COMP_SCREEN_Y); + + int m_lastPoint; + int m_lastGainPoint; + int m_lastKneePoint = 0; + + int m_windowSizeX = size().width(); + int m_windowSizeY = size().height(); + int m_kneeWindowSizeX = m_windowSizeY; + int m_kneeWindowSizeY = m_windowSizeY; + int m_controlsBoxX = 0; + int m_controlsBoxY = 0; + + float m_dbRange = 36; + + QColor m_inVolAreaColor = QColor(209, 216, 228, 17); + QColor m_inVolColor = QColor(209, 216, 228, 100); + QColor m_outVolAreaColor = QColor(209, 216, 228, 30); + QColor m_outVolColor = QColor(209, 216, 228, 240); + QColor m_gainReductionColor = QColor(180, 100, 100, 210); + QColor m_kneeColor = QColor(39, 171, 95, 255); + QColor m_kneeColor2 = QColor(9, 171, 160, 255); + QColor m_threshColor = QColor(39, 171, 95, 100); + QColor m_textColor = QColor(209, 216, 228, 50); + QColor m_graphColor = QColor(209, 216, 228, 50); + QColor m_resetColor = QColor(200, 100, 15, 200); + + float m_peakAvg; + float m_gainAvg; + + float m_yPoint; + float m_yGainPoint; + + int m_threshYPoint; + int m_threshXPoint; + + int m_compPixelMovement; + + QLabel * m_controlsBoxLabel; + QLabel * m_rmsEnabledLabel; + QLabel * m_blendEnabledLabel; + QLabel * m_lookaheadEnabledLabel; + QLabel * m_ratioEnabledLabel; + + Knob * m_thresholdKnob; + Knob * m_ratioKnob; + Knob * m_attackKnob; + Knob * m_releaseKnob; + Knob * m_kneeKnob; + Knob * m_rangeKnob; + Knob * m_lookaheadLengthKnob; + Knob * m_holdKnob; + + Knob * m_rmsKnob; + Knob * m_inBalanceKnob; + Knob * m_outBalanceKnob; + Knob * m_stereoBalanceKnob; + Knob * m_blendKnob; + Knob * m_tiltKnob; + Knob * m_tiltFreqKnob; + Knob * m_mixKnob; + + Knob * m_autoAttackKnob; + Knob * m_autoReleaseKnob; + + EqFader * m_outFader; + EqFader * m_inFader; + + PixmapButton * rmsButton; + PixmapButton * peakButton; + automatableButtonGroup * rmsPeakGroup; + + PixmapButton * leftRightButton; + PixmapButton * midSideButton; + automatableButtonGroup * leftRightMidSideGroup; + + PixmapButton * compressButton; + PixmapButton * limitButton; + automatableButtonGroup * compressLimitGroup; + + PixmapButton * unlinkedButton; + PixmapButton * maximumButton; + PixmapButton * averageButton; + PixmapButton * minimumButton; + PixmapButton * blendButton; + automatableButtonGroup * stereoLinkGroup; + + PixmapButton * autoMakeupButton; + PixmapButton * auditionButton; + PixmapButton * feedbackButton; + PixmapButton * lookaheadButton; + + QElapsedTimer m_timeElapsed; + int m_timeSinceLastUpdate = 0; + + friend class CompressorControls; +} ; + +#endif diff --git a/plugins/Compressor/CompressorControls.cpp b/plugins/Compressor/CompressorControls.cpp new file mode 100755 index 00000000000..772fb5261ee --- /dev/null +++ b/plugins/Compressor/CompressorControls.cpp @@ -0,0 +1,147 @@ +/* + * CompressorControls.cpp + * + * Copyright (c) 2020 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 "CompressorControls.h" +#include "Compressor.h" + +#include + +#include "Engine.h" +#include "Song.h" + + +CompressorControls::CompressorControls(CompressorEffect* effect) : + EffectControls(effect), + m_effect(effect), + m_thresholdModel(-8.0f, -60.0f, 0.0f, 0.001f, this, tr("Threshold")), + m_ratioModel(1.8f, 1.0f, 20.0f, 0.001f, this, tr("Ratio")), + m_attackModel(10.0f, 0.005f, 250.f, 0.001f, this, tr("Attack")), + m_releaseModel(100.0f, 1.f, 2500.f, 0.001f, this, tr("Release")), + m_kneeModel(12.0f, 0.0f, 96.0f, 0.01f, this, tr("Knee")), + m_holdModel(0.0f, 0.0f, 500.0f, 0.01f, this, tr("Hold")), + m_rangeModel(-240.0f, -240.0f, 0.0f, 0.01f, this, tr("Range")), + m_rmsModel(1.0f, 0.0f, 250.0f, 0.01f, this, tr("RMS Size")), + m_midsideModel(0.0f, 0.0f, 1.0f, this, tr("Mid/Side")), + m_peakmodeModel(0.0f, 0.0f, 1.0f, this, tr("Peak Mode")), + m_lookaheadLengthModel(0.0f, 0.0f, 20.0f, 0.0001f, this, tr("Lookahead Length")), + m_inBalanceModel(0.0f, -1.0f, 1.0f, 0.0001f, this, tr("Input Balance")), + m_outBalanceModel(0.0f, -1.0f, 1.0f, 0.0001f, this, tr("Output Balance")), + m_limiterModel(0.f, 0.f, 1.0f, this, tr("Limiter")), + m_outGainModel(0.f, -60.f, 30.f, 0.01f, this, tr("Output Gain")), + m_inGainModel(0.f, -60.f, 30.f, 0.01f, this, tr("Input Gain")), + m_blendModel(1.f, 0.f, 3.f, 0.0001f, this, tr("Blend")), + m_stereoBalanceModel(0.0f, -1.0f, 1.0f, 0.0001f, this, tr("Stereo Balance")), + m_autoMakeupModel(false, this, tr("Auto Makeup Gain")), + m_auditionModel(false, this, tr("Audition")), + m_feedbackModel(false, this, tr("Feedback")), + m_autoAttackModel(0.0f, 0.f, 100.0f, 0.01f, this, tr("Auto Attack")), + m_autoReleaseModel(0.0f, 0.f, 100.0f, 0.01f, this, tr("Auto Release")), + m_lookaheadModel(false, this, tr("Lookahead")), + m_tiltModel(0.0f, -6.0f, 6.0f, 0.0001f, this, tr("Tilt")), + m_tiltFreqModel(150.0f, 20.0f, 20000.0f, 0.1f, this, tr("Tilt Frequency")), + m_stereoLinkModel(1.0f, 0.0f, 4.0f, this, tr("Stereo Link")), + m_mixModel(100.0f, 0.f, 100.0f, 0.01f, this, tr("Mix")) +{ + m_ratioModel.setScaleLogarithmic(true); + m_holdModel.setScaleLogarithmic(true); + m_attackModel.setScaleLogarithmic(true); + m_releaseModel.setScaleLogarithmic(true); + m_thresholdModel.setScaleLogarithmic(true); + m_rangeModel.setScaleLogarithmic(true); + m_lookaheadLengthModel.setScaleLogarithmic(true); + m_rmsModel.setScaleLogarithmic(true); + m_kneeModel.setScaleLogarithmic(true); + m_tiltFreqModel.setScaleLogarithmic(true); + m_rangeModel.setScaleLogarithmic(true); +} + + +void CompressorControls::saveSettings(QDomDocument& doc, QDomElement& _this) +{ + m_thresholdModel.saveSettings(doc, _this, "threshold"); + m_ratioModel.saveSettings(doc, _this, "ratio"); + m_attackModel.saveSettings(doc, _this, "attack"); + m_releaseModel.saveSettings(doc, _this, "release"); + m_kneeModel.saveSettings(doc, _this, "knee"); + m_holdModel.saveSettings(doc, _this, "hold"); + m_rangeModel.saveSettings(doc, _this, "range"); + m_rmsModel.saveSettings(doc, _this, "rms"); + m_midsideModel.saveSettings(doc, _this, "midside"); + m_peakmodeModel.saveSettings(doc, _this, "peakmode"); + m_lookaheadLengthModel.saveSettings(doc, _this, "lookaheadLength"); + m_inBalanceModel.saveSettings(doc, _this, "inBalance"); + m_outBalanceModel.saveSettings(doc, _this, "outBalance"); + m_limiterModel.saveSettings(doc, _this, "limiter"); + m_outGainModel.saveSettings(doc, _this, "outGain"); + m_inGainModel.saveSettings(doc, _this, "inGain"); + m_blendModel.saveSettings(doc, _this, "blend"); + m_stereoBalanceModel.saveSettings(doc, _this, "stereoBalance"); + m_autoMakeupModel.saveSettings(doc, _this, "autoMakeup"); + m_auditionModel.saveSettings(doc, _this, "audition"); + m_feedbackModel.saveSettings(doc, _this, "feedback"); + m_autoAttackModel.saveSettings(doc, _this, "autoAttack"); + m_autoReleaseModel.saveSettings(doc, _this, "autoRelease"); + m_lookaheadModel.saveSettings(doc, _this, "lookahead"); + m_tiltModel.saveSettings(doc, _this, "tilt"); + m_tiltFreqModel.saveSettings(doc, _this, "tiltFreq"); + m_stereoLinkModel.saveSettings(doc, _this, "stereoLink"); + m_mixModel.saveSettings(doc, _this, "mix"); +} + + + +void CompressorControls::loadSettings(const QDomElement& _this) +{ + m_thresholdModel.loadSettings(_this, "threshold"); + m_ratioModel.loadSettings(_this, "ratio"); + m_attackModel.loadSettings(_this, "attack"); + m_releaseModel.loadSettings(_this, "release"); + m_kneeModel.loadSettings(_this, "knee"); + m_holdModel.loadSettings(_this, "hold"); + m_rangeModel.loadSettings(_this, "range"); + m_rmsModel.loadSettings(_this, "rms"); + m_midsideModel.loadSettings(_this, "midside"); + m_peakmodeModel.loadSettings(_this, "peakmode"); + m_lookaheadLengthModel.loadSettings(_this, "lookaheadLength"); + m_inBalanceModel.loadSettings(_this, "inBalance"); + m_outBalanceModel.loadSettings(_this, "outBalance"); + m_limiterModel.loadSettings(_this, "limiter"); + m_outGainModel.loadSettings(_this, "outGain"); + m_inGainModel.loadSettings(_this, "inGain"); + m_blendModel.loadSettings(_this, "blend"); + m_stereoBalanceModel.loadSettings(_this, "stereoBalance"); + m_autoMakeupModel.loadSettings(_this, "autoMakeup"); + m_auditionModel.loadSettings(_this, "audition"); + m_feedbackModel.loadSettings(_this, "feedback"); + m_autoAttackModel.loadSettings(_this, "autoAttack"); + m_autoReleaseModel.loadSettings(_this, "autoRelease"); + m_lookaheadModel.loadSettings(_this, "lookahead"); + m_tiltModel.loadSettings(_this, "tilt"); + m_tiltFreqModel.loadSettings(_this, "tiltFreq"); + m_stereoLinkModel.loadSettings(_this, "stereoLink"); + m_mixModel.loadSettings(_this, "mix"); +} + + diff --git a/plugins/Compressor/CompressorControls.h b/plugins/Compressor/CompressorControls.h new file mode 100755 index 00000000000..775a3be89f5 --- /dev/null +++ b/plugins/Compressor/CompressorControls.h @@ -0,0 +1,102 @@ +/* + * CompressorControls.h + * + * Copyright (c) 2020 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 COMPRESSOR_CONTROLS_H +#define COMPRESSOR_CONTROLS_H + +#include "CompressorControlDialog.h" + +#include "EffectControls.h" +#include "Knob.h" + + +class CompressorEffect; + + +class CompressorControls : public EffectControls +{ + Q_OBJECT +public: + CompressorControls(CompressorEffect* effect); + + void saveSettings(QDomDocument & _doc, QDomElement & _parent) override; + void loadSettings(const QDomElement & _this) override; + inline QString nodeName() const override + { + return "CompressorControls"; + } + + int controlCount() override + { + return 28; + } + + EffectControlDialog* createView() override + { + return new CompressorControlDialog(this); + } + +private: + CompressorEffect * m_effect; + + FloatModel m_thresholdModel; + FloatModel m_ratioModel; + FloatModel m_attackModel; + FloatModel m_releaseModel; + FloatModel m_kneeModel; + FloatModel m_holdModel; + FloatModel m_rangeModel; + FloatModel m_rmsModel; + IntModel m_midsideModel; + IntModel m_peakmodeModel; + FloatModel m_lookaheadLengthModel; + FloatModel m_inBalanceModel; + FloatModel m_outBalanceModel; + IntModel m_limiterModel; + FloatModel m_outGainModel; + FloatModel m_inGainModel; + FloatModel m_blendModel; + FloatModel m_stereoBalanceModel; + BoolModel m_autoMakeupModel; + BoolModel m_auditionModel; + BoolModel m_feedbackModel; + FloatModel m_autoAttackModel; + FloatModel m_autoReleaseModel; + BoolModel m_lookaheadModel; + FloatModel m_tiltModel; + FloatModel m_tiltFreqModel; + IntModel m_stereoLinkModel; + FloatModel m_mixModel; + + float m_inPeakL; + float m_inPeakR; + float m_outPeakL; + float m_outPeakR; + + friend class CompressorControlDialog; + friend class CompressorEffect; + +} ; + +#endif diff --git a/plugins/Compressor/artwork.png b/plugins/Compressor/artwork.png new file mode 100755 index 00000000000..8f94e3d494b Binary files /dev/null and b/plugins/Compressor/artwork.png differ diff --git a/plugins/Compressor/audition_sel.png b/plugins/Compressor/audition_sel.png new file mode 100644 index 00000000000..07ad76049da Binary files /dev/null and b/plugins/Compressor/audition_sel.png differ diff --git a/plugins/Compressor/audition_unsel.png b/plugins/Compressor/audition_unsel.png new file mode 100644 index 00000000000..3dc7f671cdc Binary files /dev/null and b/plugins/Compressor/audition_unsel.png differ diff --git a/plugins/Compressor/autogain_sel.png b/plugins/Compressor/autogain_sel.png new file mode 100644 index 00000000000..e61054a3e29 Binary files /dev/null and b/plugins/Compressor/autogain_sel.png differ diff --git a/plugins/Compressor/autogain_unsel.png b/plugins/Compressor/autogain_unsel.png new file mode 100644 index 00000000000..6491461dfe3 Binary files /dev/null and b/plugins/Compressor/autogain_unsel.png differ diff --git a/plugins/Compressor/average_sel.png b/plugins/Compressor/average_sel.png new file mode 100755 index 00000000000..a5e87918ee3 Binary files /dev/null and b/plugins/Compressor/average_sel.png differ diff --git a/plugins/Compressor/average_unsel.png b/plugins/Compressor/average_unsel.png new file mode 100755 index 00000000000..b7b4d9bd6ed Binary files /dev/null and b/plugins/Compressor/average_unsel.png differ diff --git a/plugins/Compressor/blend_sel.png b/plugins/Compressor/blend_sel.png new file mode 100755 index 00000000000..6cc6f817c73 Binary files /dev/null and b/plugins/Compressor/blend_sel.png differ diff --git a/plugins/Compressor/blend_unsel.png b/plugins/Compressor/blend_unsel.png new file mode 100755 index 00000000000..bba695f82e8 Binary files /dev/null and b/plugins/Compressor/blend_unsel.png differ diff --git a/plugins/Compressor/compressor_sel.png b/plugins/Compressor/compressor_sel.png new file mode 100755 index 00000000000..0d02d73a502 Binary files /dev/null and b/plugins/Compressor/compressor_sel.png differ diff --git a/plugins/Compressor/compressor_unsel.png b/plugins/Compressor/compressor_unsel.png new file mode 100755 index 00000000000..887c0f1ed73 Binary files /dev/null and b/plugins/Compressor/compressor_unsel.png differ diff --git a/plugins/Compressor/controlsBox.png b/plugins/Compressor/controlsBox.png new file mode 100755 index 00000000000..0c46797e910 Binary files /dev/null and b/plugins/Compressor/controlsBox.png differ diff --git a/plugins/Compressor/feedback_sel.png b/plugins/Compressor/feedback_sel.png new file mode 100644 index 00000000000..6488c91d3c3 Binary files /dev/null and b/plugins/Compressor/feedback_sel.png differ diff --git a/plugins/Compressor/feedback_unsel.png b/plugins/Compressor/feedback_unsel.png new file mode 100644 index 00000000000..b4d3cb98e8b Binary files /dev/null and b/plugins/Compressor/feedback_unsel.png differ diff --git a/plugins/Compressor/knob_enabled.png b/plugins/Compressor/knob_enabled.png new file mode 100644 index 00000000000..d8bdc6e64f6 Binary files /dev/null and b/plugins/Compressor/knob_enabled.png differ diff --git a/plugins/Compressor/knob_enabled_large.png b/plugins/Compressor/knob_enabled_large.png new file mode 100644 index 00000000000..929cf55c8b2 Binary files /dev/null and b/plugins/Compressor/knob_enabled_large.png differ diff --git a/plugins/Compressor/leftright_sel.png b/plugins/Compressor/leftright_sel.png new file mode 100755 index 00000000000..9949411da47 Binary files /dev/null and b/plugins/Compressor/leftright_sel.png differ diff --git a/plugins/Compressor/leftright_unsel.png b/plugins/Compressor/leftright_unsel.png new file mode 100755 index 00000000000..4c15656be52 Binary files /dev/null and b/plugins/Compressor/leftright_unsel.png differ diff --git a/plugins/Compressor/limiter_sel.png b/plugins/Compressor/limiter_sel.png new file mode 100755 index 00000000000..2d0e054af8c Binary files /dev/null and b/plugins/Compressor/limiter_sel.png differ diff --git a/plugins/Compressor/limiter_unsel.png b/plugins/Compressor/limiter_unsel.png new file mode 100755 index 00000000000..415641c5f35 Binary files /dev/null and b/plugins/Compressor/limiter_unsel.png differ diff --git a/plugins/Compressor/logo.png b/plugins/Compressor/logo.png new file mode 100755 index 00000000000..9340da708dd Binary files /dev/null and b/plugins/Compressor/logo.png differ diff --git a/plugins/Compressor/lookahead_sel.png b/plugins/Compressor/lookahead_sel.png new file mode 100644 index 00000000000..8633b4d119c Binary files /dev/null and b/plugins/Compressor/lookahead_sel.png differ diff --git a/plugins/Compressor/lookahead_unsel.png b/plugins/Compressor/lookahead_unsel.png new file mode 100644 index 00000000000..6818ef7d8bf Binary files /dev/null and b/plugins/Compressor/lookahead_unsel.png differ diff --git a/plugins/Compressor/maximum_sel.png b/plugins/Compressor/maximum_sel.png new file mode 100755 index 00000000000..7e9e9ce7449 Binary files /dev/null and b/plugins/Compressor/maximum_sel.png differ diff --git a/plugins/Compressor/maximum_unsel.png b/plugins/Compressor/maximum_unsel.png new file mode 100755 index 00000000000..c389c41297f Binary files /dev/null and b/plugins/Compressor/maximum_unsel.png differ diff --git a/plugins/Compressor/midside_sel.png b/plugins/Compressor/midside_sel.png new file mode 100755 index 00000000000..4ceeff92fba Binary files /dev/null and b/plugins/Compressor/midside_sel.png differ diff --git a/plugins/Compressor/midside_unsel.png b/plugins/Compressor/midside_unsel.png new file mode 100755 index 00000000000..8f393619506 Binary files /dev/null and b/plugins/Compressor/midside_unsel.png differ diff --git a/plugins/Compressor/minimum_sel.png b/plugins/Compressor/minimum_sel.png new file mode 100755 index 00000000000..f6d258ce327 Binary files /dev/null and b/plugins/Compressor/minimum_sel.png differ diff --git a/plugins/Compressor/minimum_unsel.png b/plugins/Compressor/minimum_unsel.png new file mode 100755 index 00000000000..4ebacd6c657 Binary files /dev/null and b/plugins/Compressor/minimum_unsel.png differ diff --git a/plugins/Compressor/peak_sel.png b/plugins/Compressor/peak_sel.png new file mode 100755 index 00000000000..8cdda92eaf7 Binary files /dev/null and b/plugins/Compressor/peak_sel.png differ diff --git a/plugins/Compressor/peak_unsel.png b/plugins/Compressor/peak_unsel.png new file mode 100755 index 00000000000..91673fa96f7 Binary files /dev/null and b/plugins/Compressor/peak_unsel.png differ diff --git a/plugins/Compressor/rms_sel.png b/plugins/Compressor/rms_sel.png new file mode 100755 index 00000000000..aa082a2f1ca Binary files /dev/null and b/plugins/Compressor/rms_sel.png differ diff --git a/plugins/Compressor/rms_unsel.png b/plugins/Compressor/rms_unsel.png new file mode 100755 index 00000000000..0221c7243e9 Binary files /dev/null and b/plugins/Compressor/rms_unsel.png differ diff --git a/plugins/Compressor/unlinked_sel.png b/plugins/Compressor/unlinked_sel.png new file mode 100755 index 00000000000..737bf95c743 Binary files /dev/null and b/plugins/Compressor/unlinked_sel.png differ diff --git a/plugins/Compressor/unlinked_unsel.png b/plugins/Compressor/unlinked_unsel.png new file mode 100755 index 00000000000..9e21cad28aa Binary files /dev/null and b/plugins/Compressor/unlinked_unsel.png differ