From 61e88b39ab39a5b8d8d7386a662ede4fa34aa2ee Mon Sep 17 00:00:00 2001 From: Max Maisel Date: Fri, 21 Feb 2020 11:23:38 +0100 Subject: [PATCH] Implement sliding RMS and sliding max sample preprocessors. Signed-off-by: Max Maisel --- src/effects/Compressor2.cpp | 154 ++++++++++++++++++++++++++++++++++-- src/effects/Compressor2.h | 50 ++++++++++++ 2 files changed, 196 insertions(+), 8 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 1e61ec65b..f7f5d03ed 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -97,6 +97,105 @@ const ComponentInterfaceSymbol EffectCompressor2::Symbol namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; } +SlidingRmsPreprocessor::SlidingRmsPreprocessor(size_t windowSize, float gain) + : mSum(0), + mGain(gain), + mWindow(windowSize, 0), + mPos(0), + mInsertCount(0) +{ +} + +float SlidingRmsPreprocessor::ProcessSample(float value) +{ + return DoProcessSample(value * value); +} + +float SlidingRmsPreprocessor::ProcessSample(float valueL, float valueR) +{ + return DoProcessSample((valueL * valueL + valueR * valueR) / 2.0); +} + +float SlidingRmsPreprocessor::DoProcessSample(float value) +{ + if(mInsertCount > REFRESH_WINDOW_EVERY) + { + // Update RMS sum directly from the circle buffer every + // REFRESH_WINDOW_EVERY samples to avoid accumulation of rounding errors. + mWindow[mPos] = value; + Refresh(); + } + else + { + // Calculate current level from root-mean-squared of + // circular buffer ("RMS"). + mSum -= mWindow[mPos]; + mWindow[mPos] = value; + mSum += mWindow[mPos]; + ++mInsertCount; + } + + // Also refresh if there are severe rounding errors that + // caused mRMSSum to be negative. + if(mSum < 0) + Refresh(); + + mPos = (mPos + 1) % mWindow.size(); + + // Multiply by gain (usually two) to approximately correct peak level + // of standard audio (avoid clipping). + return mGain * sqrt(mSum/float(mWindow.size())); +} + +void SlidingRmsPreprocessor::Refresh() +{ + // Recompute the RMS sum periodically to prevent accumulation + // of rounding errors during long waveforms. + mSum = 0; + for(const auto& sample : mWindow) + mSum += sample; + mInsertCount = 0; +} + +SlidingMaxPreprocessor::SlidingMaxPreprocessor(size_t windowSize) + : mWindow(windowSize, 0), + mMaxes(windowSize, 0), + mPos(0) +{ +} + +float SlidingMaxPreprocessor::ProcessSample(float value) +{ + return DoProcessSample(value); +} + +float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR) +{ + return DoProcessSample((fabs(valueL) + fabs(valueR)) / 2.0); +} + +float SlidingMaxPreprocessor::DoProcessSample(float value) +{ + size_t oldHead = (mPos-1) % mWindow.size(); + size_t currentHead = mPos; + size_t nextHead = (mPos+1) % mWindow.size(); + mWindow[mPos] = value; + mMaxes[mPos] = std::max(value, mMaxes[oldHead]); + + if(mPos % ((mWindow.size()+1)/2) == 0) + { + mMaxes[mPos] = mWindow[mPos]; + for(size_t i = 1; i < mWindow.size(); ++i) + { + size_t pos1 = (mPos-i+mWindow.size()) % mWindow.size(); + size_t pos2 = (mPos-i+mWindow.size()+1) % mWindow.size(); + mMaxes[pos1] = std::max(mWindow[pos1], mMaxes[pos2]); + } + } + mPos = nextHead; + return std::max(mMaxes[currentHead], mMaxes[nextHead]); +} + EffectCompressor2::EffectCompressor2() : mIgnoreGuiEvents(false) { @@ -282,7 +381,8 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) plot = mResponsePlot->GetPlotData(0); plot->pen = std::unique_ptr( safenew wxPen(AColor::WideEnvelopePen)); - plot->xdata = {0, 2, 2, 3, 3, 5}; + plot->xdata = {0, RESPONSE_PLOT_STEP_START, RESPONSE_PLOT_STEP_START, + RESPONSE_PLOT_STEP_STOP, RESPONSE_PLOT_STEP_STOP, 5}; plot->ydata = {0, 0, 1, 1, 0, 0}; plot = mResponsePlot->GetPlotData(1); @@ -290,6 +390,10 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) safenew wxPen(AColor::WideEnvelopePen)); plot->pen->SetColour(wxColor( 230,80,80 )); // Same color as TrackArtist RMS red. plot->pen->SetWidth(2); + plot->xdata.resize(RESPONSE_PLOT_SAMPLES+1); + plot->ydata.resize(RESPONSE_PLOT_SAMPLES+1); + for(size_t x = 0; x < plot->xdata.size(); ++x) + plot->xdata[x] = x * float(RESPONSE_PLOT_TIME) / float(RESPONSE_PLOT_SAMPLES); } S.EndHorizontalLay(); @@ -519,13 +623,7 @@ void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) void EffectCompressor2::UpdateUI() { UpdateCompressorPlot(); - - // TODO: update plots - PlotData* plot; - plot = mResponsePlot->GetPlotData(1); - plot->xdata = {0, 2, 2, 3, 3, 5}; - plot->ydata = {0, 0.5, 1, 1, 0.5, 0}; - mResponsePlot->Refresh(false); + UpdateResponsePlot(); } void EffectCompressor2::UpdateCompressorPlot() @@ -545,3 +643,43 @@ void EffectCompressor2::UpdateCompressorPlot() // "Compressor gain reduction: %.1f dB", plot->ydata[xsize-1])); mGainPlot->Refresh(false); } + +void EffectCompressor2::UpdateResponsePlot() +{ + PlotData* plot; + plot = mResponsePlot->GetPlotData(1); + wxASSERT(plot->xdata.size() == plot->ydata.size()); + + std::unique_ptr preproc; + float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME; + + size_t window_size = + std::max(1, int(round((mLookaheadTime + mLookbehindTime) * plot_rate))); + size_t lookahead_size = + std::max(0, int(round(mLookaheadTime * plot_rate))); + + if(mCompressBy == kRMS) + preproc = std::unique_ptr( + safenew SlidingRmsPreprocessor(window_size, 1.0)); + else + preproc = std::unique_ptr( + safenew SlidingMaxPreprocessor(window_size)); + + ssize_t step_start = RESPONSE_PLOT_STEP_START * plot_rate - lookahead_size; + ssize_t step_stop = RESPONSE_PLOT_STEP_STOP * plot_rate - lookahead_size; + + float value; + ssize_t xsize = plot->xdata.size(); + for(ssize_t i = -lookahead_size; i < xsize; ++i) + { + if(i < step_start || i > step_stop) + value = preproc->ProcessSample(0); + else + value = preproc->ProcessSample(1); + + if(i >= 0) + plot->ydata[i] = value; + } + + mResponsePlot->Refresh(false); +} diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index fc82aac4d..2661b8eda 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -23,6 +23,50 @@ class Plot; class ShuttleGui; +class SamplePreprocessor +{ + public: + virtual float ProcessSample(float value) = 0; + virtual float ProcessSample(float valueL, float valueR) = 0; +}; + +class SlidingRmsPreprocessor : public SamplePreprocessor +{ + public: + SlidingRmsPreprocessor(size_t windowSize, float gain = 2.0); + + virtual float ProcessSample(float value); + virtual float ProcessSample(float valueL, float valueR); + + static const size_t REFRESH_WINDOW_EVERY = 1048576; // 1 MB + + private: + float mSum; + float mGain; + std::vector mWindow; + size_t mPos; + size_t mInsertCount; + + inline float DoProcessSample(float value); + void Refresh(); +}; + +class SlidingMaxPreprocessor : public SamplePreprocessor +{ + public: + SlidingMaxPreprocessor(size_t windowSize); + + virtual float ProcessSample(float value); + virtual float ProcessSample(float valueL, float valueR); + + private: + std::vector mWindow; + std::vector mMaxes; + size_t mPos; + + inline float DoProcessSample(float value); +}; + class EffectCompressor2 final : public Effect { public: @@ -65,6 +109,7 @@ private: void OnUpdateUI(wxCommandEvent & evt); void UpdateUI(); void UpdateCompressorPlot(); + void UpdateResponsePlot(); int mAlgorithm; int mCompressBy; @@ -84,6 +129,11 @@ private: double mMakeupGain; double mMakeupGainDB; + static const size_t RESPONSE_PLOT_SAMPLES = 200; + static const size_t RESPONSE_PLOT_TIME = 5; + static const size_t RESPONSE_PLOT_STEP_START = 2; + static const size_t RESPONSE_PLOT_STEP_STOP = 3; + Plot* mGainPlot; Plot* mResponsePlot; bool mIgnoreGuiEvents;