From e5a6585a128d27bc6f1de88e37f21a1b6a6742a6 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Tue, 21 Aug 2018 17:32:35 +0200
Subject: [PATCH 01/33] Start work on new Compressor2 effect.

Add skeleton files and add them to build systems.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/CMakeLists.txt          |   2 +
 src/effects/Compressor2.cpp | 151 ++++++++++++++++++++++++++++++++++++
 src/effects/Compressor2.h   |  70 +++++++++++++++++
 3 files changed, 223 insertions(+)
 create mode 100644 src/effects/Compressor2.cpp
 create mode 100644 src/effects/Compressor2.h

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 83e57578d..cfa06880b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -432,6 +432,8 @@ list( APPEND SOURCES PRIVATE
   effects/ClickRemoval.h
   effects/Compressor.cpp
   effects/Compressor.h
+  effects/Compressor2.cpp
+  effects/Compressor2.h
   effects/Contrast.cpp
   effects/Contrast.h
   effects/Distortion.cpp
diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
new file mode 100644
index 000000000..42558b5b3
--- /dev/null
+++ b/src/effects/Compressor2.cpp
@@ -0,0 +1,151 @@
+/**********************************************************************
+
+  Audacity: A Digital Audio Editor
+
+  Compressor2.cpp
+
+  Max Maisel
+
+*******************************************************************//**
+
+\class EffectCompressor2
+\brief An Effect which reduces the dynamic level.
+
+*//*******************************************************************/
+
+
+#include "../Audacity.h" // for rint from configwin.h
+#include "Compressor2.h"
+
+#include <math.h>
+
+#include <wx/intl.h>
+#include <wx/valgen.h>
+
+#include "../Internat.h"
+#include "../Prefs.h"
+#include "../ProjectFileManager.h"
+#include "../Shuttle.h"
+#include "../ShuttleGui.h"
+#include "../WaveTrack.h"
+#include "../widgets/valnum.h"
+#include "../widgets/ProgressDialog.h"
+
+#include "LoadEffects.h"
+
+BEGIN_EVENT_TABLE(EffectCompressor2, wxEvtHandler)
+END_EVENT_TABLE()
+
+const ComponentInterfaceSymbol EffectCompressor2::Symbol
+{ XO("Compressor v2") };
+
+namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; }
+
+EffectCompressor2::EffectCompressor2()
+{
+   SetLinearEffectFlag(false);
+}
+
+EffectCompressor2::~EffectCompressor2()
+{
+}
+
+// ComponentInterface implementation
+
+ComponentInterfaceSymbol EffectCompressor2::GetSymbol()
+{
+   return Symbol;
+}
+
+TranslatableString EffectCompressor2::GetDescription()
+{
+   return XO("Reduces the dynamic of one or more tracks");
+}
+
+ManualPageID EffectCompressor2::ManualPage()
+{
+   return L"Compressor2";
+}
+
+// EffectDefinitionInterface implementation
+
+EffectType EffectCompressor2::GetType()
+{
+   return EffectTypeProcess;
+}
+
+// EffectClientInterface implementation
+bool EffectCompressor2::DefineParams( ShuttleParams & S )
+{
+   return true;
+}
+
+bool EffectCompressor2::GetAutomationParameters(CommandParameters & parms)
+{
+   return true;
+}
+
+bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms)
+{
+   return true;
+}
+
+// Effect implementation
+
+bool EffectCompressor2::CheckWhetherSkipEffect()
+{
+   return false;
+}
+
+bool EffectCompressor2::Startup()
+{
+   wxString base = wxT("/Effects/Compressor2/");
+   // Load the old "current" settings
+   if (gPrefs->Exists(base))
+   {
+      SaveUserPreset(GetCurrentSettingsGroup());
+
+      gPrefs->Flush();
+   }
+   return true;
+}
+
+bool EffectCompressor2::Process()
+{
+   return false;
+}
+
+void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
+{
+}
+
+bool EffectCompressor2::TransferDataToWindow()
+{
+   if (!mUIParent->TransferDataToWindow())
+   {
+      return false;
+   }
+
+   UpdateUI();
+   return true;
+}
+
+bool EffectCompressor2::TransferDataFromWindow()
+{
+   if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
+   {
+      return false;
+   }
+   return true;
+}
+
+// EffectCompressor2 implementation
+
+void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt))
+{
+   UpdateUI();
+}
+
+void EffectCompressor2::UpdateUI()
+{
+}
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
new file mode 100644
index 000000000..fb97b61a0
--- /dev/null
+++ b/src/effects/Compressor2.h
@@ -0,0 +1,70 @@
+/**********************************************************************
+
+  Audacity: A Digital Audio Editor
+
+  Compressor2.h
+
+  Max Maisel (based on Compressor effect)
+
+**********************************************************************/
+
+#ifndef __AUDACITY_EFFECT_COMPRESSOR2__
+#define __AUDACITY_EFFECT_COMPRESSOR2__
+
+#include <wx/checkbox.h>
+#include <wx/choice.h>
+#include <wx/event.h>
+#include <wx/stattext.h>
+#include <wx/string.h>
+#include <wx/textctrl.h>
+
+#include "Effect.h"
+
+class ShuttleGui;
+
+class EffectCompressor2 final : public Effect
+{
+public:
+   static const ComponentInterfaceSymbol Symbol;
+
+   EffectCompressor2();
+   virtual ~EffectCompressor2();
+
+   // ComponentInterface implementation
+
+   ComponentInterfaceSymbol GetSymbol() override;
+   TranslatableString GetDescription() override;
+   ManualPageID ManualPage() override;
+
+   // EffectDefinitionInterface implementation
+
+   EffectType GetType() override;
+
+   // EffectClientInterface implementation
+
+   bool DefineParams( ShuttleParams & S ) override;
+   bool GetAutomationParameters(CommandParameters & parms) override;
+   bool SetAutomationParameters(CommandParameters & parms) override;
+
+   // Effect implementation
+
+   bool CheckWhetherSkipEffect() override;
+   bool Startup() override;
+   bool Process() override;
+   void PopulateOrExchange(ShuttleGui & S) override;
+   bool TransferDataToWindow() override;
+   bool TransferDataFromWindow() override;
+
+private:
+   // EffectCompressor2 implementation
+
+   bool UpdateProgress();
+   void OnUpdateUI(wxCommandEvent & evt);
+   void UpdateUI();
+
+private:
+
+   DECLARE_EVENT_TABLE()
+};
+
+#endif

From 6395c8470b6de246c86367b64d454a5d47bc39c7 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sun, 9 Feb 2020 10:34:41 +0100
Subject: [PATCH 02/33] Add all user parameters to Compressor2 effect.

Don't do anything with them yet.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 126 ++++++++++++++++++++++++++++++++++++
 src/effects/Compressor2.h   |  13 ++++
 2 files changed, 139 insertions(+)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 42558b5b3..cdec61325 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -33,6 +33,49 @@
 
 #include "LoadEffects.h"
 
+enum kAlgorithms
+{
+   kExpFit,
+   kEnvPT1,
+   nAlgos
+};
+
+static const ComponentInterfaceSymbol kAlgorithmStrings[nAlgos] =
+{
+   { XO("Exponential-Fit") },
+   { XO("Analog Model (PT1)") }
+};
+
+enum kCompressBy
+{
+   kAmplitude,
+   kRMS,
+   nBy
+};
+
+static const ComponentInterfaceSymbol kCompressByStrings[nBy] =
+{
+   { XO("peak amplitude") },
+   { XO("RMS") }
+};
+
+// Define keys, defaults, minimums, and maximums for the effect parameters
+//
+//     Name         Type     Key                        Def         Min      Max       Scale
+Param( Algorithm,      int,     wxT("Algorithm"),         kEnvPT1,    0,    nAlgos-1,  1   );
+Param( CompressBy,     int,     wxT("CompressBy"),   kAmplitude,      0,   nBy-1,      1   );
+Param( StereoInd,      bool,    wxT("StereoIndependent"), false,   false,   true,      1   );
+
+Param( Threshold,      double,  wxT("Threshold"),        -12.0,   -60.0,     -1.0,     1.0 );
+Param( Ratio,          double,  wxT("Ratio"),              2.0,     1.1,    100.0,    10.0 );
+Param( KneeWidth,      double,  wxT("KneeWidth"),         10.0,     0.0,     20.0,    10.0 );
+Param( AttackTime,     double,  wxT("AttackTime"),         0.2,     0.00001, 30.0, 20000.0 );
+Param( ReleaseTime,    double,  wxT("ReleaseTime"),        1.0,     0.00001, 30.0, 20000.0 );
+Param( LookaheadTime,  double,  wxT("LookaheadTime"),      0.0,     0.0,     10.0,   200.0 );
+Param( LookbehindTime, double,  wxT("LookbehindTime"),     0.1,     0.0,     10.0,   200.0 );
+Param( MakeupGain,     double,  wxT("MakeupGain"),         0.0,     0.0,    100.0,     1.0 );
+Param( DryWet,         double,  wxT("DryWet"),           100.0,     0.0,    100.0,     1.0 );
+
 BEGIN_EVENT_TABLE(EffectCompressor2, wxEvtHandler)
 END_EVENT_TABLE()
 
@@ -43,6 +86,20 @@ namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; }
 
 EffectCompressor2::EffectCompressor2()
 {
+   mAlgorithm = DEF_Algorithm;
+   mCompressBy = DEF_CompressBy;
+   mStereoInd = DEF_StereoInd;
+
+   mThresholdDB = DEF_Threshold;
+   mRatio = DEF_Ratio;                    // positive number > 1.0
+   mKneeWidthDB = DEF_KneeWidth;
+   mAttackTime = DEF_AttackTime;          // seconds
+   mReleaseTime = DEF_ReleaseTime;          // seconds
+   mLookaheadTime = DEF_LookaheadTime;
+   mLookbehindTime = DEF_LookbehindTime;
+   mMakeupGainPct = DEF_MakeupGain;
+   mDryWetPct = DEF_DryWet;
+
    SetLinearEffectFlag(false);
 }
 
@@ -77,16 +134,71 @@ EffectType EffectCompressor2::GetType()
 // EffectClientInterface implementation
 bool EffectCompressor2::DefineParams( ShuttleParams & S )
 {
+   S.SHUTTLE_PARAM(mAlgorithm, Algorithm);
+   S.SHUTTLE_PARAM(mCompressBy, CompressBy);
+   S.SHUTTLE_PARAM(mStereoInd, StereoInd);
+
+   S.SHUTTLE_PARAM(mThresholdDB, Threshold);
+   S.SHUTTLE_PARAM(mRatio, Ratio);
+   S.SHUTTLE_PARAM(mKneeWidthDB, KneeWidth);
+   S.SHUTTLE_PARAM(mAttackTime, AttackTime);
+   S.SHUTTLE_PARAM(mReleaseTime, ReleaseTime);
+   S.SHUTTLE_PARAM(mLookaheadTime, LookaheadTime);
+   S.SHUTTLE_PARAM(mLookbehindTime, LookbehindTime);
+   S.SHUTTLE_PARAM(mMakeupGainPct, MakeupGain);
+   S.SHUTTLE_PARAM(mDryWetPct, DryWet);
+
    return true;
 }
 
 bool EffectCompressor2::GetAutomationParameters(CommandParameters & parms)
 {
+   parms.Write(KEY_Algorithm, mAlgorithm);
+   parms.Write(KEY_CompressBy, mCompressBy);
+   parms.Write(KEY_StereoInd, mStereoInd);
+
+   parms.Write(KEY_Threshold, mThresholdDB);
+   parms.Write(KEY_Ratio, mRatio);
+   parms.Write(KEY_KneeWidth, mKneeWidthDB);
+   parms.Write(KEY_AttackTime, mAttackTime);
+   parms.Write(KEY_ReleaseTime, mReleaseTime);
+   parms.Write(KEY_LookaheadTime, mLookaheadTime);
+   parms.Write(KEY_LookbehindTime, mLookbehindTime);
+   parms.Write(KEY_MakeupGain, mMakeupGainPct);
+   parms.Write(KEY_DryWet, mDryWetPct);
+
    return true;
 }
 
 bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms)
 {
+   ReadAndVerifyInt(Algorithm);
+   ReadAndVerifyInt(CompressBy);
+   ReadAndVerifyBool(StereoInd);
+
+   ReadAndVerifyDouble(Threshold);
+   ReadAndVerifyDouble(Ratio);
+   ReadAndVerifyDouble(KneeWidth);
+   ReadAndVerifyDouble(AttackTime);
+   ReadAndVerifyDouble(ReleaseTime);
+   ReadAndVerifyDouble(LookaheadTime);
+   ReadAndVerifyDouble(LookbehindTime);
+   ReadAndVerifyDouble(MakeupGain);
+   ReadAndVerifyDouble(DryWet);
+
+   mAlgorithm = Algorithm;
+   mCompressBy = CompressBy;
+   mStereoInd = StereoInd;
+
+   mThresholdDB = Threshold;
+   mRatio = Ratio;
+   mKneeWidthDB = KneeWidth;
+   mAttackTime = AttackTime;
+   mReleaseTime = ReleaseTime;
+   mLookaheadTime = LookaheadTime;
+   mLookbehindTime = LookbehindTime;
+   mMakeupGainPct = MakeupGain;
+   mDryWetPct = DryWet;
    return true;
 }
 
@@ -103,6 +215,20 @@ bool EffectCompressor2::Startup()
    // Load the old "current" settings
    if (gPrefs->Exists(base))
    {
+      mAlgorithm = DEF_Algorithm;
+      mCompressBy = DEF_CompressBy;
+      mStereoInd = DEF_StereoInd;
+
+      mThresholdDB = DEF_Threshold;
+      mRatio = DEF_Ratio;                    // positive number > 1.0
+      mKneeWidthDB = DEF_KneeWidth;
+      mAttackTime = DEF_AttackTime;          // seconds
+      mReleaseTime = DEF_ReleaseTime;          // seconds
+      mLookaheadTime = DEF_LookaheadTime;
+      mLookbehindTime = DEF_LookbehindTime;
+      mMakeupGainPct = DEF_MakeupGain;
+      mDryWetPct = DEF_DryWet;
+
       SaveUserPreset(GetCurrentSettingsGroup());
 
       gPrefs->Flush();
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index fb97b61a0..8d98daef9 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -63,6 +63,19 @@ private:
    void UpdateUI();
 
 private:
+   int    mAlgorithm;
+   int    mCompressBy;
+   bool   mStereoInd;
+
+   double    mThresholdDB;
+   double    mRatio;
+   double    mKneeWidthDB;
+   double    mAttackTime;
+   double    mReleaseTime;
+   double    mLookaheadTime;
+   double    mLookbehindTime;
+   double    mMakeupGainPct;
+   double    mDryWetPct;
 
    DECLARE_EVENT_TABLE()
 };

From 2f58dc7d94ccfb790675201a794efab6a1403499 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sun, 9 Feb 2020 14:55:23 +0100
Subject: [PATCH 03/33] Add combined slider and text box widget.

This widgets will be used in the new Compresor2 effect but it is
designed for use in other effects as well.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/CMakeLists.txt             |   2 +
 src/ShuttleGui.cpp             |  27 ++++++
 src/ShuttleGui.h               |   5 +
 src/widgets/SliderTextCtrl.cpp | 167 +++++++++++++++++++++++++++++++++
 src/widgets/SliderTextCtrl.h   |  77 +++++++++++++++
 5 files changed, 278 insertions(+)
 create mode 100644 src/widgets/SliderTextCtrl.cpp
 create mode 100644 src/widgets/SliderTextCtrl.h

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index cfa06880b..8ac44c420 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -956,6 +956,8 @@ list( APPEND SOURCES PRIVATE
   widgets/ReadOnlyText.h
   widgets/Ruler.cpp
   widgets/Ruler.h
+  widgets/SliderTextCtrl.cpp
+  widgets/SliderTextCtrl.h
   widgets/UnwritableLocationErrorDialog.cpp
   widgets/UnwritableLocationErrorDialog.h
   widgets/Warning.cpp
diff --git a/src/ShuttleGui.cpp b/src/ShuttleGui.cpp
index 82ab555c5..3328d80b2 100644
--- a/src/ShuttleGui.cpp
+++ b/src/ShuttleGui.cpp
@@ -121,6 +121,8 @@ for registering for changes.
 #include "widgets/wxTextCtrlWrapper.h"
 #include "AllThemeResources.h"
 
+#include "widgets/SliderTextCtrl.h"
+
 #if wxUSE_ACCESSIBILITY
 #include "widgets/WindowAccessible.h"
 #endif
@@ -613,6 +615,31 @@ wxSlider * ShuttleGuiBase::AddSlider(
    return pSlider;
 }
 
+SliderTextCtrl* ShuttleGuiBase::AddSliderTextCtrl(
+   const TranslatableString &Prompt, double pos, double Max, double Min,
+   int precision, double* value, double scale)
+{
+   HandleOptionality( Prompt );
+   AddPrompt( Prompt );
+   UseUpId();
+   if( mShuttleMode != eIsCreating )
+      return wxDynamicCast(wxWindow::FindWindowById( miId, mpDlg), SliderTextCtrl);
+   SliderTextCtrl * pSlider;
+   mpWind = pSlider = safenew SliderTextCtrl(GetParent(), miId,
+      pos, Min, Max, precision, scale, wxDefaultPosition, wxDefaultSize,
+      GetStyle( SliderTextCtrl::HORIZONTAL ),
+      value
+   );
+#if wxUSE_ACCESSIBILITY
+   // so that name can be set on a standard control
+   mpWind->SetAccessible(safenew WindowAccessible(mpWind));
+#endif
+   mpWind->SetName(wxStripMenuCodes(Prompt.Translation()));
+   miProp=1;
+   UpdateSizers();
+   return pSlider;
+}
+
 wxSpinCtrl * ShuttleGuiBase::AddSpinCtrl(
    const TranslatableString &Prompt, int Value, int Max, int Min)
 {
diff --git a/src/ShuttleGui.h b/src/ShuttleGui.h
index d35d928cf..da01fa8d6 100644
--- a/src/ShuttleGui.h
+++ b/src/ShuttleGui.h
@@ -28,6 +28,7 @@
 class ChoiceSetting;
 
 class wxArrayStringEx;
+class SliderTextCtrl;
 
 
 const int nMaxNestedSizers = 20;
@@ -263,6 +264,10 @@ public:
       int Value, int Max, int Min);
    wxTreeCtrl * AddTree();
 
+   SliderTextCtrl* AddSliderTextCtrl(
+      const TranslatableString &Prompt, double pos, double Max, double Min = 0,
+      int precision = 2, double* value = NULL, double scale = 0);
+
    // Pass the same initValue to the sequence of calls to AddRadioButton and
    // AddRadioButtonToGroup.
    // The radio button is filled if selector == initValue
diff --git a/src/widgets/SliderTextCtrl.cpp b/src/widgets/SliderTextCtrl.cpp
new file mode 100644
index 000000000..524d1a9f6
--- /dev/null
+++ b/src/widgets/SliderTextCtrl.cpp
@@ -0,0 +1,167 @@
+/**********************************************************************
+
+   Audacity: A Digital Audio Editor
+
+   SliderTextCtrl.cpp
+
+   Max Maisel
+
+*******************************************************************//**
+
+\class SliderTextCtrl
+\brief A slider with connected text box.
+
+*//*******************************************************************/
+
+
+#include "../Audacity.h"
+#include "audacity/Types.h"
+#include "SliderTextCtrl.h"
+
+#include <wx/defs.h>
+#include <wx/panel.h>
+#include <wx/sizer.h>
+#include <wx/slider.h>
+#include <wx/textctrl.h>
+#include <wx/valnum.h>
+
+wxDEFINE_EVENT(cEVT_SLIDERTEXT, wxCommandEvent);
+
+SliderTextCtrl::SliderTextCtrl(wxWindow *parent, wxWindowID winid,
+   double value, double min, double max, int precision, double scale,
+   const wxPoint& pos, const wxSize& size, long style, double* varValue)
+   : wxPanelWrapper(parent, winid, pos, size, wxWS_EX_VALIDATE_RECURSIVELY)
+{
+   m_log = style & LOG;
+   m_int = style & INT;
+   m_value = value;
+   m_min = min;
+   m_max = max;
+   m_zero = -std::numeric_limits<double>::infinity();
+
+   if(m_int)
+   {
+      precision = 0;
+      m_format = "%d";
+   }
+   else
+      m_format = wxString::Format("%%.%df", precision);
+
+   if(scale == 0)
+      m_scale = pow(10, precision);
+   else
+      m_scale = scale;
+
+   wxFloatingPointValidator<double> validator(precision, varValue);
+
+   if(m_log)
+   {
+      if(min <= 0.0)
+      {
+         m_zero = -double(precision) - 1.0 / m_scale;
+         min = m_zero;
+      }
+      else
+         min = log10(min);
+
+      if(value <= 0.0)
+         value = m_zero;
+      else
+         value = log10(value);
+      max = log10(max);
+   }
+
+   m_sizer = safenew wxBoxSizer(
+      style & HORIZONTAL ? wxHORIZONTAL : wxVERTICAL);
+   m_slider = safenew wxSlider(this, ID_SLIDER,
+      round(value * m_scale), floor(min * m_scale), ceil(max * m_scale),
+      wxDefaultPosition, wxDefaultSize,
+      style & HORIZONTAL ? wxSL_HORIZONTAL : wxSL_VERTICAL);
+   m_textbox = safenew wxTextCtrl(this, ID_TEXTBOX, wxEmptyString,
+      wxDefaultPosition, wxDefaultSize, 0, validator);
+
+   m_textbox->ChangeValue(FormatValue());
+   m_textbox->Bind(wxEVT_KILL_FOCUS, &SliderTextCtrl::OnKillFocus, this);
+
+   m_sizer->Add(m_slider, 1, wxEXPAND);
+   m_sizer->Add(m_textbox, 0, wxEXPAND);
+
+   SetSizer(m_sizer);
+}
+
+void SliderTextCtrl::SetMinTextboxWidth(int width)
+{
+    wxSize size = GetMinSize();
+    size.SetWidth(width);
+    m_textbox->SetMinSize(size);
+}
+
+double SliderTextCtrl::GetValue() const
+{
+   return m_value;
+}
+
+void SliderTextCtrl::SetValue(double value)
+{
+   m_value = value;
+   m_textbox->ChangeValue(FormatValue());
+}
+
+void SliderTextCtrl::OnTextChange(wxCommandEvent& event)
+{
+   double value;
+   m_textbox->GetValue().ToDouble(&value);
+   m_value = std::min(value, m_max);
+   m_value = std::max(m_value, m_min);
+   if(m_log)
+   {
+      if(m_value == 0.0)
+         value = m_zero;
+      else
+         value = log10(m_value);
+   }
+   m_slider->SetValue(round(value * m_scale));
+   event.SetEventType(cEVT_SLIDERTEXT);
+   event.Skip();
+}
+
+void SliderTextCtrl::OnSlider(wxCommandEvent& event)
+{
+   m_value = m_slider->GetValue() / m_scale;
+   if(m_log)
+   {
+      if(m_value <= m_zero)
+         m_value = 0.0;
+      else
+      {
+         m_value = pow(10.0, m_value);
+         m_value = std::max(m_min, m_value);
+         m_value = std::min(m_max, m_value);
+      }
+   }
+   m_textbox->ChangeValue(FormatValue());
+   m_textbox->SetSelection(-1, -1);
+   event.SetEventType(cEVT_SLIDERTEXT);
+   event.Skip();
+}
+
+void SliderTextCtrl::OnKillFocus(wxFocusEvent& _)
+{
+   m_textbox->ChangeValue(FormatValue());
+   wxCommandEvent event(cEVT_SLIDERTEXT, GetId());
+   wxPostEvent(GetParent(), event);
+}
+
+wxString SliderTextCtrl::FormatValue() const
+{
+   int v = m_value;
+   if(m_int)
+       return wxString::Format(m_format, v);
+   else
+       return wxString::Format(m_format, m_value);
+}
+
+BEGIN_EVENT_TABLE(SliderTextCtrl, wxControl)
+   EVT_TEXT(ID_TEXTBOX, SliderTextCtrl::OnTextChange)
+   EVT_SLIDER(ID_SLIDER, SliderTextCtrl::OnSlider)
+END_EVENT_TABLE()
diff --git a/src/widgets/SliderTextCtrl.h b/src/widgets/SliderTextCtrl.h
new file mode 100644
index 000000000..f8d27efa0
--- /dev/null
+++ b/src/widgets/SliderTextCtrl.h
@@ -0,0 +1,77 @@
+/**********************************************************************
+
+   Audacity: A Digital Audio Editor
+
+   SliderTextCtrl.h
+
+   Max Maisel
+
+   This class is a custom slider.
+
+**********************************************************************/
+
+#ifndef __AUDACITY_SLIDERTEXTCTRL__
+#define __AUDACITY_SLIDERTEXTCTRL__
+
+#include "wxPanelWrapper.h" // to inherit
+
+class wxSizer;
+class wxSlider;
+class wxTextCtrl;
+
+wxDECLARE_EVENT(cEVT_SLIDERTEXT, wxCommandEvent);
+
+#define EVT_SLIDERTEXT(winid, func) wx__DECLARE_EVT1( \
+   cEVT_SLIDERTEXT, winid, wxCommandEventHandler(func))
+
+class SliderTextCtrl : public wxPanelWrapper
+{
+   public:
+      enum Styles
+      {
+         HORIZONTAL = 1,
+         VERTICAL = 2,
+         LOG = 4,
+         INT = 8,
+      };
+
+      SliderTextCtrl(wxWindow *parent, wxWindowID winid,
+         double value, double min, double max, int precision = 2,
+         double scale = 0, const wxPoint& pos = wxDefaultPosition,
+         const wxSize& size = wxDefaultSize, long style = HORIZONTAL,
+         double* varValue = NULL);
+
+      void SetMinTextboxWidth(int width);
+
+      double GetValue() const;
+      void SetValue(double value);
+
+   private:
+      void OnTextChange(wxCommandEvent& event);
+      void OnSlider(wxCommandEvent& event);
+      void OnKillFocus(wxFocusEvent& event);
+      wxString FormatValue() const;
+
+      enum
+      {
+         ID_SLIDER = 1,
+         ID_TEXTBOX
+      };
+
+      wxSizer* m_sizer;
+      wxSlider* m_slider;
+      wxTextCtrl* m_textbox;
+
+      bool m_log;
+      bool m_int;
+      double m_value;
+      double m_scale;
+      double m_min;
+      double m_max;
+      double m_zero;
+      wxString m_format;
+
+      DECLARE_EVENT_TABLE()
+};
+
+#endif

From 2555b68a0444ac137881f7cfa9c125f848ed929d Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sun, 9 Feb 2020 14:55:23 +0100
Subject: [PATCH 04/33] Add Compressor2 GUI without graph.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 163 ++++++++++++++++++++++++++++++++++++
 src/effects/Compressor2.h   |   2 +
 2 files changed, 165 insertions(+)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index cdec61325..3130ca6f2 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -30,6 +30,7 @@
 #include "../WaveTrack.h"
 #include "../widgets/valnum.h"
 #include "../widgets/ProgressDialog.h"
+#include "../widgets/SliderTextCtrl.h"
 
 #include "LoadEffects.h"
 
@@ -76,7 +77,15 @@ Param( LookbehindTime, double,  wxT("LookbehindTime"),     0.1,     0.0,     10.
 Param( MakeupGain,     double,  wxT("MakeupGain"),         0.0,     0.0,    100.0,     1.0 );
 Param( DryWet,         double,  wxT("DryWet"),           100.0,     0.0,    100.0,     1.0 );
 
+inline int ScaleToPrecision(double scale)
+{
+   return ceil(log10(scale));
+}
+
 BEGIN_EVENT_TABLE(EffectCompressor2, wxEvtHandler)
+   EVT_CHECKBOX(wxID_ANY, EffectCompressor2::OnUpdateUI)
+   EVT_CHOICE(wxID_ANY, EffectCompressor2::OnUpdateUI)
+   EVT_SLIDERTEXT(wxID_ANY, EffectCompressor2::OnUpdateUI)
 END_EVENT_TABLE()
 
 const ComponentInterfaceSymbol EffectCompressor2::Symbol
@@ -85,6 +94,7 @@ const ComponentInterfaceSymbol EffectCompressor2::Symbol
 namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; }
 
 EffectCompressor2::EffectCompressor2()
+   : mIgnoreGuiEvents(false)
 {
    mAlgorithm = DEF_Algorithm;
    mCompressBy = DEF_CompressBy;
@@ -199,6 +209,7 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms)
    mLookbehindTime = LookbehindTime;
    mMakeupGainPct = MakeupGain;
    mDryWetPct = DryWet;
+
    return true;
 }
 
@@ -243,16 +254,166 @@ bool EffectCompressor2::Process()
 
 void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
 {
+   S.StartStatic(XO("Algorithm"));
+   {
+      S.StartMultiColumn(2, wxALIGN_LEFT);
+      {
+         S.SetStretchyCol(1);
+
+         wxChoice* ctrl = nullptr;
+
+         ctrl = S.Validator<wxGenericValidator>(&mAlgorithm)
+            .AddChoice(XO("Envelope Algorithm:"),
+               Msgids(kAlgorithmStrings, nAlgos),
+               mAlgorithm);
+
+         wxSize box_size = ctrl->GetMinSize();
+         int width = S.GetParent()->GetTextExtent(wxString::Format(
+            "%sxxxx",  kAlgorithmStrings[nAlgos-1].Translation())).GetWidth();
+         box_size.SetWidth(width);
+         ctrl->SetMinSize(box_size);
+
+         ctrl = S.Validator<wxGenericValidator>(&mCompressBy)
+            .AddChoice(XO("Compress based on:"),
+               Msgids(kCompressByStrings, nBy),
+               mCompressBy);
+         ctrl->SetMinSize(box_size);
+
+         S.Validator<wxGenericValidator>(&mStereoInd)
+            .AddCheckBox(XO("Compress stereo channels independently"),
+               DEF_StereoInd);
+      }
+      S.EndMultiColumn();
+   }
+   S.EndStatic();
+
+   S.StartStatic(XO("Compressor"));
+   {
+      S.StartMultiColumn(3, wxEXPAND);
+      {
+         S.SetStretchyCol(1);
+         int textbox_width = S.GetParent()->GetTextExtent("0.000001").GetWidth();
+         SliderTextCtrl* ctrl = nullptr;
+
+         S.AddVariableText(XO("Threshold:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Threshold"))
+            .Style(SliderTextCtrl::HORIZONTAL)
+            .AddSliderTextCtrl({}, DEF_Threshold, MAX_Threshold,
+               MIN_Threshold, ScaleToPrecision(SCL_Threshold), &mThresholdDB);
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO("dB"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+         S.AddVariableText(XO("Ratio:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Ratio"))
+            .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
+            .AddSliderTextCtrl({}, DEF_Ratio, MAX_Ratio, MIN_Ratio,
+               ScaleToPrecision(SCL_Ratio), &mRatio);
+         /* i18n-hint: Unless your language has a different convention for ratios,
+          * like 8:1, leave as is.*/
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO(":1"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+         S.AddVariableText(XO("Knee Width:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Knee Width"))
+            .Style(SliderTextCtrl::HORIZONTAL)
+            .AddSliderTextCtrl({}, DEF_KneeWidth, MAX_KneeWidth,
+               MIN_KneeWidth, ScaleToPrecision(SCL_KneeWidth),
+               &mKneeWidthDB);
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO("dB"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+         S.AddVariableText(XO("Attack Time:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Attack Time"))
+            .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
+            .AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime,
+               MIN_AttackTime, ScaleToPrecision(SCL_AttackTime),
+               &mAttackTime, SCL_AttackTime / 1000);
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO("s"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+         S.AddVariableText(XO("Release Time:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Release Time"))
+            .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
+            .AddSliderTextCtrl({}, DEF_ReleaseTime, MAX_ReleaseTime,
+               MIN_ReleaseTime, ScaleToPrecision(SCL_ReleaseTime),
+               &mReleaseTime, SCL_ReleaseTime / 1000);
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO("s"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+         S.AddVariableText(XO("Lookahead Time:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Lookahead Time"))
+            .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
+            .AddSliderTextCtrl({}, DEF_LookaheadTime, MAX_LookaheadTime,
+               MIN_LookaheadTime, ScaleToPrecision(SCL_LookaheadTime),
+               &mLookaheadTime);
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO("s"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+         S.AddVariableText(XO("Hold Time:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Hold Time"))
+            .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
+            .AddSliderTextCtrl({}, DEF_LookbehindTime, MAX_LookbehindTime,
+               MIN_LookbehindTime, ScaleToPrecision(SCL_LookbehindTime),
+               &mLookbehindTime);
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO("s"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+         /* i18n-hint: Make-up, i.e. correct for any reduction, rather than fabricate it.*/
+         S.AddVariableText(XO("Make-up Gain:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Make-up Gain"))
+            .Style(SliderTextCtrl::HORIZONTAL)
+            .AddSliderTextCtrl({}, DEF_MakeupGain, MAX_MakeupGain,
+               MIN_MakeupGain, ScaleToPrecision(SCL_MakeupGain),
+               &mMakeupGainPct);
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO("%"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+         S.AddVariableText(XO("Dry/Wet:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Dry/Wet"))
+            .Style(SliderTextCtrl::HORIZONTAL)
+            .AddSliderTextCtrl({}, DEF_DryWet, MAX_DryWet,
+               MIN_DryWet, ScaleToPrecision(SCL_DryWet),
+               &mDryWetPct);
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO("%"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+      }
+      S.EndMultiColumn();
+   }
+   S.EndVerticalLay();
 }
 
 bool EffectCompressor2::TransferDataToWindow()
 {
+   // Transferring data to window causes spurious UpdateUI events
+   // which would reset the UI values to the previous value.
+   // This guard lets the program ignore them.
+   mIgnoreGuiEvents = true;
    if (!mUIParent->TransferDataToWindow())
    {
+      mIgnoreGuiEvents = false;
       return false;
    }
 
    UpdateUI();
+   mIgnoreGuiEvents = false;
    return true;
 }
 
@@ -269,6 +430,8 @@ bool EffectCompressor2::TransferDataFromWindow()
 
 void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt))
 {
+   if(!mIgnoreGuiEvents)
+      TransferDataFromWindow();
    UpdateUI();
 }
 
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index 8d98daef9..1c9f11f89 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -77,6 +77,8 @@ private:
    double    mMakeupGainPct;
    double    mDryWetPct;
 
+   bool mIgnoreGuiEvents;
+
    DECLARE_EVENT_TABLE()
 };
 

From bcdc47bc3403e0f23ce6481e7c8298bc776fe772 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Fri, 14 Feb 2020 18:41:18 +0100
Subject: [PATCH 05/33] Add generic plot widget.

This widget will be used in the new Compressor2 effect but it is
designed for use in other effects as well.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/CMakeLists.txt   |   2 +
 src/ShuttleGui.cpp   |  28 +++++++++
 src/ShuttleGui.h     |   6 ++
 src/widgets/Plot.cpp | 136 +++++++++++++++++++++++++++++++++++++++++++
 src/widgets/Plot.h   |  58 ++++++++++++++++++
 5 files changed, 230 insertions(+)
 create mode 100644 src/widgets/Plot.cpp
 create mode 100644 src/widgets/Plot.h

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 8ac44c420..cbf28101f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -949,6 +949,8 @@ list( APPEND SOURCES PRIVATE
   widgets/Overlay.h
   widgets/OverlayPanel.cpp
   widgets/OverlayPanel.h
+  widgets/Plot.cpp
+  widgets/Plot.h
   widgets/PopupMenuTable.cpp
   widgets/PopupMenuTable.h
   widgets/ProgressDialog.cpp
diff --git a/src/ShuttleGui.cpp b/src/ShuttleGui.cpp
index 3328d80b2..ead5aa0f2 100644
--- a/src/ShuttleGui.cpp
+++ b/src/ShuttleGui.cpp
@@ -121,6 +121,7 @@ for registering for changes.
 #include "widgets/wxTextCtrlWrapper.h"
 #include "AllThemeResources.h"
 
+#include "widgets/Plot.h"
 #include "widgets/SliderTextCtrl.h"
 
 #if wxUSE_ACCESSIBILITY
@@ -777,6 +778,33 @@ void ShuttleGuiBase::AddConstTextBox(
    UpdateSizers();
 }
 
+Plot* ShuttleGuiBase::AddPlot( const TranslatableString &Prompt,
+   double x_min, double x_max, double y_min, double y_max,
+   const TranslatableString& x_label, const TranslatableString& y_label,
+   int x_format, int y_format, int count)
+{
+   HandleOptionality( Prompt );
+   AddPrompt( Prompt );
+   UseUpId();
+   if( mShuttleMode != eIsCreating )
+      return wxDynamicCast(wxWindow::FindWindowById(miId, mpDlg), Plot);
+   Plot* pPlot;
+   mpWind = pPlot = safenew Plot(GetParent(), miId,
+      x_min, x_max, y_min, y_max, x_label, y_label,
+      x_format, y_format, count,
+      wxDefaultPosition, wxDefaultSize,
+      GetStyle( SliderTextCtrl::HORIZONTAL )
+   );
+#if wxUSE_ACCESSIBILITY
+   // so that name can be set on a standard control
+   mpWind->SetAccessible(safenew WindowAccessible(mpWind));
+#endif
+   mpWind->SetName(wxStripMenuCodes(Prompt.Translation()));
+   miProp=1;
+   UpdateSizers();
+   return pPlot;
+}
+
 wxListBox * ShuttleGuiBase::AddListBox(const wxArrayStringEx &choices)
 {
    UseUpId();
diff --git a/src/ShuttleGui.h b/src/ShuttleGui.h
index da01fa8d6..04a97b28e 100644
--- a/src/ShuttleGui.h
+++ b/src/ShuttleGui.h
@@ -28,6 +28,7 @@
 class ChoiceSetting;
 
 class wxArrayStringEx;
+class Plot;
 class SliderTextCtrl;
 
 
@@ -348,6 +349,11 @@ public:
    void AddConstTextBox(
       const TranslatableString &Caption, const TranslatableString & Value );
 
+   Plot* AddPlot( const TranslatableString &Prompt,
+      double x_min, double x_max, double y_min, double y_max,
+      const TranslatableString& x_label, const TranslatableString& y_label,
+      int x_format = 1, int y_format = 1, int count = 1 );
+
 //-- Start and end functions.  These are used for sizer, or other window containers
 //   and create the appropriate widget.
    void StartHorizontalLay(int PositionFlags=wxALIGN_CENTRE, int iProp=1);
diff --git a/src/widgets/Plot.cpp b/src/widgets/Plot.cpp
new file mode 100644
index 000000000..fb1c1bea0
--- /dev/null
+++ b/src/widgets/Plot.cpp
@@ -0,0 +1,136 @@
+/**********************************************************************
+
+   Audacity: A Digital Audio Editor
+
+   Plot.cpp
+
+   Max Maisel
+
+*******************************************************************//**
+
+\class Plot
+\brief A customizable generic plot widget.
+
+*//*******************************************************************/
+
+
+#include "../Audacity.h"
+#include "audacity/Types.h"
+#include "Plot.h"
+#include "Ruler.h"
+#include "../AColor.h"
+#include "../Theme.h"
+#include "../AllThemeResources.h"
+
+#include <wx/brush.h>
+#include <wx/dcclient.h>
+#include <wx/dcmemory.h>
+
+Plot::Plot(wxWindow *parent, wxWindowID winid,
+   float x_min, float x_max, float y_min, float y_max,
+   const TranslatableString& xlabel, const TranslatableString& ylabel,
+   int xformat, int yformat, int count,
+   const wxPoint& pos, const wxSize& size, long style)
+   :
+   wxPanelWrapper(parent, winid, pos, size, style),
+   m_xmin(x_min), m_xmax(x_max), m_ymin(y_min), m_ymax(y_max),
+   m_plots(count)
+{
+   m_xruler = std::unique_ptr<Ruler>(safenew Ruler);
+   m_xruler->SetOrientation(wxHORIZONTAL);
+   m_xruler->SetFormat(static_cast<Ruler::RulerFormat>(xformat));
+   m_xruler->SetUnits(xlabel);
+   m_xruler->SetFlip(true);
+
+   m_yruler = std::unique_ptr<Ruler>(safenew Ruler);
+   m_yruler->SetOrientation(wxVERTICAL);
+   m_yruler->SetFormat(static_cast<Ruler::RulerFormat>(yformat));
+   m_yruler->SetUnits(ylabel);
+}
+
+void Plot::OnPaint(wxPaintEvent & evt)
+{
+   wxPaintDC dc(this);
+
+   int width, height;
+   GetSize(&width, &height);
+
+#if defined(__WXMSW__)
+   dc.Clear();
+#endif
+
+   // Ruler
+   int w = 0;
+   int h = 0;
+
+   m_xruler->SetBounds(0, 0, width, height);
+   m_xruler->SetRange(m_xmin, m_xmax);
+   m_xruler->GetMaxSize(NULL, &h);
+
+   m_yruler->SetBounds(0, 0, width, height);
+   m_yruler->SetRange(m_ymax, m_ymin);
+   m_yruler->GetMaxSize(&w, NULL);
+
+   m_xruler->SetBounds(w, height - h, width, height);
+   m_yruler->SetBounds(0, 0, w, height - h);
+
+   m_xruler->SetTickColour( theTheme.Colour( clrGraphLabels ));
+   m_yruler->SetTickColour( theTheme.Colour( clrGraphLabels ));
+
+   wxRect border;
+   border.x = w;
+   border.y = 0;
+   border.width = width - w;
+   border.height = height - h + 1;
+
+   dc.SetBrush(*wxWHITE_BRUSH);
+   dc.SetPen(*wxTRANSPARENT_PEN);
+   dc.DrawRectangle(border);
+
+   m_xruler->DrawGrid(dc, border.height, true, true, border.x, border.y);
+   m_yruler->DrawGrid(dc, border.width, true, true, border.x, border.y);
+
+   for(const auto& plot : m_plots)
+   {
+      wxASSERT(plot.xdata.size() == plot.ydata.size());
+      if(plot.xdata.size() == 0)
+         continue;
+      dc.SetPen(*plot.pen);
+
+      size_t xsize = plot.xdata.size();
+      for(size_t i = 1; i < xsize; ++i)
+      {
+         AColor::Line(dc,
+            XToScreen(plot.xdata[i-1], border),
+            YToScreen(plot.ydata[i-1], border),
+            XToScreen(plot.xdata[i], border),
+            YToScreen(plot.ydata[i], border));
+      }
+   }
+
+   dc.SetBrush(*wxTRANSPARENT_BRUSH);
+   dc.SetPen(*wxBLACK_PEN);
+   dc.DrawRectangle(border);
+   m_xruler->Draw(dc);
+   m_yruler->Draw(dc);
+}
+
+void Plot::OnSize(wxSizeEvent & evt)
+{
+   Refresh(false);
+}
+
+int Plot::XToScreen(float x, wxRect& rect)
+{
+   return rect.x + lrint((x-m_xmin)*rect.width/(m_xmax-m_xmin));
+}
+
+int Plot::YToScreen(float y, wxRect& rect)
+{
+   return rect.y + rect.height - lrint((y-m_ymin)*rect.height/(m_ymax-m_ymin));
+}
+
+BEGIN_EVENT_TABLE(Plot, wxPanelWrapper)
+   EVT_PAINT(Plot::OnPaint)
+   EVT_SIZE(Plot::OnSize)
+END_EVENT_TABLE()
diff --git a/src/widgets/Plot.h b/src/widgets/Plot.h
new file mode 100644
index 000000000..86253f956
--- /dev/null
+++ b/src/widgets/Plot.h
@@ -0,0 +1,58 @@
+/**********************************************************************
+
+   Audacity: A Digital Audio Editor
+
+   Plot.h
+
+   Max Maisel
+
+   This class is a generic plot.
+
+**********************************************************************/
+
+#ifndef __AUDACITY_PLOT__
+#define __AUDACITY_PLOT__
+
+#include "wxPanelWrapper.h" // to inherit
+
+#include "MemoryX.h"
+
+class Ruler;
+
+struct PlotData
+{
+   std::unique_ptr<wxPen> pen;
+   std::vector<float> xdata;
+   std::vector<float> ydata;
+};
+
+class Plot : public wxPanelWrapper
+{
+   public:
+      Plot(wxWindow *parent, wxWindowID winid,
+         float x_min, float x_max, float y_min, float y_max,
+         const TranslatableString& xlabel, const TranslatableString& ylabel,
+         int xformat = 1, int yformat = 1, //Ruler::RealFormat
+         int count = 1, const wxPoint& pos = wxDefaultPosition,
+         const wxSize& size = wxDefaultSize,
+         long style = wxTAB_TRAVERSAL | wxNO_BORDER);
+
+      inline PlotData* GetPlotData(int id)
+         { return &m_plots[id]; }
+
+   private:
+      void OnPaint(wxPaintEvent & evt);
+      void OnSize(wxSizeEvent & evt);
+
+      float m_xmin, m_xmax;
+      float m_ymin, m_ymax;
+      std::vector<PlotData> m_plots;
+      std::unique_ptr<Ruler> m_xruler, m_yruler;
+
+      int XToScreen(float x, wxRect& rect);
+      int YToScreen(float y, wxRect& rect);
+
+      DECLARE_EVENT_TABLE()
+};
+
+#endif

From ac277c61d7ec16e6c3b0a34a42f2988db81e06fe Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sat, 15 Feb 2020 14:52:55 +0100
Subject: [PATCH 06/33] Add gain and step response plot to Compressor2 effect
 GUI.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 47 +++++++++++++++++++++++++++++++++++++
 src/effects/Compressor2.h   |  3 +++
 2 files changed, 50 insertions(+)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 3130ca6f2..345029cb5 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -22,6 +22,7 @@
 #include <wx/intl.h>
 #include <wx/valgen.h>
 
+#include "../AColor.h"
 #include "../Internat.h"
 #include "../Prefs.h"
 #include "../ProjectFileManager.h"
@@ -29,7 +30,9 @@
 #include "../ShuttleGui.h"
 #include "../WaveTrack.h"
 #include "../widgets/valnum.h"
+#include "../widgets/Plot.h"
 #include "../widgets/ProgressDialog.h"
+#include "../widgets/Ruler.h"
 #include "../widgets/SliderTextCtrl.h"
 
 #include "LoadEffects.h"
@@ -254,6 +257,40 @@ bool EffectCompressor2::Process()
 
 void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
 {
+   S.SetBorder(10);
+
+   S.StartHorizontalLay(wxEXPAND, true);
+   {
+      PlotData* plot;
+
+      mGainPlot = S.MinSize( { 200, 200 } )
+         .AddPlot({}, -60, 0, -60, 0, XO("dB"), XO("dB"),
+            Ruler::LinearDBFormat, Ruler::LinearDBFormat);
+
+      plot = mGainPlot->GetPlotData(0);
+      plot->pen = std::unique_ptr<wxPen>(
+         safenew wxPen(AColor::WideEnvelopePen));
+
+      mResponsePlot = S.MinSize( { 200, 200 } )
+         .AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""),
+            Ruler::IntFormat, Ruler::RealFormat, 2);
+
+      plot = mResponsePlot->GetPlotData(0);
+      plot->pen = std::unique_ptr<wxPen>(
+         safenew wxPen(AColor::WideEnvelopePen));
+      plot->xdata = {0, 2, 2, 3, 3, 5};
+      plot->ydata = {0, 0, 1, 1, 0, 0};
+
+      plot = mResponsePlot->GetPlotData(1);
+      plot->pen = std::unique_ptr<wxPen>(
+         safenew wxPen(AColor::WideEnvelopePen));
+      plot->pen->SetColour(wxColor( 230,80,80 )); // Same color as TrackArtist RMS red.
+      plot->pen->SetWidth(2);
+   }
+   S.EndHorizontalLay();
+
+   S.SetBorder(5);
+
    S.StartStatic(XO("Algorithm"));
    {
       S.StartMultiColumn(2, wxALIGN_LEFT);
@@ -437,4 +474,14 @@ void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt))
 
 void EffectCompressor2::UpdateUI()
 {
+   PlotData* plot;
+   plot = mGainPlot->GetPlotData(0);
+   plot->xdata = {-60, -40, 0};
+   plot->ydata = {-60, -40, -20};
+   mGainPlot->Refresh(false);
+
+   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);
 }
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index 1c9f11f89..eb8f0906f 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -20,6 +20,7 @@
 
 #include "Effect.h"
 
+class Plot;
 class ShuttleGui;
 
 class EffectCompressor2 final : public Effect
@@ -77,6 +78,8 @@ private:
    double    mMakeupGainPct;
    double    mDryWetPct;
 
+   Plot* mGainPlot;
+   Plot* mResponsePlot;
    bool mIgnoreGuiEvents;
 
    DECLARE_EVENT_TABLE()

From 7326edbefe661593c86bcce789167eed7c138680 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sat, 15 Feb 2020 18:42:06 +0100
Subject: [PATCH 07/33] Implement compressor gain calculation and preview.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 70 ++++++++++++++++++++++++++++++++++---
 src/effects/Compressor2.h   |  8 ++++-
 2 files changed, 72 insertions(+), 6 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 345029cb5..1e61ec65b 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -18,6 +18,7 @@
 #include "Compressor2.h"
 
 #include <math.h>
+#include <numeric>
 
 #include <wx/intl.h>
 #include <wx/valgen.h>
@@ -270,6 +271,9 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
       plot = mGainPlot->GetPlotData(0);
       plot->pen = std::unique_ptr<wxPen>(
          safenew wxPen(AColor::WideEnvelopePen));
+      plot->xdata.resize(61);
+      plot->ydata.resize(61);
+      std::iota(plot->xdata.begin(), plot->xdata.end(), -60);
 
       mResponsePlot = S.MinSize( { 200, 200 } )
          .AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""),
@@ -465,6 +469,46 @@ bool EffectCompressor2::TransferDataFromWindow()
 
 // EffectCompressor2 implementation
 
+void EffectCompressor2::InitGainCalculation()
+{
+   mMakeupGainDB = mMakeupGainPct / 100.0 *
+      -(mThresholdDB * (1.0 - 1.0 / mRatio));
+   mMakeupGain = DB_TO_LINEAR(mMakeupGainDB);
+}
+
+double EffectCompressor2::CompressorGain(double env)
+{
+   double kneeCond;
+   double envDB = LINEAR_TO_DB(env);
+
+   // envDB can become NaN is env is exactly zero.
+   // As solution, use a very low dB value to prevent NaN propagation.
+   if(isnan(envDB))
+      envDB = -200;
+
+   kneeCond = 2.0 * (envDB - mThresholdDB);
+   if(kneeCond < -mKneeWidthDB)
+   {
+      // Below threshold: only apply make-up gain
+      return mMakeupGain;
+   }
+   else if(kneeCond >= mKneeWidthDB)
+   {
+      // Above threshold: apply compression and make-up gain
+      return DB_TO_LINEAR(mThresholdDB +
+         (envDB - mThresholdDB) / mRatio + mMakeupGainDB - envDB);
+   }
+   else
+   {
+      // Within knee: apply interpolated compression and make-up gain
+      return DB_TO_LINEAR(
+         (1.0 / mRatio - 1.0)
+         * pow(envDB - mThresholdDB + mKneeWidthDB / 2.0, 2)
+         / (2.0 * mKneeWidthDB) + mMakeupGainDB);
+   }
+}
+
+
 void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt))
 {
    if(!mIgnoreGuiEvents)
@@ -474,14 +518,30 @@ void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt))
 
 void EffectCompressor2::UpdateUI()
 {
-   PlotData* plot;
-   plot = mGainPlot->GetPlotData(0);
-   plot->xdata = {-60, -40, 0};
-   plot->ydata = {-60, -40, -20};
-   mGainPlot->Refresh(false);
+   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);
 }
+
+void EffectCompressor2::UpdateCompressorPlot()
+{
+   PlotData* plot;
+   plot = mGainPlot->GetPlotData(0);
+   wxASSERT(plot->xdata.size() == plot->ydata.size());
+
+   InitGainCalculation();
+   size_t xsize = plot->xdata.size();
+   for(size_t i = 0; i < xsize; ++i)
+      plot->ydata[i] = plot->xdata[i] +
+         LINEAR_TO_DB(CompressorGain(DB_TO_LINEAR(plot->xdata[i])));
+
+// XXX: accessibility but fails with TranslatableString required
+//   mGainPlot->SetName(wxString::Format(
+//      "Compressor gain reduction: %.1f dB", plot->ydata[xsize-1]));
+   mGainPlot->Refresh(false);
+}
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index eb8f0906f..fc82aac4d 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -58,12 +58,14 @@ public:
 
 private:
    // EffectCompressor2 implementation
+   void InitGainCalculation();
+   double CompressorGain(double env);
 
    bool UpdateProgress();
    void OnUpdateUI(wxCommandEvent & evt);
    void UpdateUI();
+   void UpdateCompressorPlot();
 
-private:
    int    mAlgorithm;
    int    mCompressBy;
    bool   mStereoInd;
@@ -78,6 +80,10 @@ private:
    double    mMakeupGainPct;
    double    mDryWetPct;
 
+   // cached intermediate values
+   double mMakeupGain;
+   double mMakeupGainDB;
+
    Plot* mGainPlot;
    Plot* mResponsePlot;
    bool mIgnoreGuiEvents;

From 61e88b39ab39a5b8d8d7386a662ede4fa34aa2ee Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Fri, 21 Feb 2020 11:23:38 +0100
Subject: [PATCH 08/33] Implement sliding RMS and sliding max sample
 preprocessors.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 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<wxPen>(
          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<SamplePreprocessor> 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<SamplePreprocessor>(
+         safenew SlidingRmsPreprocessor(window_size, 1.0));
+   else
+      preproc = std::unique_ptr<SamplePreprocessor>(
+         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<float> 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<float> mWindow;
+      std::vector<float> 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;

From 94e21d8f1c56029f2864f069ee5480eebb37dd4f Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sat, 22 Feb 2020 10:15:16 +0100
Subject: [PATCH 09/33] Implement ExpFit and Pt1 envelope detectors including
 preview.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 173 ++++++++++++++++++++++++++++++++++--
 src/effects/Compressor2.h   |  44 +++++++++
 2 files changed, 209 insertions(+), 8 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index f7f5d03ed..163f1c228 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -196,6 +196,157 @@ float SlidingMaxPreprocessor::DoProcessSample(float value)
    return std::max(mMaxes[currentHead], mMaxes[nextHead]);
 }
 
+EnvelopeDetector::EnvelopeDetector(size_t buffer_size)
+   : mPos(0),
+   mLookaheadBuffer(buffer_size, 0),
+   mProcessingBuffer(buffer_size, 0),
+   mProcessedBuffer(buffer_size, 0)
+{
+}
+
+float EnvelopeDetector::ProcessSample(float value)
+{
+   float retval = mProcessedBuffer[mPos];
+   mLookaheadBuffer[mPos++] = value;
+   if(mPos == mProcessingBuffer.size())
+   {
+      Follow();
+      mPos = 0;
+      mProcessedBuffer.swap(mProcessingBuffer);
+      mLookaheadBuffer.swap(mProcessingBuffer);
+   }
+   return retval;
+}
+
+size_t EnvelopeDetector::GetBlockSize() const
+{
+   wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
+   wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size());
+   return mLookaheadBuffer.size();
+}
+
+ExpFitEnvelopeDetector::ExpFitEnvelopeDetector(
+   float rate, float attackTime, float releaseTime)
+   : EnvelopeDetector(TAU_FACTOR * (attackTime + 1.0) * rate)
+{
+   mAttackFactor = exp(-1.0 / (rate * attackTime));
+   mReleaseFactor = exp(-1.0 / (rate * releaseTime));
+}
+
+void ExpFitEnvelopeDetector::Follow()
+{
+   /*
+   "Follow"ing algorithm by Roger B. Dannenberg, taken from
+   Nyquist.  His description follows.  -DMM
+
+   Description: this is a sophisticated envelope follower.
+   The input is an envelope, e.g. something produced with
+   the AVG function. The purpose of this function is to
+   generate a smooth envelope that is generally not less
+   than the input signal. In other words, we want to "ride"
+   the peaks of the signal with a smooth function. The
+   algorithm is as follows: keep a current output value
+   (called the "value"). The value is allowed to increase
+   by at most rise_factor and decrease by at most fall_factor.
+   Therefore, the next value should be between
+   value * rise_factor and value * fall_factor. If the input
+   is in this range, then the next value is simply the input.
+   If the input is less than value * fall_factor, then the
+   next value is just value * fall_factor, which will be greater
+   than the input signal. If the input is greater than value *
+   rise_factor, then we compute a rising envelope that meets
+   the input value by working backwards in time, changing the
+   previous values to input / rise_factor, input / rise_factor^2,
+   input / rise_factor^3, etc. until this NEW envelope intersects
+   the previously computed values. There is only a limited buffer
+   in which we can work backwards, so if the NEW envelope does not
+   intersect the old one, then make yet another pass, this time
+   from the oldest buffered value forward, increasing on each
+   sample by rise_factor to produce a maximal envelope. This will
+   still be less than the input.
+
+   The value has a lower limit of floor to make sure value has a
+   reasonable positive value from which to begin an attack.
+   */
+   wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
+   wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size());
+
+   // First apply a peak detect with the requested release rate.
+   size_t buffer_size = mProcessingBuffer.size();
+   double env = mProcessedBuffer[buffer_size-1];
+   for(size_t i = 0; i < buffer_size; ++i)
+   {
+      env *= mReleaseFactor;
+      if(mProcessingBuffer[i] > env)
+         env = mProcessingBuffer[i];
+      mProcessingBuffer[i] = env;
+   }
+   // Preprocess lookahead buffer as well.
+   for(size_t i = 0; i < buffer_size; ++i)
+   {
+      env *= mReleaseFactor;
+      if(mLookaheadBuffer[i] > env)
+         env = mLookaheadBuffer[i];
+      mLookaheadBuffer[i] = env;
+   }
+
+   // Next do the same process in reverse direction to get the
+   // requested attack rate and preprocess lookahead buffer.
+   for(ssize_t i = buffer_size - 1; i >= 0; --i)
+   {
+      env *= mAttackFactor;
+      if(mLookaheadBuffer[i] < env)
+         mLookaheadBuffer[i] = env;
+      else
+         env = mLookaheadBuffer[i];
+   }
+   for(ssize_t i = buffer_size - 1; i >= 0; --i)
+   {
+      if(mProcessingBuffer[i] < env * mAttackFactor)
+      {
+         env *= mAttackFactor;
+         mProcessingBuffer[i] = env;
+      }
+      else if(mProcessingBuffer[i] > env)
+         // Intersected the previous envelope buffer, so we are finished
+         return;
+      else
+         ; // Do nothing if we are on a plateau from peak look-around
+   }
+}
+
+Pt1EnvelopeDetector::Pt1EnvelopeDetector(
+   float rate, float attackTime, float releaseTime, bool correctGain)
+   : EnvelopeDetector(TAU_FACTOR * (attackTime + 1.0) * rate)
+{
+   // Approximate peak amplitude correction factor.
+   if(correctGain)
+      mGainCorrection = 1.0 + exp(attackTime / 30.0);
+   else
+      mGainCorrection = 1.0;
+
+   mAttackFactor = 1.0 / (attackTime * rate);
+   mReleaseFactor  = 1.0 / (releaseTime  * rate);
+}
+
+void Pt1EnvelopeDetector::Follow()
+{
+   wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
+   wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size());
+
+   // Simulate analog compressor with PT1 characteristic.
+   size_t buffer_size = mProcessingBuffer.size();
+   float level = mProcessedBuffer[buffer_size-1] / mGainCorrection;
+   for(size_t i = 0; i < buffer_size; ++i)
+   {
+      if(mProcessingBuffer[i] >= level)
+         level += mAttackFactor * (mProcessingBuffer[i] - level);
+      else
+         level += mReleaseFactor * (mProcessingBuffer[i] - level);
+      mProcessingBuffer[i] = level * mGainCorrection;
+   }
+}
+
 EffectCompressor2::EffectCompressor2()
    : mIgnoreGuiEvents(false)
 {
@@ -612,7 +763,6 @@ double EffectCompressor2::CompressorGain(double env)
    }
 }
 
-
 void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt))
 {
    if(!mIgnoreGuiEvents)
@@ -651,6 +801,7 @@ void EffectCompressor2::UpdateResponsePlot()
    wxASSERT(plot->xdata.size() == plot->ydata.size());
 
    std::unique_ptr<SamplePreprocessor> preproc;
+   std::unique_ptr<EnvelopeDetector> envelope;
    float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME;
 
    size_t window_size =
@@ -665,21 +816,27 @@ void EffectCompressor2::UpdateResponsePlot()
       preproc = std::unique_ptr<SamplePreprocessor>(
          safenew SlidingMaxPreprocessor(window_size));
 
+   if(mAlgorithm == kExpFit)
+      envelope = std::unique_ptr<EnvelopeDetector>(
+         safenew ExpFitEnvelopeDetector(plot_rate, mAttackTime, mReleaseTime));
+   else
+      envelope = std::unique_ptr<EnvelopeDetector>(
+         safenew Pt1EnvelopeDetector(plot_rate, mAttackTime, mReleaseTime, false));
+
    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)
+   ssize_t block_size = envelope->GetBlockSize();
+   for(ssize_t i = -lookahead_size; i < 2*block_size; ++i)
    {
       if(i < step_start || i > step_stop)
-         value = preproc->ProcessSample(0);
+         envelope->ProcessSample(preproc->ProcessSample(0));
       else
-         value = preproc->ProcessSample(1);
-
-      if(i >= 0)
-         plot->ydata[i] = value;
+         envelope->ProcessSample(preproc->ProcessSample(1));
    }
+   for(ssize_t i = 0; i < xsize; ++i)
+      plot->ydata[i] = envelope->ProcessSample(preproc->ProcessSample(0));
 
    mResponsePlot->Refresh(false);
 }
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index 2661b8eda..36ac69c96 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -67,6 +67,50 @@ class SlidingMaxPreprocessor : public SamplePreprocessor
       inline float DoProcessSample(float value);
 };
 
+class EnvelopeDetector
+{
+   public:
+      EnvelopeDetector(size_t buffer_size);
+
+      float ProcessSample(float value);
+      size_t GetBlockSize() const;
+   protected:
+      static const int TAU_FACTOR = 5;
+
+      size_t mPos;
+      std::vector<float> mLookaheadBuffer;
+      std::vector<float> mProcessingBuffer;
+      std::vector<float> mProcessedBuffer;
+
+      virtual void Follow() = 0;
+};
+
+class ExpFitEnvelopeDetector : public EnvelopeDetector
+{
+   public:
+      ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime);
+
+   private:
+      double mAttackFactor;
+      double mReleaseFactor;
+
+      virtual void Follow();
+};
+
+class Pt1EnvelopeDetector : public EnvelopeDetector
+{
+   public:
+      Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime,
+         bool correctGain = true);
+
+   private:
+      double mGainCorrection;
+      double mAttackFactor;
+      double mReleaseFactor;
+
+      virtual void Follow();
+};
+
 class EffectCompressor2 final : public Effect
 {
 public:

From 6941c66de8220e729fcc6fd0af78cf97c035027a Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Wed, 27 May 2020 16:39:19 +0200
Subject: [PATCH 10/33] Implement compressor2 offline processing.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 397 ++++++++++++++++++++++++++++++++++--
 src/effects/Compressor2.h   |  63 +++++-
 2 files changed, 434 insertions(+), 26 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 163f1c228..8bf9d3476 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -166,7 +166,7 @@ SlidingMaxPreprocessor::SlidingMaxPreprocessor(size_t windowSize)
 
 float SlidingMaxPreprocessor::ProcessSample(float value)
 {
-   return DoProcessSample(value);
+   return DoProcessSample(fabs(value));
 }
 
 float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR)
@@ -226,8 +226,8 @@ size_t EnvelopeDetector::GetBlockSize() const
 }
 
 ExpFitEnvelopeDetector::ExpFitEnvelopeDetector(
-   float rate, float attackTime, float releaseTime)
-   : EnvelopeDetector(TAU_FACTOR * (attackTime + 1.0) * rate)
+   float rate, float attackTime, float releaseTime, size_t bufferSize)
+   : EnvelopeDetector(bufferSize)
 {
    mAttackFactor = exp(-1.0 / (rate * attackTime));
    mReleaseFactor = exp(-1.0 / (rate * releaseTime));
@@ -316,8 +316,9 @@ void ExpFitEnvelopeDetector::Follow()
 }
 
 Pt1EnvelopeDetector::Pt1EnvelopeDetector(
-   float rate, float attackTime, float releaseTime, bool correctGain)
-   : EnvelopeDetector(TAU_FACTOR * (attackTime + 1.0) * rate)
+   float rate, float attackTime, float releaseTime, size_t bufferSize,
+   bool correctGain)
+   : EnvelopeDetector(bufferSize)
 {
    // Approximate peak amplitude correction factor.
    if(correctGain)
@@ -347,6 +348,49 @@ void Pt1EnvelopeDetector::Follow()
    }
 }
 
+void PipelineBuffer::pad_to(size_t len, float value, bool stereo)
+{
+   if(size < len)
+   {
+      size = len;
+      std::fill(mBlockBuffer[0].get() + trackSize,
+         mBlockBuffer[0].get() + size, value);
+      if(stereo)
+         std::fill(mBlockBuffer[1].get() + trackSize,
+            mBlockBuffer[1].get() + size, value);
+   }
+}
+
+void PipelineBuffer::swap(PipelineBuffer& other)
+{
+   std::swap(trackPos, other.trackPos);
+   std::swap(trackSize, other.trackSize);
+   std::swap(size, other.size);
+   std::swap(mBlockBuffer[0], other.mBlockBuffer[0]);
+   std::swap(mBlockBuffer[1], other.mBlockBuffer[1]);
+}
+
+void PipelineBuffer::init(size_t capacity, bool stereo)
+{
+   trackPos = 0;
+   trackSize = 0;
+   size = 0;
+   mCapacity = capacity;
+   mBlockBuffer[0].reinit(capacity);
+   std::fill(mBlockBuffer[0].get(), mBlockBuffer[0].get() + capacity, 0);
+   if(stereo)
+   {
+      mBlockBuffer[1].reinit(capacity);
+      std::fill(mBlockBuffer[1].get(), mBlockBuffer[1].get() + capacity, 0);
+   }
+}
+
+void PipelineBuffer::free()
+{
+   mBlockBuffer[0].reset();
+   mBlockBuffer[1].reset();
+}
+
 EffectCompressor2::EffectCompressor2()
    : mIgnoreGuiEvents(false)
 {
@@ -503,7 +547,52 @@ bool EffectCompressor2::Startup()
 
 bool EffectCompressor2::Process()
 {
-   return false;
+   // Iterate over each track
+   this->CopyInputTracks(); // Set up mOutputTracks.
+   bool bGoodResult = true;
+
+   AllocPipeline();
+   mProgressVal = 0;
+
+   for(auto track : mOutputTracks->Selected<WaveTrack>()
+      + (mStereoInd ? &Track::Any : &Track::IsLeader))
+   {
+      // Get start and end times from track
+      // PRL: No accounting for multiple channels ?
+      double trackStart = track->GetStartTime();
+      double trackEnd = track->GetEndTime();
+
+      // Set the current bounds to whichever left marker is
+      // greater and whichever right marker is less:
+      mCurT0 = mT0 < trackStart? trackStart: mT0;
+      mCurT1 = mT1 > trackEnd? trackEnd: mT1;
+
+      // Get the track rate
+      mSampleRate = track->GetRate();
+
+      auto range = mStereoInd
+         ? TrackList::SingletonRange(track)
+         : TrackList::Channels(track);
+
+      mProcStereo = range.size() > 1;
+
+      InitGainCalculation();
+      mPreproc = InitPreprocessor(mSampleRate);
+      mEnvelope = InitEnvelope(mSampleRate, mPipeline[0].capacity());
+
+      if(!ProcessOne(range))
+      {
+         // Processing failed -> abort
+         bGoodResult = false;
+         break;
+      }
+   }
+
+   this->ReplaceProcessedTracks(bGoodResult);
+   mPreproc.reset(nullptr);
+   mEnvelope.reset(nullptr);
+   FreePipeline();
+   return bGoodResult;
 }
 
 void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
@@ -726,6 +815,7 @@ bool EffectCompressor2::TransferDataFromWindow()
 
 void EffectCompressor2::InitGainCalculation()
 {
+   mDryWet = mDryWetPct / 100.0;
    mMakeupGainDB = mMakeupGainPct / 100.0 *
       -(mThresholdDB * (1.0 - 1.0 / mRatio));
    mMakeupGain = DB_TO_LINEAR(mMakeupGainDB);
@@ -763,6 +853,282 @@ double EffectCompressor2::CompressorGain(double env)
    }
 }
 
+std::unique_ptr<SamplePreprocessor> EffectCompressor2::InitPreprocessor(
+   double rate, bool preview)
+{
+   size_t window_size =
+      std::max(1, int(round((mLookaheadTime + mLookbehindTime) * rate)));
+
+   if(mCompressBy == kAmplitude)
+      return std::unique_ptr<SamplePreprocessor>(safenew
+         SlidingMaxPreprocessor(window_size));
+   else
+      return std::unique_ptr<SamplePreprocessor>(safenew
+         SlidingRmsPreprocessor(window_size, preview ? 1.0 : 2.0));
+}
+
+std::unique_ptr<EnvelopeDetector> EffectCompressor2::InitEnvelope(
+   double rate, size_t blockSize, bool preview)
+{
+   if(mAlgorithm == kExpFit)
+      return std::unique_ptr<EnvelopeDetector>(safenew
+         ExpFitEnvelopeDetector(rate, mAttackTime, mReleaseTime, blockSize));
+   else
+      return std::unique_ptr<EnvelopeDetector>(safenew
+         Pt1EnvelopeDetector(rate, mAttackTime, mReleaseTime, blockSize,
+            !preview && mCompressBy != kAmplitude));
+}
+
+size_t EffectCompressor2::CalcBufferSize(size_t sampleRate)
+{
+   size_t capacity;
+
+   mLookaheadLength =
+      std::max(0, int(round(mLookaheadTime * sampleRate)));
+   capacity = mLookaheadLength +
+      size_t(float(TAU_FACTOR) * (1.0 + mAttackTime) * sampleRate);
+   if(capacity < MIN_BUFFER_CAPACITY)
+      capacity = MIN_BUFFER_CAPACITY;
+   return capacity;
+}
+
+/// Get required buffer size for the largest whole track and allocate buffers.
+/// This reduces the amount of allocations required.
+void EffectCompressor2::AllocPipeline()
+{
+   bool stereoTrackFound = false;
+   double maxSampleRate = 0;
+   size_t capacity;
+
+   mProcStereo = false;
+
+   for(auto track : mOutputTracks->Selected<WaveTrack>() + &Track::Any)
+   {
+      maxSampleRate = std::max(maxSampleRate, track->GetRate());
+
+      // There is a stereo track
+      if(track->IsLeader())
+         stereoTrackFound = true;
+   }
+
+   // Initiate a processing quad-buffer. This buffer will (most likely)
+   // be shorter than the length of the track being processed.
+   stereoTrackFound = stereoTrackFound && !mStereoInd;
+   capacity = CalcBufferSize(maxSampleRate);
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+      mPipeline[i].init(capacity, stereoTrackFound);
+}
+
+void EffectCompressor2::FreePipeline()
+{
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+      mPipeline[i].free();
+}
+
+void EffectCompressor2::SwapPipeline()
+{
+   ++mProgressVal;
+   for(size_t i = 0; i < PIPELINE_DEPTH-1; ++i)
+      mPipeline[i].swap(mPipeline[i+1]);
+   std::cerr << "\n";
+}
+
+/// ProcessOne() takes a track, transforms it to bunch of buffer-blocks,
+/// and executes ProcessData, on it...
+bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range)
+{
+   WaveTrack* track = *range.begin();
+
+   // Transform the marker timepoints to samples
+   const auto start = track->TimeToLongSamples(mCurT0);
+   const auto end   = track->TimeToLongSamples(mCurT1);
+
+   // Get the length of the buffer (as double). len is
+   // used simply to calculate a progress meter, so it is easier
+   // to make it a double now than it is to do it later
+   mTrackLen = (end - start).as_double();
+
+   // Abort if the right marker is not to the right of the left marker
+   if(mCurT1 <= mCurT0)
+      return false;
+
+   // Go through the track one buffer at a time. s counts which
+   // sample the current buffer starts at.
+   auto pos = start;
+
+   mProgressVal = 0;
+   while(pos < end)
+   {
+      StorePipeline(range);
+      SwapPipeline();
+
+      const size_t remainingLen = (end - pos).as_size_t();
+
+      // Get a block of samples (smaller than the size of the buffer)
+      // Adjust the block size if it is the final block in the track
+      const auto blockLen = limitSampleBufferSize(
+         remainingLen, mPipeline[PIPELINE_DEPTH-1].capacity());
+
+      mPipeline[PIPELINE_DEPTH-1].trackPos = pos;
+      if(!LoadPipeline(range, blockLen))
+         return false;
+
+      if(mPipeline[0].size == 0)
+         FillPipeline();
+      else
+         ProcessPipeline();
+
+      // Increment s one blockfull of samples
+      pos += blockLen;
+   }
+
+   // Handle short selections
+   while(mPipeline[1].size == 0)
+   {
+      SwapPipeline();
+      FillPipeline();
+   }
+
+   while(PipelineHasData())
+   {
+      StorePipeline(range);
+      SwapPipeline();
+      DrainPipeline();
+   }
+   StorePipeline(range);
+
+   // Return true because the effect processing succeeded ... unless cancelled
+   return true;
+}
+
+bool EffectCompressor2::LoadPipeline(
+   TrackIterRange<WaveTrack> range, size_t len)
+{
+   sampleCount read_size = -1;
+   sampleCount last_read_size = -1;
+   // Get the samples from the track and put them in the buffer
+   int idx = 0;
+   for(auto channel : range)
+   {
+      channel->Get((samplePtr) mPipeline[PIPELINE_DEPTH-1][idx],
+         floatSample, mPipeline[PIPELINE_DEPTH-1].trackPos, len,
+         fillZero, true, &read_size);
+      // WaveTrack::Get returns the amount of read samples excluding zero
+      // filled samples from clip gaps. But in case of stereo tracks with
+      // assymetric gaps it still returns the same number for both channels.
+      //
+      // Fail if we read different sample count from stereo pair tracks.
+      // Ignore this check during first iteration (last_read_size == -1).
+      if(read_size != last_read_size && last_read_size.as_long_long() != -1)
+         return false;
+      mPipeline[PIPELINE_DEPTH-1].trackSize = read_size.as_size_t();
+      mPipeline[PIPELINE_DEPTH-1].size = read_size.as_size_t();
+      ++idx;
+   }
+
+   wxASSERT(mPipeline[PIPELINE_DEPTH-2].trackSize == 0 ||
+      mPipeline[PIPELINE_DEPTH-2].trackSize >=
+      mPipeline[PIPELINE_DEPTH-1].trackSize);
+   return true;
+}
+
+void EffectCompressor2::FillPipeline()
+{
+   // TODO: correct end conditions
+   mPipeline[PIPELINE_DEPTH-1].pad_to(mEnvelope->GetBlockSize(), 0, mProcStereo);
+
+   size_t length = mPipeline[PIPELINE_DEPTH-1].size;
+   for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp)
+   {
+      // TODO: correct initial conditions
+      if(rp < length)
+         EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp);
+      else
+         EnvelopeSample(mPipeline[PIPELINE_DEPTH-1], rp % length);
+   }
+}
+
+void EffectCompressor2::ProcessPipeline()
+{
+   float env;
+   size_t length = mPipeline[0].size;
+
+   for(size_t i = 0; i < PIPELINE_DEPTH-2; ++i)
+      { wxASSERT(mPipeline[0].size == mPipeline[i+1].size); }
+
+   for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp)
+   {
+      if(rp < length)
+         env = EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp);
+      else if((rp % length) < mPipeline[PIPELINE_DEPTH-1].size)
+         env = EnvelopeSample(mPipeline[PIPELINE_DEPTH-1], rp % length);
+      else
+         // TODO: correct end condition
+         env = mEnvelope->ProcessSample(mPreproc->ProcessSample(0.0));
+      CompressSample(env, wp);
+   }
+}
+
+inline float EffectCompressor2::EnvelopeSample(PipelineBuffer& pbuf, size_t rp)
+{
+   float preprocessed;
+   if(mProcStereo)
+      preprocessed = mPreproc->ProcessSample(pbuf[0][rp], pbuf[1][rp]);
+   else
+      preprocessed = mPreproc->ProcessSample(pbuf[0][rp]);
+   return mEnvelope->ProcessSample(preprocessed);
+}
+
+inline void EffectCompressor2::CompressSample(float env, size_t wp)
+{
+   float gain = (1.0 - mDryWet) + CompressorGain(env) * mDryWet;
+   mPipeline[0][0][wp] = mPipeline[0][0][wp] * gain;
+   if(mProcStereo)
+      mPipeline[0][1][wp] = mPipeline[0][1][wp] * gain;
+}
+
+bool EffectCompressor2::PipelineHasData()
+{
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+   {
+      if(mPipeline[i].size != 0)
+         return true;
+   }
+   return false;
+}
+
+void EffectCompressor2::DrainPipeline()
+{
+   float env;
+   size_t length = mPipeline[0].size;
+   size_t length2 = mPipeline[PIPELINE_DEPTH-2].size;
+   for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp)
+   {
+      if(rp < length2 && mPipeline[PIPELINE_DEPTH-2].size != 0)
+      {
+         env = EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp);
+      }
+      else
+         // TODO: correct end condition
+         env = mEnvelope->ProcessSample(mPreproc->ProcessSample(0.0));
+      CompressSample(env, wp);
+   }
+}
+
+void EffectCompressor2::StorePipeline(TrackIterRange<WaveTrack> range)
+{
+   int idx = 0;
+   for(auto channel : range)
+   {
+      // Copy the newly-changed samples back onto the track.
+      channel->Set((samplePtr) mPipeline[0][idx],
+         floatSample, mPipeline[0].trackPos, mPipeline[0].trackSize);
+      ++idx;
+   }
+   mPipeline[0].trackSize = 0;
+   mPipeline[0].size = 0;
+}
+
 void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt))
 {
    if(!mIgnoreGuiEvents)
@@ -804,30 +1170,17 @@ void EffectCompressor2::UpdateResponsePlot()
    std::unique_ptr<EnvelopeDetector> envelope;
    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)));
+   ssize_t block_size = float(TAU_FACTOR) * (mAttackTime + 1.0) * plot_rate;
 
-   if(mCompressBy == kRMS)
-      preproc = std::unique_ptr<SamplePreprocessor>(
-         safenew SlidingRmsPreprocessor(window_size, 1.0));
-   else
-      preproc = std::unique_ptr<SamplePreprocessor>(
-         safenew SlidingMaxPreprocessor(window_size));
-
-   if(mAlgorithm == kExpFit)
-      envelope = std::unique_ptr<EnvelopeDetector>(
-         safenew ExpFitEnvelopeDetector(plot_rate, mAttackTime, mReleaseTime));
-   else
-      envelope = std::unique_ptr<EnvelopeDetector>(
-         safenew Pt1EnvelopeDetector(plot_rate, mAttackTime, mReleaseTime, false));
+   preproc = InitPreprocessor(plot_rate, true);
+   envelope = InitEnvelope(plot_rate, block_size, true);
 
    ssize_t step_start = RESPONSE_PLOT_STEP_START * plot_rate - lookahead_size;
    ssize_t step_stop = RESPONSE_PLOT_STEP_STOP * plot_rate - lookahead_size;
 
    ssize_t xsize = plot->xdata.size();
-   ssize_t block_size = envelope->GetBlockSize();
    for(ssize_t i = -lookahead_size; i < 2*block_size; ++i)
    {
       if(i < step_start || i > step_stop)
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index 36ac69c96..57c69b766 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -75,8 +75,6 @@ class EnvelopeDetector
       float ProcessSample(float value);
       size_t GetBlockSize() const;
    protected:
-      static const int TAU_FACTOR = 5;
-
       size_t mPos;
       std::vector<float> mLookaheadBuffer;
       std::vector<float> mProcessingBuffer;
@@ -88,7 +86,8 @@ class EnvelopeDetector
 class ExpFitEnvelopeDetector : public EnvelopeDetector
 {
    public:
-      ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime);
+      ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime,
+         size_t buffer_size = 0);
 
    private:
       double mAttackFactor;
@@ -101,7 +100,7 @@ class Pt1EnvelopeDetector : public EnvelopeDetector
 {
    public:
       Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime,
-         bool correctGain = true);
+         size_t buffer_size = 0, bool correctGain = true);
 
    private:
       double mGainCorrection;
@@ -111,6 +110,27 @@ class Pt1EnvelopeDetector : public EnvelopeDetector
       virtual void Follow();
 };
 
+struct PipelineBuffer
+{
+   public:
+      sampleCount trackPos;
+      size_t trackSize;
+      size_t size;
+
+      inline float* operator[](size_t idx)
+         { return mBlockBuffer[idx].get(); }
+
+      void pad_to(size_t len, float value, bool stereo);
+      void swap(PipelineBuffer& other);
+      void init(size_t size, bool stereo);
+      inline size_t capacity() const { return mCapacity; }
+      void free();
+
+   private:
+      size_t mCapacity;
+      Floats mBlockBuffer[2];
+};
+
 class EffectCompressor2 final : public Effect
 {
 public:
@@ -148,6 +168,24 @@ private:
    // EffectCompressor2 implementation
    void InitGainCalculation();
    double CompressorGain(double env);
+   std::unique_ptr<SamplePreprocessor> InitPreprocessor(
+      double rate, bool preview = false);
+   std::unique_ptr<EnvelopeDetector> InitEnvelope(
+      double rate, size_t blockSize = 0, bool preview = false);
+   size_t CalcBufferSize(size_t sampleRate);
+
+   void AllocPipeline();
+   void FreePipeline();
+   void SwapPipeline();
+   bool ProcessOne(TrackIterRange<WaveTrack> range);
+   bool LoadPipeline(TrackIterRange<WaveTrack> range, size_t len);
+   void FillPipeline();
+   void ProcessPipeline();
+   inline float EnvelopeSample(PipelineBuffer& pbuf, size_t rp);
+   inline void CompressSample(float env, size_t wp);
+   bool PipelineHasData();
+   void DrainPipeline();
+   void StorePipeline(TrackIterRange<WaveTrack> range);
 
    bool UpdateProgress();
    void OnUpdateUI(wxCommandEvent & evt);
@@ -155,6 +193,21 @@ private:
    void UpdateCompressorPlot();
    void UpdateResponsePlot();
 
+   static const int TAU_FACTOR = 5;
+   static const size_t MIN_BUFFER_CAPACITY = 1048576; // 1MB
+
+   static const size_t PIPELINE_DEPTH = 4;
+   PipelineBuffer mPipeline[PIPELINE_DEPTH];
+
+   double mCurT0;
+   double mCurT1;
+   double mProgressVal;
+   double mTrackLen;
+   bool mProcStereo;
+
+   std::unique_ptr<SamplePreprocessor> mPreproc;
+   std::unique_ptr<EnvelopeDetector> mEnvelope;
+
    int    mAlgorithm;
    int    mCompressBy;
    bool   mStereoInd;
@@ -170,8 +223,10 @@ private:
    double    mDryWetPct;
 
    // cached intermediate values
+   double mDryWet;
    double mMakeupGain;
    double mMakeupGainDB;
+   size_t mLookaheadLength;
 
    static const size_t RESPONSE_PLOT_SAMPLES = 200;
    static const size_t RESPONSE_PLOT_TIME = 5;

From 1d7b143c915e55227b5ce85c7ea29360c3505e66 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Wed, 27 May 2020 16:39:54 +0200
Subject: [PATCH 11/33] Debugging helper code for Compressor2 effect.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 scripts/debug/compressor2_buffers.m |  25 +++++
 src/effects/Compressor2.cpp         | 160 ++++++++++++++++++++++++++++
 src/effects/Compressor2.h           |   1 +
 3 files changed, 186 insertions(+)
 create mode 100644 scripts/debug/compressor2_buffers.m

diff --git a/scripts/debug/compressor2_buffers.m b/scripts/debug/compressor2_buffers.m
new file mode 100644
index 000000000..f20c85b12
--- /dev/null
+++ b/scripts/debug/compressor2_buffers.m
@@ -0,0 +1,25 @@
+%% Debug Compressor v2 pipeline buffers
+buffer_ids = [1,2,3,4,5];
+prefix = '/tmp';
+
+figure(1);
+for k = 1:length(buffer_ids)
+  subplot(length(buffer_ids), 1, k)
+  bfile = fopen(sprintf('%s/envbuf.%d.bin', prefix, buffer_ids(k)));
+  env = fread(bfile, 'float').';
+  bfile = fopen(sprintf('%s/blockbuf.%d.bin', prefix, buffer_ids(k)));
+  block_raw = fread(bfile, 'float').';
+
+  sizes = reshape(block_raw(1:12), 3, 4);
+  capacity = (1:4).*sizes(3,:);
+  track_size = horzcat(0, capacity(1:3)) + sizes(1,:);
+  block = block_raw(13:end);
+
+  plot(block, 'b', 'linewidth', 3);
+  hold on;
+  plot(circshift(env, length(env)/3), 'r');
+  stem(capacity, ones(1, length(capacity)), 'g');
+  stem(track_size, 1.5.*ones(1, length(capacity)), 'b');
+  ylim([-2 2]);
+  hold off;
+end
\ No newline at end of file
diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 8bf9d3476..5c70ede44 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -38,6 +38,15 @@
 
 #include "LoadEffects.h"
 
+//#define DEBUG_COMPRESSOR2_DUMP_BUFFERS
+//#define DEBUG_COMPRESSOR2_ENV
+//#define DEBUG_COMPRESSOR2_TRACE
+
+#ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS
+#include <fstream>
+int buf_num;
+#endif
+
 enum kAlgorithms
 {
    kExpFit,
@@ -225,6 +234,19 @@ size_t EnvelopeDetector::GetBlockSize() const
    return mLookaheadBuffer.size();
 }
 
+const float* EnvelopeDetector::GetBuffer(int idx) const
+{
+   if(idx == 0)
+      return mProcessedBuffer.data();
+   else if(idx == 1)
+      return mProcessingBuffer.data();
+   else if(idx == 2)
+      return mLookaheadBuffer.data();
+   else
+      wxASSERT(false);
+   return nullptr;
+}
+
 ExpFitEnvelopeDetector::ExpFitEnvelopeDetector(
    float rate, float attackTime, float releaseTime, size_t bufferSize)
    : EnvelopeDetector(bufferSize)
@@ -927,10 +949,58 @@ void EffectCompressor2::FreePipeline()
 
 void EffectCompressor2::SwapPipeline()
 {
+#ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS
+   wxString blockname = wxString::Format("/tmp/blockbuf.%d.bin", buf_num);
+   std::cerr << "Writing to " << blockname << "\n" << std::flush;
+   std::fstream blockbuffer = std::fstream();
+   blockbuffer.open(blockname, std::ios::binary | std::ios::out);
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i) {
+      float val = mPipeline[i].trackSize;
+      blockbuffer.write((char*)&val, sizeof(float));
+      val = mPipeline[i].size;
+      blockbuffer.write((char*)&val, sizeof(float));
+      val = mPipeline[i].capacity();
+      blockbuffer.write((char*)&val, sizeof(float));
+   }
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+      blockbuffer.write((char*)mPipeline[i][0], mPipeline[i].capacity() * sizeof(float));
+
+   wxString envname = wxString::Format("/tmp/envbuf.%d.bin", buf_num++);
+   std::cerr << "Writing to " << envname << "\n" << std::flush;
+   std::fstream envbuffer = std::fstream();
+   envbuffer.open(envname, std::ios::binary | std::ios::out);
+   envbuffer.write((char*)mEnvelope->GetBuffer(0),
+      mEnvelope->GetBlockSize() * sizeof(float));
+   envbuffer.write((char*)mEnvelope->GetBuffer(1),
+      mEnvelope->GetBlockSize() * sizeof(float));
+   envbuffer.write((char*)mEnvelope->GetBuffer(2),
+      mEnvelope->GetBlockSize() * sizeof(float));
+
+   std::cerr << "PipelineState: ";
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+      std::cerr << !!mPipeline[i].size;
+   std::cerr << " ";
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+      std::cerr << !!mPipeline[i].trackSize;
+
+   std::cerr << "\ntrackSize: ";
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+      std::cerr << mPipeline[i].trackSize << " ";
+   std::cerr << "\ntrackPos: ";
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+      std::cerr << mPipeline[i].trackPos.as_size_t() << " ";
+   std::cerr << "\nsize: ";
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+      std::cerr << mPipeline[i].size << " ";
+   std::cerr << "\n" << std::flush;
+#endif
+
    ++mProgressVal;
    for(size_t i = 0; i < PIPELINE_DEPTH-1; ++i)
       mPipeline[i].swap(mPipeline[i+1]);
+#ifdef DEBUG_COMPRESSOR2_TRACE
    std::cerr << "\n";
+#endif
 }
 
 /// ProcessOne() takes a track, transforms it to bunch of buffer-blocks,
@@ -956,9 +1026,22 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range)
    // sample the current buffer starts at.
    auto pos = start;
 
+#ifdef DEBUG_COMPRESSOR2_TRACE
+   std::cerr << "ProcLen: " << (end - start).as_size_t() << "\n" << std::flush;
+   std::cerr << "EnvBlockLen: " << mEnvelope->GetBlockSize() << "\n" << std::flush;
+   std::cerr << "PipeBlockLen: " << mPipeline[0].capacity() << "\n" << std::flush;
+   std::cerr << "LookaheadLen: " << mLookaheadLength << "\n" << std::flush;
+#endif
+
    mProgressVal = 0;
+#ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS
+   buf_num = 0;
+#endif
    while(pos < end)
    {
+#ifdef DEBUG_COMPRESSOR2_TRACE
+      std::cerr << "ProcessBlock at: " << pos.as_size_t() << "\n" << std::flush;
+#endif
       StorePipeline(range);
       SwapPipeline();
 
@@ -985,6 +1068,15 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range)
    // Handle short selections
    while(mPipeline[1].size == 0)
    {
+#ifdef DEBUG_COMPRESSOR2_TRACE
+      std::cerr << "PaddingLoop: ";
+      for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+         std::cerr << !!mPipeline[i].size;
+      std::cerr << " ";
+      for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+         std::cerr << !!mPipeline[i].trackSize;
+      std::cerr << "\n" << std::flush;
+#endif
       SwapPipeline();
       FillPipeline();
    }
@@ -995,6 +1087,9 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range)
       SwapPipeline();
       DrainPipeline();
    }
+#ifdef DEBUG_COMPRESSOR2_TRACE
+   std::cerr << "StoreLastBlock\n" << std::flush;
+#endif
    StorePipeline(range);
 
    // Return true because the effect processing succeeded ... unless cancelled
@@ -1006,6 +1101,11 @@ bool EffectCompressor2::LoadPipeline(
 {
    sampleCount read_size = -1;
    sampleCount last_read_size = -1;
+#ifdef DEBUG_COMPRESSOR2_TRACE
+   std::cerr << "LoadBlock at: " <<
+      mPipeline[PIPELINE_DEPTH-1].trackPos.as_size_t() <<
+      " with len: " << len << "\n" << std::flush;
+#endif
    // Get the samples from the track and put them in the buffer
    int idx = 0;
    for(auto channel : range)
@@ -1034,6 +1134,16 @@ bool EffectCompressor2::LoadPipeline(
 
 void EffectCompressor2::FillPipeline()
 {
+#ifdef DEBUG_COMPRESSOR2_TRACE
+   std::cerr << "FillBlock: " <<
+      !!mPipeline[0].size << !!mPipeline[1].size <<
+      !!mPipeline[2].size << !!mPipeline[3].size <<
+      "\n" << std::flush;
+   std::cerr << "  from " << -int(mLookaheadLength)
+      << " to " << mPipeline[PIPELINE_DEPTH-1].size - mLookaheadLength << "\n" << std::flush;
+   std::cerr << "Padding from " << mPipeline[PIPELINE_DEPTH-1].trackSize
+      << " to " << mEnvelope->GetBlockSize() << "\n" << std::flush;
+#endif
    // TODO: correct end conditions
    mPipeline[PIPELINE_DEPTH-1].pad_to(mEnvelope->GetBlockSize(), 0, mProcStereo);
 
@@ -1050,12 +1160,26 @@ void EffectCompressor2::FillPipeline()
 
 void EffectCompressor2::ProcessPipeline()
 {
+#ifdef DEBUG_COMPRESSOR2_TRACE
+   std::cerr << "ProcessBlock: " <<
+      !!mPipeline[0].size << !!mPipeline[1].size <<
+      !!mPipeline[2].size << !!mPipeline[3].size <<
+      "\n" << std::flush;
+#endif
    float env;
    size_t length = mPipeline[0].size;
 
    for(size_t i = 0; i < PIPELINE_DEPTH-2; ++i)
       { wxASSERT(mPipeline[0].size == mPipeline[i+1].size); }
 
+#ifdef DEBUG_COMPRESSOR2_TRACE
+   std::cerr << "LookaheadLen: " << mLookaheadLength << "\n" << std::flush;
+   std::cerr << "PipeLength: " <<
+      mPipeline[0].size << " " << mPipeline[1].size << " " <<
+      mPipeline[2].size << " " << mPipeline[3].size <<
+      "\n" << std::flush;
+#endif
+
    for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp)
    {
       if(rp < length)
@@ -1082,7 +1206,14 @@ inline float EffectCompressor2::EnvelopeSample(PipelineBuffer& pbuf, size_t rp)
 inline void EffectCompressor2::CompressSample(float env, size_t wp)
 {
    float gain = (1.0 - mDryWet) + CompressorGain(env) * mDryWet;
+#ifdef DEBUG_COMPRESSOR2_ENV
+   if(wp < 100)
+      mPipeline[0][0][wp] = 0;
+   else
+      mPipeline[0][0][wp] = env;
+#else
    mPipeline[0][0][wp] = mPipeline[0][0][wp] * gain;
+#endif
    if(mProcStereo)
       mPipeline[0][1][wp] = mPipeline[0][1][wp] * gain;
 }
@@ -1099,13 +1230,37 @@ bool EffectCompressor2::PipelineHasData()
 
 void EffectCompressor2::DrainPipeline()
 {
+#ifdef DEBUG_COMPRESSOR2_TRACE
+   std::cerr << "DrainBlock: " <<
+      !!mPipeline[0].size << !!mPipeline[1].size <<
+      !!mPipeline[2].size << !!mPipeline[3].size <<
+      "\n" << std::flush;
+   bool once = false;
+#endif
+
    float env;
    size_t length = mPipeline[0].size;
    size_t length2 = mPipeline[PIPELINE_DEPTH-2].size;
+
+#ifdef DEBUG_COMPRESSOR2_TRACE
+   std::cerr << "LookaheadLen: " << mLookaheadLength << "\n" << std::flush;
+   std::cerr << "PipeLength: " <<
+      mPipeline[0].size << " " << mPipeline[1].size << " " <<
+      mPipeline[2].size << " " << mPipeline[3].size <<
+      "\n" << std::flush;
+#endif
+
    for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp)
    {
       if(rp < length2 && mPipeline[PIPELINE_DEPTH-2].size != 0)
       {
+#ifdef DEBUG_COMPRESSOR2_TRACE
+         if(!once)
+         {
+            once = true;
+            std::cerr << "Draining overlapping buffer\n" << std::flush;
+         }
+#endif
          env = EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp);
       }
       else
@@ -1117,6 +1272,11 @@ void EffectCompressor2::DrainPipeline()
 
 void EffectCompressor2::StorePipeline(TrackIterRange<WaveTrack> range)
 {
+#ifdef DEBUG_COMPRESSOR2_TRACE
+   std::cerr << "StoreBlock at: " << mPipeline[0].trackPos.as_size_t() <<
+      " with len: " << mPipeline[0].trackSize << "\n" << std::flush;
+#endif
+
    int idx = 0;
    for(auto channel : range)
    {
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index 57c69b766..f1b26e13d 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -74,6 +74,7 @@ class EnvelopeDetector
 
       float ProcessSample(float value);
       size_t GetBlockSize() const;
+      const float* GetBuffer(int idx) const;
    protected:
       size_t mPos;
       std::vector<float> mLookaheadBuffer;

From 277f64c4ca041fa01dec04412ed80c34f1fcf495 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sun, 8 Mar 2020 10:00:59 +0100
Subject: [PATCH 12/33] Imprement progress dialog.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 5c70ede44..c389f0080 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -995,7 +995,6 @@ void EffectCompressor2::SwapPipeline()
    std::cerr << "\n" << std::flush;
 #endif
 
-   ++mProgressVal;
    for(size_t i = 0; i < PIPELINE_DEPTH-1; ++i)
       mPipeline[i].swap(mPipeline[i+1]);
 #ifdef DEBUG_COMPRESSOR2_TRACE
@@ -1063,6 +1062,9 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range)
 
       // Increment s one blockfull of samples
       pos += blockLen;
+
+      if(!UpdateProgress())
+          return false;
    }
 
    // Handle short selections
@@ -1079,6 +1081,8 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range)
 #endif
       SwapPipeline();
       FillPipeline();
+      if(!UpdateProgress())
+          return false;
    }
 
    while(PipelineHasData())
@@ -1086,6 +1090,8 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range)
       StorePipeline(range);
       SwapPipeline();
       DrainPipeline();
+      if(!UpdateProgress())
+          return false;
    }
 #ifdef DEBUG_COMPRESSOR2_TRACE
    std::cerr << "StoreLastBlock\n" << std::flush;
@@ -1289,6 +1295,14 @@ void EffectCompressor2::StorePipeline(TrackIterRange<WaveTrack> range)
    mPipeline[0].size = 0;
 }
 
+bool EffectCompressor2::UpdateProgress()
+{
+   mProgressVal +=
+      (double(1+mProcStereo) * mPipeline[PIPELINE_DEPTH-1].trackSize)
+      / (double(GetNumWaveTracks()) * mTrackLen);
+   return !TotalProgress(mProgressVal);
+}
+
 void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt))
 {
    if(!mIgnoreGuiEvents)

From ae4189a5332b6da93c45a75edc713e7a9e5446f9 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Thu, 28 May 2020 09:33:04 +0200
Subject: [PATCH 13/33] Implement initial condition optimization.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 87 ++++++++++++++++++++++++++++++++-----
 src/effects/Compressor2.h   | 12 +++++
 2 files changed, 88 insertions(+), 11 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index c389f0080..f4916d364 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -125,6 +125,14 @@ float SlidingRmsPreprocessor::ProcessSample(float valueL, float valueR)
    return DoProcessSample((valueL * valueL + valueR * valueR) / 2.0);
 }
 
+void SlidingRmsPreprocessor::Reset()
+{
+   mSum = 0;
+   mPos = 0;
+   mInsertCount = 0;
+   std::fill(mWindow.begin(), mWindow.end(), 0);
+}
+
 float SlidingRmsPreprocessor::DoProcessSample(float value)
 {
    if(mInsertCount > REFRESH_WINDOW_EVERY)
@@ -183,6 +191,13 @@ float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR)
    return DoProcessSample((fabs(valueL) + fabs(valueR)) / 2.0);
 }
 
+void SlidingMaxPreprocessor::Reset()
+{
+   mPos = 0;
+   std::fill(mWindow.begin(), mWindow.end(), 0);
+   std::fill(mMaxes.begin(), mMaxes.end(), 0);
+}
+
 float SlidingMaxPreprocessor::DoProcessSample(float value)
 {
    size_t oldHead     = (mPos-1) % mWindow.size();
@@ -207,6 +222,8 @@ float SlidingMaxPreprocessor::DoProcessSample(float value)
 
 EnvelopeDetector::EnvelopeDetector(size_t buffer_size)
    : mPos(0),
+   mInitialCondition(0),
+   mInitialBlockSize(0),
    mLookaheadBuffer(buffer_size, 0),
    mProcessingBuffer(buffer_size, 0),
    mProcessedBuffer(buffer_size, 0)
@@ -227,6 +244,10 @@ float EnvelopeDetector::ProcessSample(float value)
    return retval;
 }
 
+void EnvelopeDetector::CalcInitialCondition(float value)
+{
+}
+
 size_t EnvelopeDetector::GetBlockSize() const
 {
    wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
@@ -350,6 +371,28 @@ Pt1EnvelopeDetector::Pt1EnvelopeDetector(
 
    mAttackFactor = 1.0 / (attackTime * rate);
    mReleaseFactor  = 1.0 / (releaseTime  * rate);
+   mInitialBlockSize = std::min(size_t(rate * sqrt(attackTime)), bufferSize);
+}
+
+void Pt1EnvelopeDetector::CalcInitialCondition(float value)
+{
+   mLookaheadBuffer[mPos++] = value;
+   if(mPos == mInitialBlockSize)
+   {
+      float level = 0;
+      for(size_t i = 0; i < mPos; ++i)
+      {
+         if(mLookaheadBuffer[i] >= level)
+            if(i < mInitialBlockSize / 5)
+               level += 5 * mAttackFactor * (mLookaheadBuffer[i] - level);
+            else
+               level += mAttackFactor * (mLookaheadBuffer[i] - level);
+         else
+            level += mReleaseFactor * (mLookaheadBuffer[i] - level);
+      }
+      mInitialCondition = level;
+      mPos = 0;
+   }
 }
 
 void Pt1EnvelopeDetector::Follow()
@@ -399,12 +442,16 @@ void PipelineBuffer::init(size_t capacity, bool stereo)
    size = 0;
    mCapacity = capacity;
    mBlockBuffer[0].reinit(capacity);
-   std::fill(mBlockBuffer[0].get(), mBlockBuffer[0].get() + capacity, 0);
    if(stereo)
-   {
       mBlockBuffer[1].reinit(capacity);
-      std::fill(mBlockBuffer[1].get(), mBlockBuffer[1].get() + capacity, 0);
-   }
+   fill(0, stereo);
+}
+
+void PipelineBuffer::fill(float value, bool stereo)
+{
+   std::fill(mBlockBuffer[0].get(), mBlockBuffer[0].get() + mCapacity, value);
+   if(stereo)
+      std::fill(mBlockBuffer[1].get(), mBlockBuffer[1].get() + mCapacity, value);
 }
 
 void PipelineBuffer::free()
@@ -1032,6 +1079,7 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range)
    std::cerr << "LookaheadLen: " << mLookaheadLength << "\n" << std::flush;
 #endif
 
+   bool first = true;
    mProgressVal = 0;
 #ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS
    buf_num = 0;
@@ -1055,6 +1103,21 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range)
       if(!LoadPipeline(range, blockLen))
          return false;
 
+      if(first)
+      {
+         first = false;
+         size_t sampleCount = mEnvelope->InitialConditionSize();
+         for(size_t i = 0; i < sampleCount; ++i)
+         {
+            size_t rp = i % mPipeline[PIPELINE_DEPTH-1].trackSize;
+            mEnvelope->CalcInitialCondition(
+               PreprocSample(mPipeline[PIPELINE_DEPTH-1], rp));
+         }
+         mPipeline[PIPELINE_DEPTH-2].fill(
+            mEnvelope->InitialCondition(), mProcStereo);
+         mPreproc->Reset();
+      }
+
       if(mPipeline[0].size == 0)
          FillPipeline();
       else
@@ -1156,7 +1219,6 @@ void EffectCompressor2::FillPipeline()
    size_t length = mPipeline[PIPELINE_DEPTH-1].size;
    for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp)
    {
-      // TODO: correct initial conditions
       if(rp < length)
          EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp);
       else
@@ -1199,14 +1261,17 @@ void EffectCompressor2::ProcessPipeline()
    }
 }
 
+inline float EffectCompressor2::PreprocSample(PipelineBuffer& pbuf, size_t rp)
+{
+   if(mProcStereo)
+      return mPreproc->ProcessSample(pbuf[0][rp], pbuf[1][rp]);
+   else
+      return mPreproc->ProcessSample(pbuf[0][rp]);
+}
+
 inline float EffectCompressor2::EnvelopeSample(PipelineBuffer& pbuf, size_t rp)
 {
-   float preprocessed;
-   if(mProcStereo)
-      preprocessed = mPreproc->ProcessSample(pbuf[0][rp], pbuf[1][rp]);
-   else
-      preprocessed = mPreproc->ProcessSample(pbuf[0][rp]);
-   return mEnvelope->ProcessSample(preprocessed);
+   return mEnvelope->ProcessSample(PreprocSample(pbuf, rp));
 }
 
 inline void EffectCompressor2::CompressSample(float env, size_t wp)
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index f1b26e13d..9e223075d 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -28,6 +28,7 @@ class SamplePreprocessor
    public:
       virtual float ProcessSample(float value) = 0;
       virtual float ProcessSample(float valueL, float valueR) = 0;
+      virtual void Reset() = 0;
 };
 
 class SlidingRmsPreprocessor : public SamplePreprocessor
@@ -37,6 +38,7 @@ class SlidingRmsPreprocessor : public SamplePreprocessor
 
       virtual float ProcessSample(float value);
       virtual float ProcessSample(float valueL, float valueR);
+      virtual void Reset();
 
       static const size_t REFRESH_WINDOW_EVERY = 1048576; // 1 MB
 
@@ -58,6 +60,7 @@ class SlidingMaxPreprocessor : public SamplePreprocessor
 
       virtual float ProcessSample(float value);
       virtual float ProcessSample(float valueL, float valueR);
+      virtual void Reset();
 
    private:
       std::vector<float> mWindow;
@@ -75,8 +78,14 @@ class EnvelopeDetector
       float ProcessSample(float value);
       size_t GetBlockSize() const;
       const float* GetBuffer(int idx) const;
+
+      virtual void CalcInitialCondition(float value);
+      inline float InitialCondition() const { return mInitialCondition; }
+      inline size_t InitialConditionSize() const { return mInitialBlockSize; }
    protected:
       size_t mPos;
+      float mInitialCondition;
+      size_t mInitialBlockSize;
       std::vector<float> mLookaheadBuffer;
       std::vector<float> mProcessingBuffer;
       std::vector<float> mProcessedBuffer;
@@ -102,6 +111,7 @@ class Pt1EnvelopeDetector : public EnvelopeDetector
    public:
       Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime,
          size_t buffer_size = 0, bool correctGain = true);
+      virtual void CalcInitialCondition(float value);
 
    private:
       double mGainCorrection;
@@ -124,6 +134,7 @@ struct PipelineBuffer
       void pad_to(size_t len, float value, bool stereo);
       void swap(PipelineBuffer& other);
       void init(size_t size, bool stereo);
+      void fill(float value, bool stereo);
       inline size_t capacity() const { return mCapacity; }
       void free();
 
@@ -182,6 +193,7 @@ private:
    bool LoadPipeline(TrackIterRange<WaveTrack> range, size_t len);
    void FillPipeline();
    void ProcessPipeline();
+   inline float PreprocSample(PipelineBuffer& pbuf, size_t rp);
    inline float EnvelopeSample(PipelineBuffer& pbuf, size_t rp);
    inline void CompressSample(float env, size_t wp);
    bool PipelineHasData();

From 1d728bec89a4eda74ff2537a56368fb29df730f9 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Fri, 12 Jun 2020 13:47:02 +0200
Subject: [PATCH 14/33] Improve octave test framework and refactor loudness
 test.

Add helper functions "import_from_aud" and "export_to_aud" to
de-deplicate common code in tests.
The export functions reads back the exported signal to remove the impact
of quantization errors during wav export from test results.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 tests/octave/.gitignore      |  1 +
 tests/octave/loudness_test.m | 95 +++++++++---------------------------
 tests/octave/run_test.m      | 38 ++++++++++++---
 3 files changed, 57 insertions(+), 77 deletions(-)
 create mode 100644 tests/octave/.gitignore

diff --git a/tests/octave/.gitignore b/tests/octave/.gitignore
new file mode 100644
index 000000000..d8dd7532a
--- /dev/null
+++ b/tests/octave/.gitignore
@@ -0,0 +1 @@
+*.wav
diff --git a/tests/octave/loudness_test.m b/tests/octave/loudness_test.m
index 43a9ea74d..ac53616c4 100644
--- a/tests/octave/loudness_test.m
+++ b/tests/octave/loudness_test.m
@@ -125,20 +125,12 @@ end
 ## Test Loudness LUFS mode: block to short and all silent
 CURRENT_TEST = "Loudness LUFS mode, short silent block";
 fs= 44100;
-x = zeros(ceil(fs*0.35), 2);
-audiowrite(TMP_FILENAME, x, fs);
-if EXPORT_TEST_SIGNALS
-  audiowrite(cstrcat(pwd(), "/Loudness-LUFS-silence-test.wav"), x, fs);
-end
+x1 = zeros(ceil(fs*0.35), 2);
 
 remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs, "Loudness-LUFS-silence-test.wav");
 aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=1 NormalizeTo=0 StereoIndependent=0\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(2);
 do_test_equ(y, x, "identity");
 
 ## Test Loudness LUFS mode: stereo dependent
@@ -146,76 +138,50 @@ CURRENT_TEST = "Loudness LUFS mode, keep DC and stereo balance";
 randn("seed", 1);
 # Include some silence in the test signal to test loudness gating
 # and vary the overall loudness over time.
-x = [0.1*randn(15*fs, 2).', zeros(5*fs, 2).', 0.1*randn(15*fs, 2).'].';
-x(:,1) = x(:,1) .* sin(2*pi/fs/35*(1:1:35*fs)).' .* 1.2;
-x(:,2) = x(:,2) .* sin(2*pi/fs/35*(1:1:35*fs)).';
-audiowrite(TMP_FILENAME, x, fs);
-if EXPORT_TEST_SIGNALS
-  audiowrite(cstrcat(pwd(), "/Loudness-LUFS-stereo-test.wav"), x, fs);
-end
+x1 = [0.1*randn(15*fs, 2).', zeros(5*fs, 2).', 0.1*randn(15*fs, 2).'].';
+x1(:,1) = x1(:,1) .* sin(2*pi/fs/35*(1:1:35*fs)).' .* 1.2;
+x1(:,2) = x1(:,2) .* sin(2*pi/fs/35*(1:1:35*fs)).';
 
 remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs, "Loudness-LUFS-stereo-test.wav");
 aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=1 NormalizeTo=0 StereoIndependent=0\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(2);
 do_test_equ(calc_LUFS(y, fs), -23, "loudness", LUFS_epsilon);
 do_test_neq(calc_LUFS(y(:,1), fs), calc_LUFS(y(:,2), fs), "stereo balance", 1);
 
 ## Test Loudness LUFS mode, stereo independent
 CURRENT_TEST = "Loudness LUFS mode, stereo independence";
-audiowrite(TMP_FILENAME, x, fs);
 remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs);
 aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=0 NormalizeTo=0 StereoIndependent=1\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(2);
 # Independently processed stereo channels have half the target loudness.
 do_test_equ(calc_LUFS(y(:,1), fs), -26, "channel 1 loudness", LUFS_epsilon);
 do_test_equ(calc_LUFS(y(:,2), fs), -26, "channel 2 loudness", LUFS_epsilon);
 
 ## Test Loudness LUFS mode: mono as mono
 CURRENT_TEST = "Test Loudness LUFS mode: mono as mono";
-x = x(:,1);
-audiowrite(TMP_FILENAME, x, fs);
-if EXPORT_TEST_SIGNALS
-  audiowrite(cstrcat(pwd(), "/Loudness-LUFS-mono-test.wav"), x, fs);
-end
+x1 = x1(:,1);
 
 remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs, "Loudness-LUFS-mono-test.wav");
 aud_do("LoudnessNormalization: LUFSLevel=-26 DualMono=0 NormalizeTo=0 StereoIndependent=1\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=1\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(1);
 do_test_equ(calc_LUFS(y, fs), -26, "loudness", LUFS_epsilon);
 
 ## Test Loudness LUFS mode: mono as dual-mono
 CURRENT_TEST = "Test Loudness LUFS mode: mono as dual-mono";
-audiowrite(TMP_FILENAME, x, fs);
-
 remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs);
 aud_do("LoudnessNormalization: LUFSLevel=-26 DualMono=1 NormalizeTo=0 StereoIndependent=0\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=1\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(1);
 # This shall be 3 LU quieter as it is compared to strict spec.
 do_test_equ(calc_LUFS(y, fs), -29, "loudness", LUFS_epsilon);
 
 ## Test Loudness LUFS mode: multi-rate project
 CURRENT_TEST = "Test Loudness LUFS mode: multi-rate project";
-audiowrite(TMP_FILENAME, x, fs);
+audiowrite(TMP_FILENAME, x1, fs);
+x = audioread(TMP_FILENAME);
 
 remove_all_tracks();
 aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
@@ -228,6 +194,7 @@ audiowrite(TMP_FILENAME, x1, fs1);
 if EXPORT_TEST_SIGNALS
   audiowrite(cstrcat(pwd(), "/Loudness-LUFS-stereo-test-8kHz.wav"), x1, fs1);
 end
+x1 = audioread(TMP_FILENAME);
 
 aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
 select_tracks(0, 100);
@@ -255,36 +222,22 @@ do_test_neq(calc_LUFS(y1(:,1), fs), calc_LUFS(y1(:,2), fs), "stereo balance trac
 CURRENT_TEST = "Loudness RMS mode, stereo independent";
 randn("seed", 1);
 fs= 44100;
-x = 0.1*randn(30*fs, 2);
-x(:,1) = x(:,1) * 0.6;
-audiowrite(TMP_FILENAME, x, fs);
-if EXPORT_TEST_SIGNALS
-  audiowrite(cstrcat(pwd(), "/Loudness-RMS-test.wav"), x, fs);
-end
+x1 = 0.1*randn(30*fs, 2);
+x1(:,1) = x1(:,1) * 0.6;
 
 remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs, "Loudness-RMS-test.wav");
 aud_do("LoudnessNormalization: RMSLevel=-20 DualMono=0 NormalizeTo=1 StereoIndependent=1\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(2);
 do_test_equ(20*log10(sqrt(sum(y(:,1).*y(:,1)/length(y)))), -20, "channel 1 RMS");
 do_test_equ(20*log10(sqrt(sum(y(:,2).*y(:,2)/length(y)))), -20, "channel 2 RMS");
 
 ## Test Loudness RMS mode: stereo dependent
 CURRENT_TEST = "Loudness RMS mode, stereo dependent";
-audiowrite(TMP_FILENAME, x, fs);
-
 remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs);
 aud_do("LoudnessNormalization: RMSLevel=-22 DualMono=1 NormalizeTo=1 StereoIndependent=0\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(2);
 # Stereo RMS must be calculated in quadratic domain.
 do_test_equ(20*log10(sqrt(sum(rms(y).^2)/size(y)(2))), -22, "RMS");
 do_test_neq(20*log10(rms(y(:,1))), 20*log10(rms(y(:,2))), "stereo balance", 1);
diff --git a/tests/octave/run_test.m b/tests/octave/run_test.m
index d14810334..3496ddd7f 100755
--- a/tests/octave/run_test.m
+++ b/tests/octave/run_test.m
@@ -29,10 +29,13 @@ if nargin == 2
 end
 
 ## Initialization and helper functions
+global TMP_FILENAME;
+global EXPORT_TEST_SIGNALS;
 UID=num2str(getuid());
 PIPE_TO_PATH=strcat("/tmp/audacity_script_pipe.to.", UID);
 PIPE_FROM_PATH=strcat("/tmp/audacity_script_pipe.from.", UID);
 TMP_FILENAME=strcat(pwd(), "/tmp.wav");
+EXPORT_TEST_SIGNALS = false;
 
 printf("Open scripting pipes, this may freeze if Audacity does not run...\n");
 
@@ -74,6 +77,27 @@ function select_tracks(num, count)
   aud_do(sprintf("SelectTracks: Track=%d TrackCount=%d Mode=Set\n", num, count));
 end
 
+function x_in = import_from_aud(channels)
+  global TMP_FILENAME;
+  aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=", ...
+    num2str(channels), "\n"));
+  system("sync");
+  x_in = audioread(TMP_FILENAME);
+end
+
+function x_out = export_to_aud(x, fs, name = "")
+  global TMP_FILENAME;
+  global EXPORT_TEST_SIGNALS;
+  audiowrite(TMP_FILENAME, x, fs);
+  if EXPORT_TEST_SIGNALS && length(name) != 0
+    audiowrite(cstrcat(pwd(), "/", name), x, fs);
+  end
+  # Read it back to avoid quantization-noise in tests
+  x_out = audioread(TMP_FILENAME);
+  aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
+  select_tracks(0, 100);
+end
+
 ## Float equal comparison helper
 function [ret] = float_eq(x, y, eps=0.001)
   ret = abs(x - y) < eps;
@@ -99,41 +123,43 @@ function plot_failure(x, y)
   plot(x, 'r')
   hold on
   plot(y, 'b')
-  plot(log10(abs(x-y)), 'g')
+  delta = abs(x-y);
+  max(delta)
+  plot(log10(delta), 'g')
   hold off
   legend("Audacity", "Octave", "log-delta", "location", "southeast")
   input("Press enter to continue", "s")
 end
 
-function do_test_equ(x, y, msg, eps=0.001, skip = false)
+function do_test_equ(x, y, msg = "", eps = 0.001, skip = false)
   cmp = all(all(float_eq(x, y, eps)));
   if do_test(cmp, msg, skip) == 0
     plot_failure(x, y);
   end
 end
 
-function do_test_neq(x, y, msg, eps=0.001, skip = false)
+function do_test_neq(x, y, msg = "", eps = 0.001, skip = false)
   cmp = all(all(!float_eq(x, y, eps)));
   if do_test(cmp, msg, skip) == 0
     plot_failure(x, y);
   end
 end
 
-function do_test_gte(x, y, msg, skip = false)
+function do_test_gte(x, y, msg = "", skip = false)
   cmp = all(all(x >= y));
   if do_test(cmp, msg, skip) == 0
     plot_failure(x, y);
   end
 end
 
-function do_test_lte(x, y, msg, skip = false)
+function do_test_lte(x, y, msg = "", skip = false)
   cmp = all(all(x <= y));
   if do_test(cmp, msg, skip) == 0
     plot_failure(x, y);
   end
 end
 
-function result = do_test(result, msg, skip = false)
+function result = do_test(result, msg = "", skip = false)
   global TESTS_RUN;
   global TESTS_FAILED;
   global TESTS_SKIPPED;

From 2a6c2aaf806b01a4df27a4ea86fcf476176964cc Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sun, 31 May 2020 10:49:08 +0200
Subject: [PATCH 15/33] Add Compressor 2 octave tests.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 tests/octave/compressor2_test.m | 306 ++++++++++++++++++++++++++++++++
 1 file changed, 306 insertions(+)
 create mode 100644 tests/octave/compressor2_test.m

diff --git a/tests/octave/compressor2_test.m b/tests/octave/compressor2_test.m
new file mode 100644
index 000000000..b24c83a82
--- /dev/null
+++ b/tests/octave/compressor2_test.m
@@ -0,0 +1,306 @@
+## Audacity Compressor2 effect unit test
+#
+# Max Maisel
+#
+# This tests the Compressor effect with various pseudo-random mono and stereo
+# noise sequences and sinewaves. The test sequences have different amplitudes
+# per channel and sometimes a DC component.
+#
+# Avoid large parameters for AttackTime, ReleaseTime and LookaroundTime in
+# this script as settling behaviour is different and will cause test failure.
+#
+
+pkg load signal;
+pkg load image;
+
+printf("Running Compressor effect tests.\n");
+
+EXPORT_TEST_SIGNALS = true;
+
+## PT1 envelope helper function for symmetric attack and release times.
+function y = env_PT1(x, fs, t_ar, gain = 0)
+  T = 1/(t_ar*fs);
+  si = mean(mean(abs(x(1:fs*t_ar/20,:))));
+  c = (gain != 0) * gain + (gain == 0) * (1.0 + exp(t_ar/30.0));
+  y = c*filter(T, [1 T-1], mean(abs(x), 2), si*c);
+end
+
+## PT1 envelope helper function for asymmetric attack and release times.
+# This function is much slower than the symmetric counterpart.
+function y = env_PT1_asym(x, fs, t_a, t_r, gain = 0)
+  C_a = 1.0 / (fs*t_a);
+  C_r = 1.0 / (fs*t_r);
+  si = mean(mean(abs(x(1:fs*t_a/20,:))));
+  c = (gain != 0) * gain + (gain == 0) * (1.0 + exp(t_a/30.0));
+
+  x_m = mean(abs(x), 2);
+  y = zeros(length(x_m), 1);
+  level = si;
+
+  for k = 1:1:length(x_m)
+    if x_m(k) >= level
+      level = level + C_a * (x_m(k) - level);
+    else
+      level = level + C_r * (x_m(k) - level);
+    end
+    y(k) = c * level;
+  end
+end
+
+## Compressor gain helper function
+function gain = comp_gain(env, thresh_DB, ratio, kneeW_DB, makeup)
+  makeupG_DB  = -thresh_DB * (1-1/ratio) * makeup / 100;
+  env_DB      = 20*log10(env);
+  kneeCond_DB = 2*(env_DB-thresh_DB);
+
+  belowKnee  = kneeCond_DB < -kneeW_DB;
+  aboveKnee  = kneeCond_DB >= kneeW_DB;
+  # & is element-wise &&
+  withinKnee = (kneeCond_DB >= -kneeW_DB) & (kneeCond_DB < kneeW_DB);
+
+  gain_DB = zeros(size(env));
+  gain_DB(belowKnee) = makeupG_DB;
+  gain_DB(aboveKnee) = thresh_DB + ...
+    (env_DB(aboveKnee) - thresh_DB) / ratio + ...
+    makeupG_DB - env_DB(aboveKnee);
+  # Prevent division by zero
+  kneeW_DB(kneeW_DB==0) = 0.000001;
+  gain_DB(withinKnee) = (1/ratio-1) * ...
+    (env_DB(withinKnee) - thresh_DB + kneeW_DB/2).^2 / ...
+    (2*kneeW_DB) + makeupG_DB;
+
+  gain = 10.^(gain_DB/20);
+end
+
+# Ignore first samples due to settling effects helper function
+function y = settled(x, fs = 44100, tau = 1, both = 0)
+  y = x(round(3*fs*tau):length(x)-round(3*fs*tau*both),:);
+end
+
+# XXX: This Octave function is REALLY slow.
+#      Maximum value of n*fs < 10000
+function y = lookaround_RMS(x, fs, n1, n2)
+  kernel = cat(1, zeros(n2*fs,1), ones(n1*fs, 1), ones(n2*fs, 1), zeros(n1*fs, 1));
+  y = zeros(size(x));
+  for i=1:1:size(x)(2)
+    y(:,i) = conv(x(:,i).^2, kernel, 'same')./(n1+n2)/fs;
+  end
+
+  y = 2*sqrt(sum(y, 2)./size(y)(2));
+end
+
+# XXX: This Octave function is REALLY slow.
+#      Maximum value of n*fs < 10000
+function y = lookaround_max(x, fs, n1, n2)
+  kernel = cat(1, zeros(n2*fs,1), ones(n1*fs, 1), ones(n2*fs, 1), zeros(n1*fs, 1));
+  x = mean(abs(x), 2);
+  y = imdilate(x, kernel);
+end
+
+################################################################################
+
+## Test Compressor, mono thresholding
+CURRENT_TEST = "Compressor2, mono thresholding";
+fs = 44100;
+
+randn("seed", 1);
+x1 = 0.01*randn(30*fs,1) .* sin(2*pi/fs/35*(1:1:30*fs)).';
+
+remove_all_tracks();
+x = export_to_aud(x1, fs, name = "Compressor-threshold-test.wav");
+aud_do("CompressorV2: Threshold=-20 Algorithm=0 AttackTime=0.1 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
+y = import_from_aud(1);
+
+# All input samples are below threshold so output must be equal to input.
+do_test_equ(x, y);
+
+## Test Compressor, mono compression PT1 - no lookaround
+CURRENT_TEST = "Compressor2, mono compression PT1";
+x1 = x1.*10;
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2.5 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0.0 LookbehindTime=0.0 KneeWidth=12\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 1), ...
+  comp_gain(settled(env_PT1(x, fs, 0.5, 1), fs, 1), -20, 2.5, 12, 0).* ...
+  settled(x, fs, 1));
+
+## Test Compressor, mono compression PT1 - sinewave - no lookaround
+CURRENT_TEST = "Compressor2, mono compression PT1 - sinewave";
+
+x2 = sin(2*pi*300/fs*(1:1:20*fs)).';
+remove_all_tracks();
+x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
+aud_do("CompressorV2: Threshold=-23 Algorithm=1 CompressBy=1 Ratio=2.5 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=12\n");
+y = import_from_aud(1);
+
+# Gain factor 2 because we compress by RMS but do not use lookaround_RMS as
+# lookaround is zero.
+do_test_equ(settled(y, fs, 1), ...
+  comp_gain(settled(2*env_PT1(x, fs, 0.5), fs, 1), -23, 2.5, 12, 0).* ...
+  settled(x, fs, 1));
+
+## Test Compressor, mono compression PT1 - faded sinewave - medium signal
+CURRENT_TEST = "Compressor2, mono compression PT1 - faded sinewave - medium signal";
+
+x2 = sin(2*pi*300/fs*(1:1:50*fs)).' .* horzcat(1:1:25*fs, 25*fs:-1:1).' ./ (25*fs);
+remove_all_tracks();
+x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
+aud_do("CompressorV2: Threshold=-10 Algorithm=1 CompressBy=0 Ratio=100 AttackTime=0.01 ReleaseTime=0.01 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 1), ...
+  comp_gain(settled(env_PT1(x, fs, 0.01, 1), fs, 1), -10, 100, 0, 0).* ...
+  settled(x, fs, 1));
+
+## Test Compressor, mono compression PT1 - faded sinewave - 50 sec signal - no lookaround
+CURRENT_TEST = "Compressor2, mono compression PT1 - faded sinewave - long signal";
+
+x2 = vertcat(x2, x2);
+remove_all_tracks();
+x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
+aud_do("CompressorV2: Threshold=-10 Algorithm=1 CompressBy=0 Ratio=100 AttackTime=0.01 ReleaseTime=0.01 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 1), ...
+  comp_gain(settled(env_PT1(x, fs, 0.01, 1), fs, 1), -10, 100, 0, 0).* ...
+  settled(x, fs, 1));
+
+## Test Compressor, mono compression PT1 - sinewave - no lookaround - long attack time
+CURRENT_TEST = "Compressor2, mono compression PT1 - sinewave - asymetric attack / release";
+
+x2 = sin(2*pi*300/fs*(1:1:20*fs)).';
+remove_all_tracks();
+x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
+aud_do("CompressorV2: Threshold=-6 Algorithm=1 CompressBy=0 Ratio=2.0 AttackTime=1.0 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0 MakeupGain=0\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 1), ...
+  comp_gain(settled(env_PT1_asym(x, fs, 1.0, 0.3, 1.0), fs, 1), -6, 2.0, 0, 0).*
+  settled(x, fs, 1));
+
+## Test Compressor, mono lookaround max
+CURRENT_TEST = "Compressor2, mono asymmetric lookaround max";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("CompressorV2: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.1 KneeWidth=5 MakeupGain=50\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 0.6), ...
+  comp_gain(settled(env_PT1(lookaround_max(x, fs, 0.2, 0.1), fs, 0.3, 1), fs, 0.6), ...
+  -17, 1.2, 5, 50).*settled(x, fs, 0.6));
+
+## Test Compressor, mono lookaround RMS
+CURRENT_TEST = "Compressor2, mono asymmetric lookaround RMS";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.2 KneeWidth=3 MakeupGain=80\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 2), ...
+  comp_gain(settled(env_PT1(lookaround_RMS(x, fs, 0.1, 0.2), fs, 1), fs, 2), -20, 3, 3, 80) ...
+  .*settled(x, fs, 2));
+
+## Test Compressor, mono lookaround max with selection
+CURRENT_TEST = "Compressor2, mono lookaround max with selection";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+
+aud_do("Select: Start=2 End=5 Mode=Set\n");
+aud_do("CompressorV2: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\n");
+y = import_from_aud(1);
+x = x(2*fs+1:5*fs);
+
+do_test_equ(settled(y, fs, 0.1), ...
+  comp_gain(settled(env_PT1(lookaround_max(x, fs, 0.2, 0.2), fs, 0.3, 1), fs, 0.1), ...
+  -17, 1.2, 5, 50).*settled(x, fs, 0.1));
+
+## Test Compressor, mono, ultra short attack time
+CURRENT_TEST = "Compressor2, mono, ultra short attack time";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.00001 ReleaseTime=0.00001 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
+y = import_from_aud(2);
+
+# XXX: use larger epsilon due to numerical issues
+# (float in audacity vs double in octave vs wav files for exchange)
+do_test_equ(settled(y, fs, 1), ...
+  comp_gain(settled(env_PT1(x, fs, 0.00001, 1), fs), -20, 2, 10, 0) ...
+  .*settled(x, fs, 1), "", 0.15);
+
+## Test Compressor, stereo compression PT1 - no lookaround
+randn("seed", 2);
+x1 = 0.2*randn(35*fs, 2);
+x1(:,1) = x1(:,1) .* sin(2*pi/fs/35*(1:1:35*fs)).';
+x1(:,2) = x1(:,2) .* (sin(2*pi/fs/75*(1:1:35*fs)).' + 0.1);
+
+CURRENT_TEST = "Compressor2, stereo compression PT1";
+remove_all_tracks();
+x = export_to_aud(x1, fs, "Compressor-stereo-test.wav");
+aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y, fs, 1), ...
+  comp_gain(settled(env_PT1(x, fs, 0.5, 1), fs), -20, 2, 10, 0) ...
+  .*settled(x, fs, 1), "stereo dependent");
+
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("CompressorV2: Threshold=-20 Algorithm=1 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10 StereoIndependent=1\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y(:,1), fs, 1), ...
+  comp_gain(settled(env_PT1(x(:,1), fs, 0.5, 1), fs, 1), -20, 2, 10, 0) ...
+  .*settled(x(:,1), fs, 1), "channel 1");
+do_test_equ(settled(y(:,2), fs, 1), ...
+  comp_gain(settled(env_PT1(x(:,2), fs, 0.5, 1), fs, 1), -20, 2, 10, 0) ...
+  .*settled(x(:,2), fs, 1), "channel 2");
+
+## Test Compressor, stereo compression PT1 - sinewave
+CURRENT_TEST = "Compressor2, stereo compression PT1 - sinewave";
+x2 = sin(2*pi*300/fs*(1:1:20*fs)).';
+x2 = [x2, sin(2*pi*310/fs*(1:1:20*fs)).'];
+
+remove_all_tracks();
+x = export_to_aud(x2, fs, "Compressor-stereo-sine-test.wav");
+aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y, fs, 1), ...
+  comp_gain(settled(env_PT1(x, fs, 0.5, 1), fs, 1), -20, 2, 10, 0) ...
+  .*settled(x, fs, 1), "stereo dependent");
+
+remove_all_tracks();
+x = export_to_aud(x2, fs);
+aud_do("CompressorV2: Threshold=-20 Algorithm=1 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10 StereoIndependent=1\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y(:,1), fs, 1), ...
+  comp_gain(settled(env_PT1(x(:,1), fs, 0.5, 1), fs, 1), -20, 2, 10, 0) ...
+  .*settled(x(:,1), fs, 1), "channel 1");
+do_test_equ(settled(y(:,2), fs, 1), ...
+  comp_gain(settled(env_PT1(x(:,2), fs, 0.5, 1), fs, 1), -20, 2, 10, 0) ...
+  .*settled(x(:,2), fs, 1), "channel 2");
+
+## Test Compressor, stereo lookaround max
+CURRENT_TEST = "Compressor2, stereo lookaround max";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("CompressorV2: Threshold=-17 Algorithm=1 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y, fs, 0.6), ...
+  comp_gain(settled(env_PT1(lookaround_max(x, fs, 0.2, 0.2), fs, 0.3, 1), fs, 0.6), ...
+  -17, 1.2, 5, 50).*settled(x, fs, 0.6));
+
+## Test Compressor, stereo lookaround RMS
+CURRENT_TEST = "Compressor2, stereo lookaround RMS";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("CompressorV2: Threshold=-20 Algorithm=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.1 KneeWidth=3 CompressBy=1 MakeupGain=60\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y, fs, 2.5), ...
+  comp_gain(settled(env_PT1(lookaround_RMS(x, fs, 0.1, 0.1), fs, 1), fs, 2.5), -20, 3, 3, 60) ...
+  .*settled(x, fs, 2.5));

From aa619de49c16eca0a3cd34ea954b4c621599e563 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Tue, 15 Sep 2020 15:23:42 +0200
Subject: [PATCH 16/33] Add realtime support to Compressor2 effect.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 176 ++++++++++++++++++++++++++++++++----
 src/effects/Compressor2.h   |  36 +++++++-
 2 files changed, 190 insertions(+), 22 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index f4916d364..9a2bf5bdf 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -133,6 +133,12 @@ void SlidingRmsPreprocessor::Reset()
    std::fill(mWindow.begin(), mWindow.end(), 0);
 }
 
+void SlidingRmsPreprocessor::SetWindowSize(size_t windowSize)
+{
+   mWindow.resize(windowSize);
+   Reset();
+}
+
 float SlidingRmsPreprocessor::DoProcessSample(float value)
 {
    if(mInsertCount > REFRESH_WINDOW_EVERY)
@@ -198,6 +204,13 @@ void SlidingMaxPreprocessor::Reset()
    std::fill(mMaxes.begin(), mMaxes.end(), 0);
 }
 
+void SlidingMaxPreprocessor::SetWindowSize(size_t windowSize)
+{
+   mWindow.resize(windowSize);
+   mMaxes.resize(windowSize);
+   Reset();
+}
+
 float SlidingMaxPreprocessor::DoProcessSample(float value)
 {
    size_t oldHead     = (mPos-1) % mWindow.size();
@@ -272,8 +285,14 @@ ExpFitEnvelopeDetector::ExpFitEnvelopeDetector(
    float rate, float attackTime, float releaseTime, size_t bufferSize)
    : EnvelopeDetector(bufferSize)
 {
-   mAttackFactor = exp(-1.0 / (rate * attackTime));
-   mReleaseFactor = exp(-1.0 / (rate * releaseTime));
+   SetParams(rate, attackTime, releaseTime);
+}
+
+void ExpFitEnvelopeDetector::SetParams(
+   float sampleRate, float attackTime, float releaseTime)
+{
+   mAttackFactor = exp(-1.0 / (sampleRate * attackTime));
+   mReleaseFactor = exp(-1.0 / (sampleRate * releaseTime));
 }
 
 void ExpFitEnvelopeDetector::Follow()
@@ -361,17 +380,24 @@ void ExpFitEnvelopeDetector::Follow()
 Pt1EnvelopeDetector::Pt1EnvelopeDetector(
    float rate, float attackTime, float releaseTime, size_t bufferSize,
    bool correctGain)
-   : EnvelopeDetector(bufferSize)
+   : EnvelopeDetector(bufferSize),
+   mCorrectGain(correctGain)
+{
+   SetParams(rate, attackTime, releaseTime);
+}
+
+void Pt1EnvelopeDetector::SetParams(
+   float sampleRate, float attackTime, float releaseTime)
 {
    // Approximate peak amplitude correction factor.
-   if(correctGain)
+   if(mCorrectGain)
       mGainCorrection = 1.0 + exp(attackTime / 30.0);
    else
       mGainCorrection = 1.0;
 
-   mAttackFactor = 1.0 / (attackTime * rate);
-   mReleaseFactor  = 1.0 / (releaseTime  * rate);
-   mInitialBlockSize = std::min(size_t(rate * sqrt(attackTime)), bufferSize);
+   mAttackFactor = 1.0 / (attackTime * sampleRate);
+   mReleaseFactor  = 1.0 / (releaseTime  * sampleRate);
+   mInitialBlockSize = std::min(size_t(sampleRate * sqrt(attackTime)), mLookaheadBuffer.size());
 }
 
 void Pt1EnvelopeDetector::CalcInitialCondition(float value)
@@ -461,7 +487,11 @@ void PipelineBuffer::free()
 }
 
 EffectCompressor2::EffectCompressor2()
-   : mIgnoreGuiEvents(false)
+   : mIgnoreGuiEvents(false),
+   mAlgorithmCtrl(0),
+   mPreprocCtrl(0),
+   mAttackTimeCtrl(0),
+   mLookaheadTimeCtrl(0)
 {
    mAlgorithm = DEF_Algorithm;
    mCompressBy = DEF_CompressBy;
@@ -508,6 +538,84 @@ EffectType EffectCompressor2::GetType()
    return EffectTypeProcess;
 }
 
+bool EffectCompressor2::SupportsRealtime()
+{
+#if defined(EXPERIMENTAL_REALTIME_AUDACITY_EFFECTS)
+   return true;
+#else
+   return false;
+#endif
+}
+
+unsigned EffectCompressor2::GetAudioInCount()
+{
+   return 2;
+}
+
+unsigned EffectCompressor2::GetAudioOutCount()
+{
+   return 2;
+}
+
+bool EffectCompressor2::RealtimeInitialize()
+{
+   SetBlockSize(512);
+   AllocRealtimePipeline();
+   mAlgorithmCtrl->Enable(false);
+   mPreprocCtrl->Enable(false);
+   mLookaheadTimeCtrl->Enable(false);
+   if(mAlgorithm == kExpFit)
+      mAttackTimeCtrl->Enable(false);
+   return true;
+}
+
+bool EffectCompressor2::RealtimeAddProcessor(
+   unsigned WXUNUSED(numChannels), float sampleRate)
+{
+   mSampleRate = sampleRate;
+   mProcStereo = true;
+   mPreproc = InitPreprocessor(mSampleRate);
+   mEnvelope = InitEnvelope(mSampleRate, mPipeline[0].size);
+
+   return true;
+}
+
+bool EffectCompressor2::RealtimeFinalize()
+{
+   mPreproc.reset(nullptr);
+   mEnvelope.reset(nullptr);
+   FreePipeline();
+   mAlgorithmCtrl->Enable(true);
+   mPreprocCtrl->Enable(true);
+   mLookaheadTimeCtrl->Enable(true);
+   if(mAlgorithm == kExpFit)
+      mAttackTimeCtrl->Enable(true);
+   return true;
+}
+
+size_t EffectCompressor2::RealtimeProcess(
+   int group, float **inbuf, float **outbuf, size_t numSamples)
+{
+   std::lock_guard<std::mutex> guard(mRealtimeMutex);
+   const size_t j = PIPELINE_DEPTH-1;
+   for(size_t i = 0; i < numSamples; ++i)
+   {
+      if(mPipeline[j].trackSize == mPipeline[j].size)
+      {
+         ProcessPipeline();
+         mPipeline[j].trackSize = 0;
+         SwapPipeline();
+      }
+
+      outbuf[0][i] = mPipeline[j][0][mPipeline[j].trackSize];
+      outbuf[1][i] = mPipeline[j][1][mPipeline[j].trackSize];
+      mPipeline[j][0][mPipeline[j].trackSize] = inbuf[0][i];
+      mPipeline[j][1][mPipeline[j].trackSize] = inbuf[1][i];
+      ++mPipeline[j].trackSize;
+   }
+   return numSamples;
+}
+
 // EffectClientInterface implementation
 bool EffectCompressor2::DefineParams( ShuttleParams & S )
 {
@@ -714,24 +822,22 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
       {
          S.SetStretchyCol(1);
 
-         wxChoice* ctrl = nullptr;
-
-         ctrl = S.Validator<wxGenericValidator>(&mAlgorithm)
+         mAlgorithmCtrl = S.Validator<wxGenericValidator>(&mAlgorithm)
             .AddChoice(XO("Envelope Algorithm:"),
                Msgids(kAlgorithmStrings, nAlgos),
                mAlgorithm);
 
-         wxSize box_size = ctrl->GetMinSize();
+         wxSize box_size = mAlgorithmCtrl->GetMinSize();
          int width = S.GetParent()->GetTextExtent(wxString::Format(
             "%sxxxx",  kAlgorithmStrings[nAlgos-1].Translation())).GetWidth();
          box_size.SetWidth(width);
-         ctrl->SetMinSize(box_size);
+         mAlgorithmCtrl->SetMinSize(box_size);
 
-         ctrl = S.Validator<wxGenericValidator>(&mCompressBy)
+         mPreprocCtrl = S.Validator<wxGenericValidator>(&mCompressBy)
             .AddChoice(XO("Compress based on:"),
                Msgids(kCompressByStrings, nBy),
                mCompressBy);
-         ctrl->SetMinSize(box_size);
+         mPreprocCtrl->SetMinSize(box_size);
 
          S.Validator<wxGenericValidator>(&mStereoInd)
             .AddCheckBox(XO("Compress stereo channels independently"),
@@ -784,12 +890,12 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
 
          S.AddVariableText(XO("Attack Time:"), true,
             wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
-         ctrl = S.Name(XO("Attack Time"))
+         mAttackTimeCtrl = S.Name(XO("Attack Time"))
             .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
             .AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime,
                MIN_AttackTime, ScaleToPrecision(SCL_AttackTime),
                &mAttackTime, SCL_AttackTime / 1000);
-         ctrl->SetMinTextboxWidth(textbox_width);
+         mAttackTimeCtrl->SetMinTextboxWidth(textbox_width);
          S.AddVariableText(XO("s"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
 
@@ -806,12 +912,12 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
 
          S.AddVariableText(XO("Lookahead Time:"), true,
             wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
-         ctrl = S.Name(XO("Lookahead Time"))
+         mLookaheadTimeCtrl = S.Name(XO("Lookahead Time"))
             .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
             .AddSliderTextCtrl({}, DEF_LookaheadTime, MAX_LookaheadTime,
                MIN_LookaheadTime, ScaleToPrecision(SCL_LookaheadTime),
                &mLookaheadTime);
-         ctrl->SetMinTextboxWidth(textbox_width);
+         mLookaheadTimeCtrl->SetMinTextboxWidth(textbox_width);
          S.AddVariableText(XO("s"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
 
@@ -988,6 +1094,24 @@ void EffectCompressor2::AllocPipeline()
       mPipeline[i].init(capacity, stereoTrackFound);
 }
 
+void EffectCompressor2::AllocRealtimePipeline()
+{
+   mLookaheadLength =
+      std::max(0, int(round(mLookaheadTime * mSampleRate)));
+
+   size_t blockSize = std::max(mLookaheadLength, size_t(512));
+   if(mAlgorithm == kExpFit)
+   {
+      size_t riseTime = round(5.0 * (0.1 + mAttackTime)) * mSampleRate;
+      blockSize = std::max(blockSize, riseTime);
+   }
+   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+   {
+      mPipeline[i].init(blockSize, true);
+      mPipeline[i].size = blockSize;
+   }
+}
+
 void EffectCompressor2::FreePipeline()
 {
    for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
@@ -1379,6 +1503,8 @@ void EffectCompressor2::UpdateUI()
 {
    UpdateCompressorPlot();
    UpdateResponsePlot();
+   if(mEnvelope.get() != nullptr)
+      UpdateRealtimeParams();
 }
 
 void EffectCompressor2::UpdateCompressorPlot()
@@ -1432,3 +1558,15 @@ void EffectCompressor2::UpdateResponsePlot()
 
    mResponsePlot->Refresh(false);
 }
+
+void EffectCompressor2::UpdateRealtimeParams()
+{
+   std::lock_guard<std::mutex> guard(mRealtimeMutex);
+   // TODO: extract it
+   size_t window_size =
+      std::max(1, int(round((mLookaheadTime + mLookbehindTime) * mSampleRate)));
+   mLookaheadLength = // TODO: depup this everywhere
+      std::max(0, int(round(mLookaheadTime * mSampleRate)));
+   mPreproc->SetWindowSize(window_size);
+   mEnvelope->SetParams(mSampleRate, mAttackTime, mReleaseTime);
+}
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index 9e223075d..5506e5899 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -22,6 +22,7 @@
 
 class Plot;
 class ShuttleGui;
+class SliderTextCtrl;
 
 class SamplePreprocessor
 {
@@ -29,6 +30,7 @@ class SamplePreprocessor
       virtual float ProcessSample(float value) = 0;
       virtual float ProcessSample(float valueL, float valueR) = 0;
       virtual void Reset() = 0;
+      virtual void SetWindowSize(size_t windowSize) = 0;
 };
 
 class SlidingRmsPreprocessor : public SamplePreprocessor
@@ -39,6 +41,7 @@ class SlidingRmsPreprocessor : public SamplePreprocessor
       virtual float ProcessSample(float value);
       virtual float ProcessSample(float valueL, float valueR);
       virtual void Reset();
+      virtual void SetWindowSize(size_t windowSize);
 
       static const size_t REFRESH_WINDOW_EVERY = 1048576; // 1 MB
 
@@ -61,6 +64,7 @@ class SlidingMaxPreprocessor : public SamplePreprocessor
       virtual float ProcessSample(float value);
       virtual float ProcessSample(float valueL, float valueR);
       virtual void Reset();
+      virtual void SetWindowSize(size_t windowSize);
 
    private:
       std::vector<float> mWindow;
@@ -82,6 +86,10 @@ class EnvelopeDetector
       virtual void CalcInitialCondition(float value);
       inline float InitialCondition() const { return mInitialCondition; }
       inline size_t InitialConditionSize() const { return mInitialBlockSize; }
+
+      virtual void SetParams(float sampleRate, float attackTime,
+         float releaseTime) = 0;
+
    protected:
       size_t mPos;
       float mInitialCondition;
@@ -97,7 +105,10 @@ class ExpFitEnvelopeDetector : public EnvelopeDetector
 {
    public:
       ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime,
-         size_t buffer_size = 0);
+         size_t buffer_size);
+
+      virtual void SetParams(float sampleRate, float attackTime,
+         float releaseTime);
 
    private:
       double mAttackFactor;
@@ -110,10 +121,14 @@ class Pt1EnvelopeDetector : public EnvelopeDetector
 {
    public:
       Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime,
-         size_t buffer_size = 0, bool correctGain = true);
+         size_t buffer_size, bool correctGain = true);
       virtual void CalcInitialCondition(float value);
 
+      virtual void SetParams(float sampleRate, float attackTime,
+         float releaseTime);
+
    private:
+      bool mCorrectGain;
       double mGainCorrection;
       double mAttackFactor;
       double mReleaseFactor;
@@ -160,9 +175,17 @@ public:
    // EffectDefinitionInterface implementation
 
    EffectType GetType() override;
+   bool SupportsRealtime() override;
 
    // EffectClientInterface implementation
 
+   unsigned GetAudioInCount() override;
+   unsigned GetAudioOutCount() override;
+   bool RealtimeInitialize() override;
+   bool RealtimeAddProcessor(unsigned numChannels, float sampleRate) override;
+   bool RealtimeFinalize() override;
+   size_t RealtimeProcess(int group, float **inbuf, float **outbuf,
+      size_t numSamples) override;
    bool DefineParams( ShuttleParams & S ) override;
    bool GetAutomationParameters(CommandParameters & parms) override;
    bool SetAutomationParameters(CommandParameters & parms) override;
@@ -187,6 +210,7 @@ private:
    size_t CalcBufferSize(size_t sampleRate);
 
    void AllocPipeline();
+   void AllocRealtimePipeline();
    void FreePipeline();
    void SwapPipeline();
    bool ProcessOne(TrackIterRange<WaveTrack> range);
@@ -205,6 +229,7 @@ private:
    void UpdateUI();
    void UpdateCompressorPlot();
    void UpdateResponsePlot();
+   void UpdateRealtimeParams();
 
    static const int TAU_FACTOR = 5;
    static const size_t MIN_BUFFER_CAPACITY = 1048576; // 1MB
@@ -218,6 +243,7 @@ private:
    double mTrackLen;
    bool mProcStereo;
 
+   std::mutex mRealtimeMutex;
    std::unique_ptr<SamplePreprocessor> mPreproc;
    std::unique_ptr<EnvelopeDetector> mEnvelope;
 
@@ -246,9 +272,13 @@ private:
    static const size_t RESPONSE_PLOT_STEP_START = 2;
    static const size_t RESPONSE_PLOT_STEP_STOP = 3;
 
+   bool mIgnoreGuiEvents;
    Plot* mGainPlot;
    Plot* mResponsePlot;
-   bool mIgnoreGuiEvents;
+   wxChoice* mAlgorithmCtrl;
+   wxChoice* mPreprocCtrl;
+   SliderTextCtrl* mAttackTimeCtrl;
+   SliderTextCtrl* mLookaheadTimeCtrl;
 
    DECLARE_EVENT_TABLE()
 };

From dfffeb76dc2de79d3f5adb734eb5eced8df3d096 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Wed, 27 May 2020 21:13:55 +0200
Subject: [PATCH 17/33] Add Compressor2 realtime debugging utilities.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 scripts/debug/compressor2_trace.m | 63 ++++++++++++++++++++++++++
 src/effects/Compressor2.cpp       | 73 ++++++++++++++++++++++++++++++-
 src/effects/Compressor2.h         |  5 +++
 3 files changed, 140 insertions(+), 1 deletion(-)
 create mode 100644 scripts/debug/compressor2_trace.m

diff --git a/scripts/debug/compressor2_trace.m b/scripts/debug/compressor2_trace.m
new file mode 100644
index 000000000..fa8284dcf
--- /dev/null
+++ b/scripts/debug/compressor2_trace.m
@@ -0,0 +1,63 @@
+## plot realtime trace data from Compressor2 effect
+
+stereo = true;
+bfile = fopen("/tmp/audio.out");
+
+if stereo
+  width = 15;
+else
+  width = 13;
+end
+
+raw_data = reshape(fread(bfile, 'float'), width, []).';
+
+data = struct;
+data.threshold_DB = raw_data(:,1);
+data.ratio = raw_data(:,2);
+data.kneewidth_DB = raw_data(:,3);
+data.attack_time = raw_data(:,4);
+data.release_time = raw_data(:,5);
+data.lookahead_time = raw_data(:,6);
+data.lookbehind_time = raw_data(:,7);
+data.makeup_gain_pct = raw_data(:,8);
+data.dry_wet_pct = raw_data(:,9);
+
+if stereo
+  data.in = horzcat(raw_data(:,10), raw_data(:,11));
+  data.env = raw_data(:,12);
+  data.gain = raw_data(:,13);
+  data.out = horzcat(raw_data(:,14), raw_data(:,15));
+else
+  data.in = raw_data(:,10);
+  data.env = raw_data(:,11);
+  data.gain = raw_data(:,12);
+  data.out = raw_data(:,13);
+end
+
+figure(1);
+plot(data.in.*100, 'b');
+hold on;
+plot(data.out.*100, 'g');
+plot(data.threshold_DB, 'r');
+plot(data.ratio, 'r');
+plot(data.kneewidth_DB, 'r');
+plot(data.attack_time.*10, 'c', "linewidth", 2);
+plot(data.release_time.*10, 'c', "linewidth", 2);
+plot(data.lookahead_time, 'm');
+plot(data.lookbehind_time, 'm');
+plot(data.makeup_gain_pct, 'r');
+plot(data.dry_wet_pct, 'r');
+plot(data.env.*100, 'k', "linewidth", 2);
+plot(data.gain.*50, 'k', "linestyle", '--');
+hold off;
+grid;
+
+if stereo
+  legend("in*100", "in*100", "out*100", "out*100", "threshold", "ratio", ...
+    "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ...
+    "makeup", "dry/wet", "env*100", "gain*50");
+else
+  legend("in*100", "out*100", "threshold", "ratio", ...
+    "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ...
+    "makeup", "dry/wet", "env*100", "gain*50");
+end
diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 9a2bf5bdf..52ccc5ee7 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -41,10 +41,12 @@
 //#define DEBUG_COMPRESSOR2_DUMP_BUFFERS
 //#define DEBUG_COMPRESSOR2_ENV
 //#define DEBUG_COMPRESSOR2_TRACE
+//#define DEBUG_COMPRESSOR2_TRACE2
 
-#ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS
+#if defined(DEBUG_COMPRESSOR2_DUMP_BUFFERS) or defined(DEBUG_COMPRESSOR2_TRACE2)
 #include <fstream>
 int buf_num;
+std::fstream debugfile;
 #endif
 
 enum kAlgorithms
@@ -243,6 +245,15 @@ EnvelopeDetector::EnvelopeDetector(size_t buffer_size)
 {
 }
 
+float EnvelopeDetector::AttackFactor()
+{
+   return 0;
+}
+float EnvelopeDetector::DecayFactor()
+{
+   return 0;
+}
+
 float EnvelopeDetector::ProcessSample(float value)
 {
    float retval = mProcessedBuffer[mPos];
@@ -386,6 +397,15 @@ Pt1EnvelopeDetector::Pt1EnvelopeDetector(
    SetParams(rate, attackTime, releaseTime);
 }
 
+float Pt1EnvelopeDetector::AttackFactor()
+{
+   return mAttackFactor;
+}
+float Pt1EnvelopeDetector::DecayFactor()
+{
+   return mReleaseFactor;
+}
+
 void Pt1EnvelopeDetector::SetParams(
    float sampleRate, float attackTime, float releaseTime)
 {
@@ -577,6 +597,12 @@ bool EffectCompressor2::RealtimeAddProcessor(
    mPreproc = InitPreprocessor(mSampleRate);
    mEnvelope = InitEnvelope(mSampleRate, mPipeline[0].size);
 
+   mProgressVal = 0;
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+   debugfile.close();
+   debugfile.open("/tmp/audio.out", std::ios::trunc | std::ios::out);
+#endif
+
    return true;
 }
 
@@ -590,6 +616,9 @@ bool EffectCompressor2::RealtimeFinalize()
    mLookaheadTimeCtrl->Enable(true);
    if(mAlgorithm == kExpFit)
       mAttackTimeCtrl->Enable(true);
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+   debugfile.close();
+#endif
    return true;
 }
 
@@ -731,6 +760,11 @@ bool EffectCompressor2::Process()
    AllocPipeline();
    mProgressVal = 0;
 
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+   debugfile.close();
+   debugfile.open("/tmp/audio.out", std::ios::trunc | std::ios::out);
+#endif
+
    for(auto track : mOutputTracks->Selected<WaveTrack>()
       + (mStereoInd ? &Track::Any : &Track::IsLeader))
    {
@@ -769,6 +803,9 @@ bool EffectCompressor2::Process()
    mPreproc.reset(nullptr);
    mEnvelope.reset(nullptr);
    FreePipeline();
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+   debugfile.close();
+#endif
    return bGoodResult;
 }
 
@@ -1401,6 +1438,34 @@ inline float EffectCompressor2::EnvelopeSample(PipelineBuffer& pbuf, size_t rp)
 inline void EffectCompressor2::CompressSample(float env, size_t wp)
 {
    float gain = (1.0 - mDryWet) + CompressorGain(env) * mDryWet;
+
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+   float ThresholdDB = mThresholdDB;
+   float Ratio = mRatio;
+   float KneeWidthDB = mKneeWidthDB;
+   float AttackTime = mAttackTime;
+   float ReleaseTime = mReleaseTime;
+   float LookaheadTime = mLookaheadTime;
+   float LookbehindTime = mLookbehindTime;
+   float MakeupGainPct = mMakeupGainPct;
+   float DryWetPct = mDryWetPct;
+
+   debugfile.write((char*)&ThresholdDB, sizeof(float));
+   debugfile.write((char*)&Ratio, sizeof(float));
+   debugfile.write((char*)&KneeWidthDB, sizeof(float));
+   debugfile.write((char*)&AttackTime, sizeof(float));
+   debugfile.write((char*)&ReleaseTime, sizeof(float));
+   debugfile.write((char*)&LookaheadTime, sizeof(float));
+   debugfile.write((char*)&LookbehindTime, sizeof(float));
+   debugfile.write((char*)&MakeupGainPct, sizeof(float));
+   debugfile.write((char*)&DryWetPct, sizeof(float));
+   debugfile.write((char*)&mPipeline[0][0][wp], sizeof(float));
+   if(mProcStereo)
+      debugfile.write((char*)&mPipeline[0][1][wp], sizeof(float));
+   debugfile.write((char*)&env, sizeof(float));
+   debugfile.write((char*)&gain, sizeof(float));
+#endif
+
 #ifdef DEBUG_COMPRESSOR2_ENV
    if(wp < 100)
       mPipeline[0][0][wp] = 0;
@@ -1411,6 +1476,12 @@ inline void EffectCompressor2::CompressSample(float env, size_t wp)
 #endif
    if(mProcStereo)
       mPipeline[0][1][wp] = mPipeline[0][1][wp] * gain;
+
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+   debugfile.write((char*)&mPipeline[0][0][wp], sizeof(float));
+   if(mProcStereo)
+      debugfile.write((char*)&mPipeline[0][1][wp], sizeof(float));
+#endif
 }
 
 bool EffectCompressor2::PipelineHasData()
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index 5506e5899..097c9bb30 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -90,6 +90,9 @@ class EnvelopeDetector
       virtual void SetParams(float sampleRate, float attackTime,
          float releaseTime) = 0;
 
+      virtual float AttackFactor();
+      virtual float DecayFactor();
+
    protected:
       size_t mPos;
       float mInitialCondition;
@@ -126,6 +129,8 @@ class Pt1EnvelopeDetector : public EnvelopeDetector
 
       virtual void SetParams(float sampleRate, float attackTime,
          float releaseTime);
+      virtual float AttackFactor();
+      virtual float DecayFactor();
 
    private:
       bool mCorrectGain;

From 9606aa73122ad84b719ce5e745dd438741d2655a Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Mon, 14 Sep 2020 13:04:16 +0200
Subject: [PATCH 18/33] Deduplicate Compressor2 effect code.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 34 +++++++++++++++++-----------------
 src/effects/Compressor2.h   |  5 ++++-
 2 files changed, 21 insertions(+), 18 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 52ccc5ee7..0680afd73 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -1068,9 +1068,7 @@ double EffectCompressor2::CompressorGain(double env)
 std::unique_ptr<SamplePreprocessor> EffectCompressor2::InitPreprocessor(
    double rate, bool preview)
 {
-   size_t window_size =
-      std::max(1, int(round((mLookaheadTime + mLookbehindTime) * rate)));
-
+   size_t window_size = CalcWindowLength(rate);
    if(mCompressBy == kAmplitude)
       return std::unique_ptr<SamplePreprocessor>(safenew
          SlidingMaxPreprocessor(window_size));
@@ -1091,12 +1089,10 @@ std::unique_ptr<EnvelopeDetector> EffectCompressor2::InitEnvelope(
             !preview && mCompressBy != kAmplitude));
 }
 
-size_t EffectCompressor2::CalcBufferSize(size_t sampleRate)
+size_t EffectCompressor2::CalcBufferSize(double sampleRate)
 {
    size_t capacity;
-
-   mLookaheadLength =
-      std::max(0, int(round(mLookaheadTime * sampleRate)));
+   mLookaheadLength = CalcLookaheadLength(sampleRate);
    capacity = mLookaheadLength +
       size_t(float(TAU_FACTOR) * (1.0 + mAttackTime) * sampleRate);
    if(capacity < MIN_BUFFER_CAPACITY)
@@ -1104,6 +1100,16 @@ size_t EffectCompressor2::CalcBufferSize(size_t sampleRate)
    return capacity;
 }
 
+size_t EffectCompressor2::CalcLookaheadLength(double rate)
+{
+   return std::max(0, int(round(mLookaheadTime * rate)));
+}
+
+size_t EffectCompressor2::CalcWindowLength(double rate)
+{
+   return std::max(1, int(round((mLookaheadTime + mLookbehindTime) * rate)));
+}
+
 /// Get required buffer size for the largest whole track and allocate buffers.
 /// This reduces the amount of allocations required.
 void EffectCompressor2::AllocPipeline()
@@ -1133,9 +1139,7 @@ void EffectCompressor2::AllocPipeline()
 
 void EffectCompressor2::AllocRealtimePipeline()
 {
-   mLookaheadLength =
-      std::max(0, int(round(mLookaheadTime * mSampleRate)));
-
+   mLookaheadLength = CalcLookaheadLength(mSampleRate);
    size_t blockSize = std::max(mLookaheadLength, size_t(512));
    if(mAlgorithm == kExpFit)
    {
@@ -1606,8 +1610,7 @@ void EffectCompressor2::UpdateResponsePlot()
    std::unique_ptr<EnvelopeDetector> envelope;
    float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME;
 
-   size_t lookahead_size =
-      std::max(0, int(round(mLookaheadTime * plot_rate)));
+   size_t lookahead_size = CalcLookaheadLength(plot_rate);
    ssize_t block_size = float(TAU_FACTOR) * (mAttackTime + 1.0) * plot_rate;
 
    preproc = InitPreprocessor(plot_rate, true);
@@ -1633,11 +1636,8 @@ void EffectCompressor2::UpdateResponsePlot()
 void EffectCompressor2::UpdateRealtimeParams()
 {
    std::lock_guard<std::mutex> guard(mRealtimeMutex);
-   // TODO: extract it
-   size_t window_size =
-      std::max(1, int(round((mLookaheadTime + mLookbehindTime) * mSampleRate)));
-   mLookaheadLength = // TODO: depup this everywhere
-      std::max(0, int(round(mLookaheadTime * mSampleRate)));
+   size_t window_size = CalcWindowLength(mSampleRate);
+   mLookaheadLength = CalcLookaheadLength(mSampleRate);
    mPreproc->SetWindowSize(window_size);
    mEnvelope->SetParams(mSampleRate, mAttackTime, mReleaseTime);
 }
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index 097c9bb30..10cdd8921 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -212,7 +212,10 @@ private:
       double rate, bool preview = false);
    std::unique_ptr<EnvelopeDetector> InitEnvelope(
       double rate, size_t blockSize = 0, bool preview = false);
-   size_t CalcBufferSize(size_t sampleRate);
+   size_t CalcBufferSize(double sampleRate);
+
+   inline size_t CalcLookaheadLength(double rate);
+   inline size_t CalcWindowLength(double rate);
 
    void AllocPipeline();
    void AllocRealtimePipeline();

From 1b76326d0111de255c98dde7e164fc00405e6dd3 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Wed, 16 Sep 2020 09:01:21 +0200
Subject: [PATCH 19/33] Use TranslatableString::Format for Compressor v2 plot
 accessibility.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 0680afd73..8701a7748 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -831,6 +831,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
       mResponsePlot = S.MinSize( { 200, 200 } )
          .AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""),
             Ruler::IntFormat, Ruler::RealFormat, 2);
+      mResponsePlot->SetName(XO("Compressor step response plot"));
 
       plot = mResponsePlot->GetPlotData(0);
       plot->pen = std::unique_ptr<wxPen>(
@@ -1594,9 +1595,8 @@ void EffectCompressor2::UpdateCompressorPlot()
       plot->ydata[i] = plot->xdata[i] +
          LINEAR_TO_DB(CompressorGain(DB_TO_LINEAR(plot->xdata[i])));
 
-// XXX: accessibility but fails with TranslatableString required
-//   mGainPlot->SetName(wxString::Format(
-//      "Compressor gain reduction: %.1f dB", plot->ydata[xsize-1]));
+   mGainPlot->SetName(XO("Compressor gain reduction: %.1f dB").
+      Format(plot->ydata[xsize-1]));
    mGainPlot->Refresh(false);
 }
 

From e725c4825cbbc29cd63884f0292f04d08bbc8238 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Fri, 25 Sep 2020 10:55:33 +0200
Subject: [PATCH 20/33] Rename Compressor v2 to "Dynamic Compressor".

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp     |  4 ++--
 tests/octave/compressor2_test.m | 32 ++++++++++++++++----------------
 2 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 8701a7748..4ea2d295b 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -104,7 +104,7 @@ BEGIN_EVENT_TABLE(EffectCompressor2, wxEvtHandler)
 END_EVENT_TABLE()
 
 const ComponentInterfaceSymbol EffectCompressor2::Symbol
-{ XO("Compressor v2") };
+{ XO("Dynamic Compressor") };
 
 namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; }
 
@@ -548,7 +548,7 @@ TranslatableString EffectCompressor2::GetDescription()
 
 ManualPageID EffectCompressor2::ManualPage()
 {
-   return L"Compressor2";
+   return L"Dynamic_Compressor";
 }
 
 // EffectDefinitionInterface implementation
diff --git a/tests/octave/compressor2_test.m b/tests/octave/compressor2_test.m
index b24c83a82..90e714eb1 100644
--- a/tests/octave/compressor2_test.m
+++ b/tests/octave/compressor2_test.m
@@ -108,7 +108,7 @@ x1 = 0.01*randn(30*fs,1) .* sin(2*pi/fs/35*(1:1:30*fs)).';
 
 remove_all_tracks();
 x = export_to_aud(x1, fs, name = "Compressor-threshold-test.wav");
-aud_do("CompressorV2: Threshold=-20 Algorithm=0 AttackTime=0.1 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=0 AttackTime=0.1 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
 y = import_from_aud(1);
 
 # All input samples are below threshold so output must be equal to input.
@@ -119,7 +119,7 @@ CURRENT_TEST = "Compressor2, mono compression PT1";
 x1 = x1.*10;
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2.5 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0.0 LookbehindTime=0.0 KneeWidth=12\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2.5 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0.0 LookbehindTime=0.0 KneeWidth=12\n");
 y = import_from_aud(1);
 
 do_test_equ(settled(y, fs, 1), ...
@@ -132,7 +132,7 @@ CURRENT_TEST = "Compressor2, mono compression PT1 - sinewave";
 x2 = sin(2*pi*300/fs*(1:1:20*fs)).';
 remove_all_tracks();
 x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
-aud_do("CompressorV2: Threshold=-23 Algorithm=1 CompressBy=1 Ratio=2.5 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=12\n");
+aud_do("DynamicCompressor: Threshold=-23 Algorithm=1 CompressBy=1 Ratio=2.5 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=12\n");
 y = import_from_aud(1);
 
 # Gain factor 2 because we compress by RMS but do not use lookaround_RMS as
@@ -147,7 +147,7 @@ CURRENT_TEST = "Compressor2, mono compression PT1 - faded sinewave - medium sign
 x2 = sin(2*pi*300/fs*(1:1:50*fs)).' .* horzcat(1:1:25*fs, 25*fs:-1:1).' ./ (25*fs);
 remove_all_tracks();
 x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
-aud_do("CompressorV2: Threshold=-10 Algorithm=1 CompressBy=0 Ratio=100 AttackTime=0.01 ReleaseTime=0.01 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
+aud_do("DynamicCompressor: Threshold=-10 Algorithm=1 CompressBy=0 Ratio=100 AttackTime=0.01 ReleaseTime=0.01 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
 y = import_from_aud(1);
 
 do_test_equ(settled(y, fs, 1), ...
@@ -160,7 +160,7 @@ CURRENT_TEST = "Compressor2, mono compression PT1 - faded sinewave - long signal
 x2 = vertcat(x2, x2);
 remove_all_tracks();
 x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
-aud_do("CompressorV2: Threshold=-10 Algorithm=1 CompressBy=0 Ratio=100 AttackTime=0.01 ReleaseTime=0.01 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
+aud_do("DynamicCompressor: Threshold=-10 Algorithm=1 CompressBy=0 Ratio=100 AttackTime=0.01 ReleaseTime=0.01 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
 y = import_from_aud(1);
 
 do_test_equ(settled(y, fs, 1), ...
@@ -173,7 +173,7 @@ CURRENT_TEST = "Compressor2, mono compression PT1 - sinewave - asymetric attack
 x2 = sin(2*pi*300/fs*(1:1:20*fs)).';
 remove_all_tracks();
 x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
-aud_do("CompressorV2: Threshold=-6 Algorithm=1 CompressBy=0 Ratio=2.0 AttackTime=1.0 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0 MakeupGain=0\n");
+aud_do("DynamicCompressor: Threshold=-6 Algorithm=1 CompressBy=0 Ratio=2.0 AttackTime=1.0 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0 MakeupGain=0\n");
 y = import_from_aud(1);
 
 do_test_equ(settled(y, fs, 1), ...
@@ -184,7 +184,7 @@ do_test_equ(settled(y, fs, 1), ...
 CURRENT_TEST = "Compressor2, mono asymmetric lookaround max";
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("CompressorV2: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.1 KneeWidth=5 MakeupGain=50\n");
+aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.1 KneeWidth=5 MakeupGain=50\n");
 y = import_from_aud(1);
 
 do_test_equ(settled(y, fs, 0.6), ...
@@ -195,7 +195,7 @@ do_test_equ(settled(y, fs, 0.6), ...
 CURRENT_TEST = "Compressor2, mono asymmetric lookaround RMS";
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.2 KneeWidth=3 MakeupGain=80\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.2 KneeWidth=3 MakeupGain=80\n");
 y = import_from_aud(1);
 
 do_test_equ(settled(y, fs, 2), ...
@@ -208,7 +208,7 @@ remove_all_tracks();
 x = export_to_aud(x1, fs);
 
 aud_do("Select: Start=2 End=5 Mode=Set\n");
-aud_do("CompressorV2: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\n");
+aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\n");
 y = import_from_aud(1);
 x = x(2*fs+1:5*fs);
 
@@ -220,7 +220,7 @@ do_test_equ(settled(y, fs, 0.1), ...
 CURRENT_TEST = "Compressor2, mono, ultra short attack time";
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.00001 ReleaseTime=0.00001 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.00001 ReleaseTime=0.00001 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
 y = import_from_aud(2);
 
 # XXX: use larger epsilon due to numerical issues
@@ -238,7 +238,7 @@ x1(:,2) = x1(:,2) .* (sin(2*pi/fs/75*(1:1:35*fs)).' + 0.1);
 CURRENT_TEST = "Compressor2, stereo compression PT1";
 remove_all_tracks();
 x = export_to_aud(x1, fs, "Compressor-stereo-test.wav");
-aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
 y = import_from_aud(2);
 
 do_test_equ(settled(y, fs, 1), ...
@@ -247,7 +247,7 @@ do_test_equ(settled(y, fs, 1), ...
 
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("CompressorV2: Threshold=-20 Algorithm=1 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10 StereoIndependent=1\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10 StereoIndependent=1\n");
 y = import_from_aud(2);
 
 do_test_equ(settled(y(:,1), fs, 1), ...
@@ -264,7 +264,7 @@ x2 = [x2, sin(2*pi*310/fs*(1:1:20*fs)).'];
 
 remove_all_tracks();
 x = export_to_aud(x2, fs, "Compressor-stereo-sine-test.wav");
-aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
 y = import_from_aud(2);
 
 do_test_equ(settled(y, fs, 1), ...
@@ -273,7 +273,7 @@ do_test_equ(settled(y, fs, 1), ...
 
 remove_all_tracks();
 x = export_to_aud(x2, fs);
-aud_do("CompressorV2: Threshold=-20 Algorithm=1 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10 StereoIndependent=1\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10 StereoIndependent=1\n");
 y = import_from_aud(2);
 
 do_test_equ(settled(y(:,1), fs, 1), ...
@@ -287,7 +287,7 @@ do_test_equ(settled(y(:,2), fs, 1), ...
 CURRENT_TEST = "Compressor2, stereo lookaround max";
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("CompressorV2: Threshold=-17 Algorithm=1 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\n");
+aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\n");
 y = import_from_aud(2);
 
 do_test_equ(settled(y, fs, 0.6), ...
@@ -298,7 +298,7 @@ do_test_equ(settled(y, fs, 0.6), ...
 CURRENT_TEST = "Compressor2, stereo lookaround RMS";
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("CompressorV2: Threshold=-20 Algorithm=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.1 KneeWidth=3 CompressBy=1 MakeupGain=60\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.1 KneeWidth=3 CompressBy=1 MakeupGain=60\n");
 y = import_from_aud(2);
 
 do_test_equ(settled(y, fs, 2.5), ...

From 510f89567ad4a0ef57ffbc2589957b118a484866 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Fri, 25 Sep 2020 12:41:27 +0200
Subject: [PATCH 21/33] Optimize Compressor dialog layout.

The old layout did not fit well on small screens.
I tried using StartScroller() but this caused problems on large screens.
Also fix clipped text in textboxes on some themes.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 97 +++++++++++++++++++++++++------------
 1 file changed, 65 insertions(+), 32 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 4ea2d295b..8f8ea52d8 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -813,7 +813,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
 {
    S.SetBorder(10);
 
-   S.StartHorizontalLay(wxEXPAND, true);
+   S.StartHorizontalLay(wxEXPAND, 1);
    {
       PlotData* plot;
 
@@ -856,6 +856,11 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
 
    S.StartStatic(XO("Algorithm"));
    {
+      wxSize box_size;
+      int width;
+
+      S.StartHorizontalLay(wxEXPAND, 1);
+      S.StartVerticalLay(1);
       S.StartMultiColumn(2, wxALIGN_LEFT);
       {
          S.SetStretchyCol(1);
@@ -865,33 +870,48 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
                Msgids(kAlgorithmStrings, nAlgos),
                mAlgorithm);
 
-         wxSize box_size = mAlgorithmCtrl->GetMinSize();
-         int width = S.GetParent()->GetTextExtent(wxString::Format(
+         box_size = mAlgorithmCtrl->GetMinSize();
+         width = S.GetParent()->GetTextExtent(wxString::Format(
             "%sxxxx",  kAlgorithmStrings[nAlgos-1].Translation())).GetWidth();
          box_size.SetWidth(width);
          mAlgorithmCtrl->SetMinSize(box_size);
+      }
+      S.EndMultiColumn();
+      S.EndVerticalLay();
+
+      S.AddSpace(15, 0);
+
+      S.StartVerticalLay(1);
+      S.StartMultiColumn(2, wxALIGN_LEFT);
+      {
+         S.SetStretchyCol(1);
 
          mPreprocCtrl = S.Validator<wxGenericValidator>(&mCompressBy)
             .AddChoice(XO("Compress based on:"),
                Msgids(kCompressByStrings, nBy),
                mCompressBy);
          mPreprocCtrl->SetMinSize(box_size);
-
-         S.Validator<wxGenericValidator>(&mStereoInd)
-            .AddCheckBox(XO("Compress stereo channels independently"),
-               DEF_StereoInd);
       }
       S.EndMultiColumn();
+      S.EndVerticalLay();
+      S.EndHorizontalLay();
+
+      S.Validator<wxGenericValidator>(&mStereoInd)
+         .AddCheckBox(XO("Compress stereo channels independently"),
+            DEF_StereoInd);
    }
    S.EndStatic();
 
    S.StartStatic(XO("Compressor"));
    {
+      int textbox_width = S.GetParent()->GetTextExtent("10.000001XX").GetWidth();
+      SliderTextCtrl* ctrl = nullptr;
+
+      S.StartHorizontalLay(wxEXPAND, true);
+      S.StartVerticalLay(1);
       S.StartMultiColumn(3, wxEXPAND);
       {
          S.SetStretchyCol(1);
-         int textbox_width = S.GetParent()->GetTextExtent("0.000001").GetWidth();
-         SliderTextCtrl* ctrl = nullptr;
 
          S.AddVariableText(XO("Threshold:"), true,
             wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
@@ -926,6 +946,40 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
          S.AddVariableText(XO("dB"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
 
+         /* i18n-hint: Make-up, i.e. correct for any reduction, rather than fabricate it.*/
+         S.AddVariableText(XO("Make-up Gain:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Make-up Gain"))
+            .Style(SliderTextCtrl::HORIZONTAL)
+            .AddSliderTextCtrl({}, DEF_MakeupGain, MAX_MakeupGain,
+               MIN_MakeupGain, ScaleToPrecision(SCL_MakeupGain),
+               &mMakeupGainPct);
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO("%"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+         S.AddVariableText(XO("Dry/Wet:"), true,
+            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+         ctrl = S.Name(XO("Dry/Wet"))
+            .Style(SliderTextCtrl::HORIZONTAL)
+            .AddSliderTextCtrl({}, DEF_DryWet, MAX_DryWet,
+               MIN_DryWet, ScaleToPrecision(SCL_DryWet),
+               &mDryWetPct);
+         ctrl->SetMinTextboxWidth(textbox_width);
+         S.AddVariableText(XO("%"), true,
+            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+      }
+      S.EndMultiColumn();
+      S.EndVerticalLay();
+
+      S.AddSpace(15, 0, 0);
+
+      S.StartHorizontalLay(wxEXPAND, true);
+      S.StartVerticalLay(1);
+      S.StartMultiColumn(3, wxEXPAND);
+      {
+         S.SetStretchyCol(1);
+
          S.AddVariableText(XO("Attack Time:"), true,
             wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
          mAttackTimeCtrl = S.Name(XO("Attack Time"))
@@ -969,31 +1023,10 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
          ctrl->SetMinTextboxWidth(textbox_width);
          S.AddVariableText(XO("s"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
-
-         /* i18n-hint: Make-up, i.e. correct for any reduction, rather than fabricate it.*/
-         S.AddVariableText(XO("Make-up Gain:"), true,
-            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
-         ctrl = S.Name(XO("Make-up Gain"))
-            .Style(SliderTextCtrl::HORIZONTAL)
-            .AddSliderTextCtrl({}, DEF_MakeupGain, MAX_MakeupGain,
-               MIN_MakeupGain, ScaleToPrecision(SCL_MakeupGain),
-               &mMakeupGainPct);
-         ctrl->SetMinTextboxWidth(textbox_width);
-         S.AddVariableText(XO("%"), true,
-            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
-
-         S.AddVariableText(XO("Dry/Wet:"), true,
-            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
-         ctrl = S.Name(XO("Dry/Wet"))
-            .Style(SliderTextCtrl::HORIZONTAL)
-            .AddSliderTextCtrl({}, DEF_DryWet, MAX_DryWet,
-               MIN_DryWet, ScaleToPrecision(SCL_DryWet),
-               &mDryWetPct);
-         ctrl->SetMinTextboxWidth(textbox_width);
-         S.AddVariableText(XO("%"), true,
-            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
       }
       S.EndMultiColumn();
+      S.EndVerticalLay();
+      S.EndHorizontalLay();
    }
    S.EndVerticalLay();
 }

From be1883bdd30d93e942f0b0bfffd4fc3a88010b0a Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sat, 26 Sep 2020 14:10:14 +0200
Subject: [PATCH 22/33] Fix invalid Compressor plots if out of range values are
 entered in textboxes.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 8f8ea52d8..c9b0c1e7f 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -97,6 +97,11 @@ inline int ScaleToPrecision(double scale)
    return ceil(log10(scale));
 }
 
+inline bool IsInRange(double val, double min, double max)
+{
+   return val >= min && val <= max;
+}
+
 BEGIN_EVENT_TABLE(EffectCompressor2, wxEvtHandler)
    EVT_CHECKBOX(wxID_ANY, EffectCompressor2::OnUpdateUI)
    EVT_CHOICE(wxID_ANY, EffectCompressor2::OnUpdateUI)
@@ -1622,6 +1627,15 @@ void EffectCompressor2::UpdateCompressorPlot()
    plot = mGainPlot->GetPlotData(0);
    wxASSERT(plot->xdata.size() == plot->ydata.size());
 
+   if(!IsInRange(mThresholdDB, MIN_Threshold, MAX_Threshold))
+       return;
+   if(!IsInRange(mRatio, MIN_Ratio, MAX_Ratio))
+       return;
+   if(!IsInRange(mKneeWidthDB, MIN_KneeWidth, MAX_KneeWidth))
+       return;
+   if(!IsInRange(mMakeupGainPct, MIN_MakeupGain, MAX_MakeupGain))
+       return;
+
    InitGainCalculation();
    size_t xsize = plot->xdata.size();
    for(size_t i = 0; i < xsize; ++i)
@@ -1639,6 +1653,15 @@ void EffectCompressor2::UpdateResponsePlot()
    plot = mResponsePlot->GetPlotData(1);
    wxASSERT(plot->xdata.size() == plot->ydata.size());
 
+   if(!IsInRange(mAttackTime, MIN_AttackTime, MAX_AttackTime))
+       return;
+   if(!IsInRange(mReleaseTime, MIN_ReleaseTime, MAX_ReleaseTime))
+       return;
+   if(!IsInRange(mLookaheadTime, MIN_LookaheadTime, MAX_LookaheadTime))
+       return;
+   if(!IsInRange(mLookbehindTime, MIN_LookbehindTime, MAX_LookbehindTime))
+       return;
+
    std::unique_ptr<SamplePreprocessor> preproc;
    std::unique_ptr<EnvelopeDetector> envelope;
    float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME;

From 4b4fbafb0e06f9a34bc1477b6b77f91c429abc12 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Tue, 2 Feb 2021 19:21:32 +0100
Subject: [PATCH 23/33] Remove "PT1" from dynamic compressor effect dialog.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index c9b0c1e7f..4546bd9ea 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -59,7 +59,7 @@ enum kAlgorithms
 static const ComponentInterfaceSymbol kAlgorithmStrings[nAlgos] =
 {
    { XO("Exponential-Fit") },
-   { XO("Analog Model (PT1)") }
+   { XO("Analog Model") }
 };
 
 enum kCompressBy

From 7a23e6a52fcd7846f18a0aebfb9b2970ddd54f1c Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Tue, 2 Feb 2021 19:30:32 +0100
Subject: [PATCH 24/33] Remove Dry/Wet mix from dynamic compressor effect.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 scripts/debug/compressor2_trace.m | 24 +++++++++++-------------
 src/effects/Compressor2.cpp       | 23 +----------------------
 src/effects/Compressor2.h         |  2 --
 3 files changed, 12 insertions(+), 37 deletions(-)

diff --git a/scripts/debug/compressor2_trace.m b/scripts/debug/compressor2_trace.m
index fa8284dcf..0307a0a9c 100644
--- a/scripts/debug/compressor2_trace.m
+++ b/scripts/debug/compressor2_trace.m
@@ -4,9 +4,9 @@ stereo = true;
 bfile = fopen("/tmp/audio.out");
 
 if stereo
-  width = 15;
+  width = 14;
 else
-  width = 13;
+  width = 12;
 end
 
 raw_data = reshape(fread(bfile, 'float'), width, []).';
@@ -20,18 +20,17 @@ data.release_time = raw_data(:,5);
 data.lookahead_time = raw_data(:,6);
 data.lookbehind_time = raw_data(:,7);
 data.makeup_gain_pct = raw_data(:,8);
-data.dry_wet_pct = raw_data(:,9);
 
 if stereo
-  data.in = horzcat(raw_data(:,10), raw_data(:,11));
-  data.env = raw_data(:,12);
-  data.gain = raw_data(:,13);
-  data.out = horzcat(raw_data(:,14), raw_data(:,15));
-else
-  data.in = raw_data(:,10);
+  data.in = horzcat(raw_data(:,9), raw_data(:,10));
   data.env = raw_data(:,11);
   data.gain = raw_data(:,12);
-  data.out = raw_data(:,13);
+  data.out = horzcat(raw_data(:,13), raw_data(:,14));
+else
+  data.in = raw_data(:,9);
+  data.env = raw_data(:,10);
+  data.gain = raw_data(:,11);
+  data.out = raw_data(:,12);
 end
 
 figure(1);
@@ -46,7 +45,6 @@ plot(data.release_time.*10, 'c', "linewidth", 2);
 plot(data.lookahead_time, 'm');
 plot(data.lookbehind_time, 'm');
 plot(data.makeup_gain_pct, 'r');
-plot(data.dry_wet_pct, 'r');
 plot(data.env.*100, 'k', "linewidth", 2);
 plot(data.gain.*50, 'k', "linestyle", '--');
 hold off;
@@ -55,9 +53,9 @@ grid;
 if stereo
   legend("in*100", "in*100", "out*100", "out*100", "threshold", "ratio", ...
     "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ...
-    "makeup", "dry/wet", "env*100", "gain*50");
+    "makeup", "env*100", "gain*50");
 else
   legend("in*100", "out*100", "threshold", "ratio", ...
     "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ...
-    "makeup", "dry/wet", "env*100", "gain*50");
+    "makeup", "env*100", "gain*50");
 end
diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 4546bd9ea..148137861 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -90,7 +90,6 @@ Param( ReleaseTime,    double,  wxT("ReleaseTime"),        1.0,     0.00001, 30.
 Param( LookaheadTime,  double,  wxT("LookaheadTime"),      0.0,     0.0,     10.0,   200.0 );
 Param( LookbehindTime, double,  wxT("LookbehindTime"),     0.1,     0.0,     10.0,   200.0 );
 Param( MakeupGain,     double,  wxT("MakeupGain"),         0.0,     0.0,    100.0,     1.0 );
-Param( DryWet,         double,  wxT("DryWet"),           100.0,     0.0,    100.0,     1.0 );
 
 inline int ScaleToPrecision(double scale)
 {
@@ -530,7 +529,6 @@ EffectCompressor2::EffectCompressor2()
    mLookaheadTime = DEF_LookaheadTime;
    mLookbehindTime = DEF_LookbehindTime;
    mMakeupGainPct = DEF_MakeupGain;
-   mDryWetPct = DEF_DryWet;
 
    SetLinearEffectFlag(false);
 }
@@ -665,7 +663,6 @@ bool EffectCompressor2::DefineParams( ShuttleParams & S )
    S.SHUTTLE_PARAM(mLookaheadTime, LookaheadTime);
    S.SHUTTLE_PARAM(mLookbehindTime, LookbehindTime);
    S.SHUTTLE_PARAM(mMakeupGainPct, MakeupGain);
-   S.SHUTTLE_PARAM(mDryWetPct, DryWet);
 
    return true;
 }
@@ -684,7 +681,6 @@ bool EffectCompressor2::GetAutomationParameters(CommandParameters & parms)
    parms.Write(KEY_LookaheadTime, mLookaheadTime);
    parms.Write(KEY_LookbehindTime, mLookbehindTime);
    parms.Write(KEY_MakeupGain, mMakeupGainPct);
-   parms.Write(KEY_DryWet, mDryWetPct);
 
    return true;
 }
@@ -703,7 +699,6 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms)
    ReadAndVerifyDouble(LookaheadTime);
    ReadAndVerifyDouble(LookbehindTime);
    ReadAndVerifyDouble(MakeupGain);
-   ReadAndVerifyDouble(DryWet);
 
    mAlgorithm = Algorithm;
    mCompressBy = CompressBy;
@@ -717,7 +712,6 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms)
    mLookaheadTime = LookaheadTime;
    mLookbehindTime = LookbehindTime;
    mMakeupGainPct = MakeupGain;
-   mDryWetPct = DryWet;
 
    return true;
 }
@@ -747,7 +741,6 @@ bool EffectCompressor2::Startup()
       mLookaheadTime = DEF_LookaheadTime;
       mLookbehindTime = DEF_LookbehindTime;
       mMakeupGainPct = DEF_MakeupGain;
-      mDryWetPct = DEF_DryWet;
 
       SaveUserPreset(GetCurrentSettingsGroup());
 
@@ -962,17 +955,6 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
          ctrl->SetMinTextboxWidth(textbox_width);
          S.AddVariableText(XO("%"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
-
-         S.AddVariableText(XO("Dry/Wet:"), true,
-            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
-         ctrl = S.Name(XO("Dry/Wet"))
-            .Style(SliderTextCtrl::HORIZONTAL)
-            .AddSliderTextCtrl({}, DEF_DryWet, MAX_DryWet,
-               MIN_DryWet, ScaleToPrecision(SCL_DryWet),
-               &mDryWetPct);
-         ctrl->SetMinTextboxWidth(textbox_width);
-         S.AddVariableText(XO("%"), true,
-            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
       }
       S.EndMultiColumn();
       S.EndVerticalLay();
@@ -1066,7 +1048,6 @@ bool EffectCompressor2::TransferDataFromWindow()
 
 void EffectCompressor2::InitGainCalculation()
 {
-   mDryWet = mDryWetPct / 100.0;
    mMakeupGainDB = mMakeupGainPct / 100.0 *
       -(mThresholdDB * (1.0 - 1.0 / mRatio));
    mMakeupGain = DB_TO_LINEAR(mMakeupGainDB);
@@ -1480,7 +1461,7 @@ inline float EffectCompressor2::EnvelopeSample(PipelineBuffer& pbuf, size_t rp)
 
 inline void EffectCompressor2::CompressSample(float env, size_t wp)
 {
-   float gain = (1.0 - mDryWet) + CompressorGain(env) * mDryWet;
+   float gain = CompressorGain(env);
 
 #ifdef DEBUG_COMPRESSOR2_TRACE2
    float ThresholdDB = mThresholdDB;
@@ -1491,7 +1472,6 @@ inline void EffectCompressor2::CompressSample(float env, size_t wp)
    float LookaheadTime = mLookaheadTime;
    float LookbehindTime = mLookbehindTime;
    float MakeupGainPct = mMakeupGainPct;
-   float DryWetPct = mDryWetPct;
 
    debugfile.write((char*)&ThresholdDB, sizeof(float));
    debugfile.write((char*)&Ratio, sizeof(float));
@@ -1501,7 +1481,6 @@ inline void EffectCompressor2::CompressSample(float env, size_t wp)
    debugfile.write((char*)&LookaheadTime, sizeof(float));
    debugfile.write((char*)&LookbehindTime, sizeof(float));
    debugfile.write((char*)&MakeupGainPct, sizeof(float));
-   debugfile.write((char*)&DryWetPct, sizeof(float));
    debugfile.write((char*)&mPipeline[0][0][wp], sizeof(float));
    if(mProcStereo)
       debugfile.write((char*)&mPipeline[0][1][wp], sizeof(float));
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index 10cdd8921..a813a508e 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -267,10 +267,8 @@ private:
    double    mLookaheadTime;
    double    mLookbehindTime;
    double    mMakeupGainPct;
-   double    mDryWetPct;
 
    // cached intermediate values
-   double mDryWet;
    double mMakeupGain;
    double mMakeupGainDB;
    size_t mLookaheadLength;

From 90d7c1d2265a33e88113c03b62b3a823870e044c Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sun, 7 Feb 2021 18:31:34 +0100
Subject: [PATCH 25/33] Add labels to Compressor2 plots and tweak parameter
 labels.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 148137861..214dd2346 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -815,6 +815,9 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
    {
       PlotData* plot;
 
+      S.StartVerticalLay();
+      S.AddVariableText(XO("Envelope dependent gain"), 0,
+         wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
       mGainPlot = S.MinSize( { 200, 200 } )
          .AddPlot({}, -60, 0, -60, 0, XO("dB"), XO("dB"),
             Ruler::LinearDBFormat, Ruler::LinearDBFormat);
@@ -826,10 +829,15 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
       plot->ydata.resize(61);
       std::iota(plot->xdata.begin(), plot->xdata.end(), -60);
 
+      S.EndVerticalLay();
+      S.StartVerticalLay();
+
+      S.AddVariableText(XO("Envelope detector step response"), 0,
+         wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
       mResponsePlot = S.MinSize( { 200, 200 } )
          .AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""),
             Ruler::IntFormat, Ruler::RealFormat, 2);
-      mResponsePlot->SetName(XO("Compressor step response plot"));
+      mResponsePlot->SetName(XO("Envelope detector step response plot"));
 
       plot = mResponsePlot->GetPlotData(0);
       plot->pen = std::unique_ptr<wxPen>(
@@ -847,6 +855,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
       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.EndVerticalLay();
    }
    S.EndHorizontalLay();
 
@@ -967,9 +976,9 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
       {
          S.SetStretchyCol(1);
 
-         S.AddVariableText(XO("Attack Time:"), true,
+         S.AddVariableText(XO("Attack:"), true,
             wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
-         mAttackTimeCtrl = S.Name(XO("Attack Time"))
+         mAttackTimeCtrl = S.Name(XO("Attack"))
             .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
             .AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime,
                MIN_AttackTime, ScaleToPrecision(SCL_AttackTime),
@@ -978,9 +987,9 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
          S.AddVariableText(XO("s"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
 
-         S.AddVariableText(XO("Release Time:"), true,
+         S.AddVariableText(XO("Release:"), true,
             wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
-         ctrl = S.Name(XO("Release Time"))
+         ctrl = S.Name(XO("Release"))
             .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
             .AddSliderTextCtrl({}, DEF_ReleaseTime, MAX_ReleaseTime,
                MIN_ReleaseTime, ScaleToPrecision(SCL_ReleaseTime),

From 3d4e504bc38955f9cc3c1cba39989811b109c5fa Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Tue, 9 Feb 2021 19:38:02 +0100
Subject: [PATCH 26/33] Draw full compressor response plot

instead of just the envelope detector step response.
Also increase minimum plot width for non realtime enabled case.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 53 ++++++++++++++++++++++++++++---------
 src/effects/Compressor2.h   |  9 ++++---
 2 files changed, 47 insertions(+), 15 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 214dd2346..f458bd0bf 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -131,9 +131,9 @@ float SlidingRmsPreprocessor::ProcessSample(float valueL, float valueR)
    return DoProcessSample((valueL * valueL + valueR * valueR) / 2.0);
 }
 
-void SlidingRmsPreprocessor::Reset()
+void SlidingRmsPreprocessor::Reset(float level)
 {
-   mSum = 0;
+   mSum = (level / mGain) * (level / mGain) * float(mWindow.size());
    mPos = 0;
    mInsertCount = 0;
    std::fill(mWindow.begin(), mWindow.end(), 0);
@@ -203,11 +203,11 @@ float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR)
    return DoProcessSample((fabs(valueL) + fabs(valueR)) / 2.0);
 }
 
-void SlidingMaxPreprocessor::Reset()
+void SlidingMaxPreprocessor::Reset(float value)
 {
    mPos = 0;
-   std::fill(mWindow.begin(), mWindow.end(), 0);
-   std::fill(mMaxes.begin(), mMaxes.end(), 0);
+   std::fill(mWindow.begin(), mWindow.end(), value);
+   std::fill(mMaxes.begin(), mMaxes.end(), value);
 }
 
 void SlidingMaxPreprocessor::SetWindowSize(size_t windowSize)
@@ -303,6 +303,13 @@ ExpFitEnvelopeDetector::ExpFitEnvelopeDetector(
    SetParams(rate, attackTime, releaseTime);
 }
 
+void ExpFitEnvelopeDetector::Reset(float value)
+{
+   std::fill(mProcessedBuffer.begin(), mProcessedBuffer.end(), value);
+   std::fill(mProcessingBuffer.begin(), mProcessingBuffer.end(), value);
+   std::fill(mLookaheadBuffer.begin(), mLookaheadBuffer.end(), value);
+}
+
 void ExpFitEnvelopeDetector::SetParams(
    float sampleRate, float attackTime, float releaseTime)
 {
@@ -410,6 +417,14 @@ float Pt1EnvelopeDetector::DecayFactor()
    return mReleaseFactor;
 }
 
+void Pt1EnvelopeDetector::Reset(float value)
+{
+   value *= mGainCorrection;
+   std::fill(mProcessedBuffer.begin(), mProcessedBuffer.end(), value);
+   std::fill(mProcessingBuffer.begin(), mProcessingBuffer.end(), value);
+   std::fill(mLookaheadBuffer.begin(), mLookaheadBuffer.end(), value);
+}
+
 void Pt1EnvelopeDetector::SetParams(
    float sampleRate, float attackTime, float releaseTime)
 {
@@ -818,7 +833,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
       S.StartVerticalLay();
       S.AddVariableText(XO("Envelope dependent gain"), 0,
          wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
-      mGainPlot = S.MinSize( { 200, 200 } )
+      mGainPlot = S.MinSize( { 400, 200 } )
          .AddPlot({}, -60, 0, -60, 0, XO("dB"), XO("dB"),
             Ruler::LinearDBFormat, Ruler::LinearDBFormat);
 
@@ -832,19 +847,19 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
       S.EndVerticalLay();
       S.StartVerticalLay();
 
-      S.AddVariableText(XO("Envelope detector step response"), 0,
+      S.AddVariableText(XO("Compressor step response"), 0,
          wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
-      mResponsePlot = S.MinSize( { 200, 200 } )
+      mResponsePlot = S.MinSize( { 400, 200 } )
          .AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""),
             Ruler::IntFormat, Ruler::RealFormat, 2);
-      mResponsePlot->SetName(XO("Envelope detector step response plot"));
+      mResponsePlot->SetName(XO("Compressor step response plot"));
 
       plot = mResponsePlot->GetPlotData(0);
       plot->pen = std::unique_ptr<wxPen>(
          safenew wxPen(AColor::WideEnvelopePen));
       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->ydata = {0.1, 0.1, 1, 1, 0.1, 0.1};
 
       plot = mResponsePlot->GetPlotData(1);
       plot->pen = std::unique_ptr<wxPen>(
@@ -1655,11 +1670,16 @@ void EffectCompressor2::UpdateResponsePlot()
    float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME;
 
    size_t lookahead_size = CalcLookaheadLength(plot_rate);
+   lookahead_size -= (lookahead_size > 0);
    ssize_t block_size = float(TAU_FACTOR) * (mAttackTime + 1.0) * plot_rate;
 
+   InitGainCalculation();
    preproc = InitPreprocessor(plot_rate, true);
    envelope = InitEnvelope(plot_rate, block_size, true);
 
+   preproc->Reset(0.1);
+   envelope->Reset(0.1);
+
    ssize_t step_start = RESPONSE_PLOT_STEP_START * plot_rate - lookahead_size;
    ssize_t step_stop = RESPONSE_PLOT_STEP_STOP * plot_rate - lookahead_size;
 
@@ -1667,12 +1687,21 @@ void EffectCompressor2::UpdateResponsePlot()
    for(ssize_t i = -lookahead_size; i < 2*block_size; ++i)
    {
       if(i < step_start || i > step_stop)
-         envelope->ProcessSample(preproc->ProcessSample(0));
+         envelope->ProcessSample(preproc->ProcessSample(0.1));
       else
          envelope->ProcessSample(preproc->ProcessSample(1));
    }
+
    for(ssize_t i = 0; i < xsize; ++i)
-      plot->ydata[i] = envelope->ProcessSample(preproc->ProcessSample(0));
+   {
+      float x = 1;
+      if(i < RESPONSE_PLOT_STEP_START * plot_rate ||
+            i > RESPONSE_PLOT_STEP_STOP * plot_rate)
+          x = 0.1;
+
+      plot->ydata[i] = x * CompressorGain(
+         envelope->ProcessSample(preproc->ProcessSample(0.1)));
+   }
 
    mResponsePlot->Refresh(false);
 }
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index a813a508e..de09581e8 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -29,7 +29,7 @@ class SamplePreprocessor
    public:
       virtual float ProcessSample(float value) = 0;
       virtual float ProcessSample(float valueL, float valueR) = 0;
-      virtual void Reset() = 0;
+      virtual void Reset(float value = 0) = 0;
       virtual void SetWindowSize(size_t windowSize) = 0;
 };
 
@@ -40,7 +40,7 @@ class SlidingRmsPreprocessor : public SamplePreprocessor
 
       virtual float ProcessSample(float value);
       virtual float ProcessSample(float valueL, float valueR);
-      virtual void Reset();
+      virtual void Reset(float value = 0);
       virtual void SetWindowSize(size_t windowSize);
 
       static const size_t REFRESH_WINDOW_EVERY = 1048576; // 1 MB
@@ -63,7 +63,7 @@ class SlidingMaxPreprocessor : public SamplePreprocessor
 
       virtual float ProcessSample(float value);
       virtual float ProcessSample(float valueL, float valueR);
-      virtual void Reset();
+      virtual void Reset(float value = 0);
       virtual void SetWindowSize(size_t windowSize);
 
    private:
@@ -87,6 +87,7 @@ class EnvelopeDetector
       inline float InitialCondition() const { return mInitialCondition; }
       inline size_t InitialConditionSize() const { return mInitialBlockSize; }
 
+      virtual void Reset(float value = 0) = 0;
       virtual void SetParams(float sampleRate, float attackTime,
          float releaseTime) = 0;
 
@@ -110,6 +111,7 @@ class ExpFitEnvelopeDetector : public EnvelopeDetector
       ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime,
          size_t buffer_size);
 
+      virtual void Reset(float value);
       virtual void SetParams(float sampleRate, float attackTime,
          float releaseTime);
 
@@ -127,6 +129,7 @@ class Pt1EnvelopeDetector : public EnvelopeDetector
          size_t buffer_size, bool correctGain = true);
       virtual void CalcInitialCondition(float value);
 
+      virtual void Reset(float value);
       virtual void SetParams(float sampleRate, float attackTime,
          float releaseTime);
       virtual float AttackFactor();

From fd4276e3f2540491ad368eae509f212e14360ffd Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Fri, 12 Feb 2021 15:10:14 +0100
Subject: [PATCH 27/33] Clamp attack/release times at 1/sample_rate.

Otherwise, numerical instability could occur.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index f458bd0bf..5fa0aa4e3 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -313,6 +313,8 @@ void ExpFitEnvelopeDetector::Reset(float value)
 void ExpFitEnvelopeDetector::SetParams(
    float sampleRate, float attackTime, float releaseTime)
 {
+   attackTime = std::max(attackTime, 1.0f / sampleRate);
+   releaseTime = std::max(releaseTime, 1.0f / sampleRate);
    mAttackFactor = exp(-1.0 / (sampleRate * attackTime));
    mReleaseFactor = exp(-1.0 / (sampleRate * releaseTime));
 }
@@ -428,6 +430,9 @@ void Pt1EnvelopeDetector::Reset(float value)
 void Pt1EnvelopeDetector::SetParams(
    float sampleRate, float attackTime, float releaseTime)
 {
+   attackTime = std::max(attackTime, 1.0f / sampleRate);
+   releaseTime = std::max(releaseTime, 1.0f / sampleRate);
+
    // Approximate peak amplitude correction factor.
    if(mCorrectGain)
       mGainCorrection = 1.0 + exp(attackTime / 30.0);

From dec7e72abce6a7d0cd75df8bd37a3a38b58d92cd Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Fri, 12 Feb 2021 14:25:37 +0100
Subject: [PATCH 28/33] Add "offset" to SliderTextCtrl widget

This parameter adds an offset to the value before calculating the
logarithm to tweak the scaling of the slider.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/ShuttleGui.cpp             |  4 ++--
 src/ShuttleGui.h               |  2 +-
 src/widgets/SliderTextCtrl.cpp | 14 ++++++++------
 src/widgets/SliderTextCtrl.h   |  4 +++-
 4 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/src/ShuttleGui.cpp b/src/ShuttleGui.cpp
index ead5aa0f2..48f5e78b2 100644
--- a/src/ShuttleGui.cpp
+++ b/src/ShuttleGui.cpp
@@ -618,7 +618,7 @@ wxSlider * ShuttleGuiBase::AddSlider(
 
 SliderTextCtrl* ShuttleGuiBase::AddSliderTextCtrl(
    const TranslatableString &Prompt, double pos, double Max, double Min,
-   int precision, double* value, double scale)
+   int precision, double* value, double scale, double offset)
 {
    HandleOptionality( Prompt );
    AddPrompt( Prompt );
@@ -627,7 +627,7 @@ SliderTextCtrl* ShuttleGuiBase::AddSliderTextCtrl(
       return wxDynamicCast(wxWindow::FindWindowById( miId, mpDlg), SliderTextCtrl);
    SliderTextCtrl * pSlider;
    mpWind = pSlider = safenew SliderTextCtrl(GetParent(), miId,
-      pos, Min, Max, precision, scale, wxDefaultPosition, wxDefaultSize,
+      pos, Min, Max, precision, scale, offset, wxDefaultPosition, wxDefaultSize,
       GetStyle( SliderTextCtrl::HORIZONTAL ),
       value
    );
diff --git a/src/ShuttleGui.h b/src/ShuttleGui.h
index 04a97b28e..5bd72d9a8 100644
--- a/src/ShuttleGui.h
+++ b/src/ShuttleGui.h
@@ -267,7 +267,7 @@ public:
 
    SliderTextCtrl* AddSliderTextCtrl(
       const TranslatableString &Prompt, double pos, double Max, double Min = 0,
-      int precision = 2, double* value = NULL, double scale = 0);
+      int precision = 2, double* value = NULL, double scale = 0, double offset = 0);
 
    // Pass the same initValue to the sequence of calls to AddRadioButton and
    // AddRadioButtonToGroup.
diff --git a/src/widgets/SliderTextCtrl.cpp b/src/widgets/SliderTextCtrl.cpp
index 524d1a9f6..837152d89 100644
--- a/src/widgets/SliderTextCtrl.cpp
+++ b/src/widgets/SliderTextCtrl.cpp
@@ -29,7 +29,8 @@ wxDEFINE_EVENT(cEVT_SLIDERTEXT, wxCommandEvent);
 
 SliderTextCtrl::SliderTextCtrl(wxWindow *parent, wxWindowID winid,
    double value, double min, double max, int precision, double scale,
-   const wxPoint& pos, const wxSize& size, long style, double* varValue)
+   double offset, const wxPoint& pos, const wxSize& size, long style,
+   double* varValue)
    : wxPanelWrapper(parent, winid, pos, size, wxWS_EX_VALIDATE_RECURSIVELY)
 {
    m_log = style & LOG;
@@ -38,6 +39,7 @@ SliderTextCtrl::SliderTextCtrl(wxWindow *parent, wxWindowID winid,
    m_min = min;
    m_max = max;
    m_zero = -std::numeric_limits<double>::infinity();
+   m_offset = offset;
 
    if(m_int)
    {
@@ -62,13 +64,13 @@ SliderTextCtrl::SliderTextCtrl(wxWindow *parent, wxWindowID winid,
          min = m_zero;
       }
       else
-         min = log10(min);
+         min = log10(min + m_offset);
 
       if(value <= 0.0)
          value = m_zero;
       else
-         value = log10(value);
-      max = log10(max);
+         value = log10(value + m_offset);
+      max = log10(max + m_offset);
    }
 
    m_sizer = safenew wxBoxSizer(
@@ -118,7 +120,7 @@ void SliderTextCtrl::OnTextChange(wxCommandEvent& event)
       if(m_value == 0.0)
          value = m_zero;
       else
-         value = log10(m_value);
+         value = log10(m_value + m_offset);
    }
    m_slider->SetValue(round(value * m_scale));
    event.SetEventType(cEVT_SLIDERTEXT);
@@ -134,7 +136,7 @@ void SliderTextCtrl::OnSlider(wxCommandEvent& event)
          m_value = 0.0;
       else
       {
-         m_value = pow(10.0, m_value);
+         m_value = pow(10.0, m_value) - m_offset;
          m_value = std::max(m_min, m_value);
          m_value = std::min(m_max, m_value);
       }
diff --git a/src/widgets/SliderTextCtrl.h b/src/widgets/SliderTextCtrl.h
index f8d27efa0..b2c0d15da 100644
--- a/src/widgets/SliderTextCtrl.h
+++ b/src/widgets/SliderTextCtrl.h
@@ -37,7 +37,8 @@ class SliderTextCtrl : public wxPanelWrapper
 
       SliderTextCtrl(wxWindow *parent, wxWindowID winid,
          double value, double min, double max, int precision = 2,
-         double scale = 0, const wxPoint& pos = wxDefaultPosition,
+         double scale = 0, double offset = 0,
+         const wxPoint& pos = wxDefaultPosition,
          const wxSize& size = wxDefaultSize, long style = HORIZONTAL,
          double* varValue = NULL);
 
@@ -69,6 +70,7 @@ class SliderTextCtrl : public wxPanelWrapper
       double m_min;
       double m_max;
       double m_zero;
+      double m_offset;
       wxString m_format;
 
       DECLARE_EVENT_TABLE()

From 5c14ec43d3ceb72526500427baba2187aec83321 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Thu, 11 Feb 2021 18:30:05 +0100
Subject: [PATCH 29/33] Tweak parameter ranges and lookaround keyboard tick
 scaling.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp     | 16 ++++++++--------
 tests/octave/compressor2_test.m |  2 +-
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 5fa0aa4e3..5af30a1ae 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -83,10 +83,10 @@ Param( CompressBy,     int,     wxT("CompressBy"),   kAmplitude,      0,   nBy-1
 Param( StereoInd,      bool,    wxT("StereoIndependent"), false,   false,   true,      1   );
 
 Param( Threshold,      double,  wxT("Threshold"),        -12.0,   -60.0,     -1.0,     1.0 );
-Param( Ratio,          double,  wxT("Ratio"),              2.0,     1.1,    100.0,    10.0 );
+Param( Ratio,          double,  wxT("Ratio"),              2.0,     1.1,    100.0,    20.0 );
 Param( KneeWidth,      double,  wxT("KneeWidth"),         10.0,     0.0,     20.0,    10.0 );
-Param( AttackTime,     double,  wxT("AttackTime"),         0.2,     0.00001, 30.0, 20000.0 );
-Param( ReleaseTime,    double,  wxT("ReleaseTime"),        1.0,     0.00001, 30.0, 20000.0 );
+Param( AttackTime,     double,  wxT("AttackTime"),         0.2,     0.0001,  30.0,  2000.0 );
+Param( ReleaseTime,    double,  wxT("ReleaseTime"),        1.0,     0.0001,  30.0,  2000.0 );
 Param( LookaheadTime,  double,  wxT("LookaheadTime"),      0.0,     0.0,     10.0,   200.0 );
 Param( LookbehindTime, double,  wxT("LookbehindTime"),     0.1,     0.0,     10.0,   200.0 );
 Param( MakeupGain,     double,  wxT("MakeupGain"),         0.0,     0.0,    100.0,     1.0 );
@@ -931,7 +931,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
 
    S.StartStatic(XO("Compressor"));
    {
-      int textbox_width = S.GetParent()->GetTextExtent("10.000001XX").GetWidth();
+      int textbox_width = S.GetParent()->GetTextExtent("10.00001XX").GetWidth();
       SliderTextCtrl* ctrl = nullptr;
 
       S.StartHorizontalLay(wxEXPAND, true);
@@ -1002,7 +1002,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
             .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
             .AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime,
                MIN_AttackTime, ScaleToPrecision(SCL_AttackTime),
-               &mAttackTime, SCL_AttackTime / 1000);
+               &mAttackTime, SCL_AttackTime / 100, 0.033);
          mAttackTimeCtrl->SetMinTextboxWidth(textbox_width);
          S.AddVariableText(XO("s"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
@@ -1013,7 +1013,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
             .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
             .AddSliderTextCtrl({}, DEF_ReleaseTime, MAX_ReleaseTime,
                MIN_ReleaseTime, ScaleToPrecision(SCL_ReleaseTime),
-               &mReleaseTime, SCL_ReleaseTime / 1000);
+               &mReleaseTime, SCL_ReleaseTime / 100, 0.033);
          ctrl->SetMinTextboxWidth(textbox_width);
          S.AddVariableText(XO("s"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
@@ -1024,7 +1024,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
             .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
             .AddSliderTextCtrl({}, DEF_LookaheadTime, MAX_LookaheadTime,
                MIN_LookaheadTime, ScaleToPrecision(SCL_LookaheadTime),
-               &mLookaheadTime);
+               &mLookaheadTime, SCL_LookaheadTime / 10);
          mLookaheadTimeCtrl->SetMinTextboxWidth(textbox_width);
          S.AddVariableText(XO("s"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
@@ -1035,7 +1035,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
             .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
             .AddSliderTextCtrl({}, DEF_LookbehindTime, MAX_LookbehindTime,
                MIN_LookbehindTime, ScaleToPrecision(SCL_LookbehindTime),
-               &mLookbehindTime);
+               &mLookbehindTime, SCL_LookbehindTime / 10);
          ctrl->SetMinTextboxWidth(textbox_width);
          S.AddVariableText(XO("s"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
diff --git a/tests/octave/compressor2_test.m b/tests/octave/compressor2_test.m
index 90e714eb1..81c5783eb 100644
--- a/tests/octave/compressor2_test.m
+++ b/tests/octave/compressor2_test.m
@@ -220,7 +220,7 @@ do_test_equ(settled(y, fs, 0.1), ...
 CURRENT_TEST = "Compressor2, mono, ultra short attack time";
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.00001 ReleaseTime=0.00001 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.0001 ReleaseTime=0.0001 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
 y = import_from_aud(2);
 
 # XXX: use larger epsilon due to numerical issues

From c534e07424e934488f8870368943ef3c379ff763 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Mon, 15 Feb 2021 19:08:59 +0100
Subject: [PATCH 30/33] Replace Compressor2 dynamic makeup gain with fixed
 output gain.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 scripts/debug/compressor2_trace.m |  8 ++---
 src/effects/Compressor2.cpp       | 49 ++++++++++++-------------------
 src/effects/Compressor2.h         |  5 +---
 tests/octave/compressor2_test.m   | 31 ++++++++++---------
 4 files changed, 39 insertions(+), 54 deletions(-)

diff --git a/scripts/debug/compressor2_trace.m b/scripts/debug/compressor2_trace.m
index 0307a0a9c..f7b2721d8 100644
--- a/scripts/debug/compressor2_trace.m
+++ b/scripts/debug/compressor2_trace.m
@@ -19,7 +19,7 @@ data.attack_time = raw_data(:,4);
 data.release_time = raw_data(:,5);
 data.lookahead_time = raw_data(:,6);
 data.lookbehind_time = raw_data(:,7);
-data.makeup_gain_pct = raw_data(:,8);
+data.output_gain_DB = raw_data(:,8);
 
 if stereo
   data.in = horzcat(raw_data(:,9), raw_data(:,10));
@@ -44,7 +44,7 @@ plot(data.attack_time.*10, 'c', "linewidth", 2);
 plot(data.release_time.*10, 'c', "linewidth", 2);
 plot(data.lookahead_time, 'm');
 plot(data.lookbehind_time, 'm');
-plot(data.makeup_gain_pct, 'r');
+plot(data.output_gain_DB, 'r');
 plot(data.env.*100, 'k', "linewidth", 2);
 plot(data.gain.*50, 'k', "linestyle", '--');
 hold off;
@@ -53,9 +53,9 @@ grid;
 if stereo
   legend("in*100", "in*100", "out*100", "out*100", "threshold", "ratio", ...
     "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ...
-    "makeup", "env*100", "gain*50");
+    "out_gain", "env*100", "gain*50");
 else
   legend("in*100", "out*100", "threshold", "ratio", ...
     "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ...
-    "makeup", "env*100", "gain*50");
+    "out_gain", "env*100", "gain*50");
 end
diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 5af30a1ae..98a2a65ae 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -89,7 +89,7 @@ Param( AttackTime,     double,  wxT("AttackTime"),         0.2,     0.0001,  30.
 Param( ReleaseTime,    double,  wxT("ReleaseTime"),        1.0,     0.0001,  30.0,  2000.0 );
 Param( LookaheadTime,  double,  wxT("LookaheadTime"),      0.0,     0.0,     10.0,   200.0 );
 Param( LookbehindTime, double,  wxT("LookbehindTime"),     0.1,     0.0,     10.0,   200.0 );
-Param( MakeupGain,     double,  wxT("MakeupGain"),         0.0,     0.0,    100.0,     1.0 );
+Param( OutputGain,     double,  wxT("OutputGain"),         0.0,     0.0,     50.0,    10.0 );
 
 inline int ScaleToPrecision(double scale)
 {
@@ -548,7 +548,7 @@ EffectCompressor2::EffectCompressor2()
    mReleaseTime = DEF_ReleaseTime;          // seconds
    mLookaheadTime = DEF_LookaheadTime;
    mLookbehindTime = DEF_LookbehindTime;
-   mMakeupGainPct = DEF_MakeupGain;
+   mOutputGainDB = DEF_OutputGain;
 
    SetLinearEffectFlag(false);
 }
@@ -682,7 +682,7 @@ bool EffectCompressor2::DefineParams( ShuttleParams & S )
    S.SHUTTLE_PARAM(mReleaseTime, ReleaseTime);
    S.SHUTTLE_PARAM(mLookaheadTime, LookaheadTime);
    S.SHUTTLE_PARAM(mLookbehindTime, LookbehindTime);
-   S.SHUTTLE_PARAM(mMakeupGainPct, MakeupGain);
+   S.SHUTTLE_PARAM(mOutputGainDB, OutputGain);
 
    return true;
 }
@@ -700,7 +700,7 @@ bool EffectCompressor2::GetAutomationParameters(CommandParameters & parms)
    parms.Write(KEY_ReleaseTime, mReleaseTime);
    parms.Write(KEY_LookaheadTime, mLookaheadTime);
    parms.Write(KEY_LookbehindTime, mLookbehindTime);
-   parms.Write(KEY_MakeupGain, mMakeupGainPct);
+   parms.Write(KEY_OutputGain, mOutputGainDB);
 
    return true;
 }
@@ -718,7 +718,7 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms)
    ReadAndVerifyDouble(ReleaseTime);
    ReadAndVerifyDouble(LookaheadTime);
    ReadAndVerifyDouble(LookbehindTime);
-   ReadAndVerifyDouble(MakeupGain);
+   ReadAndVerifyDouble(OutputGain);
 
    mAlgorithm = Algorithm;
    mCompressBy = CompressBy;
@@ -731,7 +731,7 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms)
    mReleaseTime = ReleaseTime;
    mLookaheadTime = LookaheadTime;
    mLookbehindTime = LookbehindTime;
-   mMakeupGainPct = MakeupGain;
+   mOutputGainDB = OutputGain;
 
    return true;
 }
@@ -760,7 +760,7 @@ bool EffectCompressor2::Startup()
       mReleaseTime = DEF_ReleaseTime;          // seconds
       mLookaheadTime = DEF_LookaheadTime;
       mLookbehindTime = DEF_LookbehindTime;
-      mMakeupGainPct = DEF_MakeupGain;
+      mOutputGainDB = DEF_OutputGain;
 
       SaveUserPreset(GetCurrentSettingsGroup());
 
@@ -805,7 +805,6 @@ bool EffectCompressor2::Process()
 
       mProcStereo = range.size() > 1;
 
-      InitGainCalculation();
       mPreproc = InitPreprocessor(mSampleRate);
       mEnvelope = InitEnvelope(mSampleRate, mPipeline[0].capacity());
 
@@ -973,16 +972,15 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
          S.AddVariableText(XO("dB"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
 
-         /* i18n-hint: Make-up, i.e. correct for any reduction, rather than fabricate it.*/
-         S.AddVariableText(XO("Make-up Gain:"), true,
+         S.AddVariableText(XO("Output Gain:"), true,
             wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
-         ctrl = S.Name(XO("Make-up Gain"))
+         ctrl = S.Name(XO("Output Gain"))
             .Style(SliderTextCtrl::HORIZONTAL)
-            .AddSliderTextCtrl({}, DEF_MakeupGain, MAX_MakeupGain,
-               MIN_MakeupGain, ScaleToPrecision(SCL_MakeupGain),
-               &mMakeupGainPct);
+            .AddSliderTextCtrl({}, DEF_OutputGain, MAX_OutputGain,
+               MIN_OutputGain, ScaleToPrecision(SCL_OutputGain),
+               &mOutputGainDB);
          ctrl->SetMinTextboxWidth(textbox_width);
-         S.AddVariableText(XO("%"), true,
+         S.AddVariableText(XO("dB"), true,
             wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
       }
       S.EndMultiColumn();
@@ -1075,13 +1073,6 @@ bool EffectCompressor2::TransferDataFromWindow()
 
 // EffectCompressor2 implementation
 
-void EffectCompressor2::InitGainCalculation()
-{
-   mMakeupGainDB = mMakeupGainPct / 100.0 *
-      -(mThresholdDB * (1.0 - 1.0 / mRatio));
-   mMakeupGain = DB_TO_LINEAR(mMakeupGainDB);
-}
-
 double EffectCompressor2::CompressorGain(double env)
 {
    double kneeCond;
@@ -1096,13 +1087,13 @@ double EffectCompressor2::CompressorGain(double env)
    if(kneeCond < -mKneeWidthDB)
    {
       // Below threshold: only apply make-up gain
-      return mMakeupGain;
+      return DB_TO_LINEAR(mOutputGainDB);
    }
    else if(kneeCond >= mKneeWidthDB)
    {
       // Above threshold: apply compression and make-up gain
       return DB_TO_LINEAR(mThresholdDB +
-         (envDB - mThresholdDB) / mRatio + mMakeupGainDB - envDB);
+         (envDB - mThresholdDB) / mRatio + mOutputGainDB - envDB);
    }
    else
    {
@@ -1110,7 +1101,7 @@ double EffectCompressor2::CompressorGain(double env)
       return DB_TO_LINEAR(
          (1.0 / mRatio - 1.0)
          * pow(envDB - mThresholdDB + mKneeWidthDB / 2.0, 2)
-         / (2.0 * mKneeWidthDB) + mMakeupGainDB);
+         / (2.0 * mKneeWidthDB) + mOutputGainDB);
    }
 }
 
@@ -1500,7 +1491,7 @@ inline void EffectCompressor2::CompressSample(float env, size_t wp)
    float ReleaseTime = mReleaseTime;
    float LookaheadTime = mLookaheadTime;
    float LookbehindTime = mLookbehindTime;
-   float MakeupGainPct = mMakeupGainPct;
+   float OutputGainDB = mOutputGainDB;
 
    debugfile.write((char*)&ThresholdDB, sizeof(float));
    debugfile.write((char*)&Ratio, sizeof(float));
@@ -1509,7 +1500,7 @@ inline void EffectCompressor2::CompressSample(float env, size_t wp)
    debugfile.write((char*)&ReleaseTime, sizeof(float));
    debugfile.write((char*)&LookaheadTime, sizeof(float));
    debugfile.write((char*)&LookbehindTime, sizeof(float));
-   debugfile.write((char*)&MakeupGainPct, sizeof(float));
+   debugfile.write((char*)&OutputGainDB, sizeof(float));
    debugfile.write((char*)&mPipeline[0][0][wp], sizeof(float));
    if(mProcStereo)
       debugfile.write((char*)&mPipeline[0][1][wp], sizeof(float));
@@ -1641,10 +1632,9 @@ void EffectCompressor2::UpdateCompressorPlot()
        return;
    if(!IsInRange(mKneeWidthDB, MIN_KneeWidth, MAX_KneeWidth))
        return;
-   if(!IsInRange(mMakeupGainPct, MIN_MakeupGain, MAX_MakeupGain))
+   if(!IsInRange(mOutputGainDB, MIN_OutputGain, MAX_OutputGain))
        return;
 
-   InitGainCalculation();
    size_t xsize = plot->xdata.size();
    for(size_t i = 0; i < xsize; ++i)
       plot->ydata[i] = plot->xdata[i] +
@@ -1678,7 +1668,6 @@ void EffectCompressor2::UpdateResponsePlot()
    lookahead_size -= (lookahead_size > 0);
    ssize_t block_size = float(TAU_FACTOR) * (mAttackTime + 1.0) * plot_rate;
 
-   InitGainCalculation();
    preproc = InitPreprocessor(plot_rate, true);
    envelope = InitEnvelope(plot_rate, block_size, true);
 
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index de09581e8..f1be14686 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -209,7 +209,6 @@ public:
 
 private:
    // EffectCompressor2 implementation
-   void InitGainCalculation();
    double CompressorGain(double env);
    std::unique_ptr<SamplePreprocessor> InitPreprocessor(
       double rate, bool preview = false);
@@ -269,11 +268,9 @@ private:
    double    mReleaseTime;
    double    mLookaheadTime;
    double    mLookbehindTime;
-   double    mMakeupGainPct;
+   double    mOutputGainDB;
 
    // cached intermediate values
-   double mMakeupGain;
-   double mMakeupGainDB;
    size_t mLookaheadLength;
 
    static const size_t RESPONSE_PLOT_SAMPLES = 200;
diff --git a/tests/octave/compressor2_test.m b/tests/octave/compressor2_test.m
index 81c5783eb..46404c188 100644
--- a/tests/octave/compressor2_test.m
+++ b/tests/octave/compressor2_test.m
@@ -48,8 +48,7 @@ function y = env_PT1_asym(x, fs, t_a, t_r, gain = 0)
 end
 
 ## Compressor gain helper function
-function gain = comp_gain(env, thresh_DB, ratio, kneeW_DB, makeup)
-  makeupG_DB  = -thresh_DB * (1-1/ratio) * makeup / 100;
+function gain = comp_gain(env, thresh_DB, ratio, kneeW_DB, outG_DB)
   env_DB      = 20*log10(env);
   kneeCond_DB = 2*(env_DB-thresh_DB);
 
@@ -59,15 +58,15 @@ function gain = comp_gain(env, thresh_DB, ratio, kneeW_DB, makeup)
   withinKnee = (kneeCond_DB >= -kneeW_DB) & (kneeCond_DB < kneeW_DB);
 
   gain_DB = zeros(size(env));
-  gain_DB(belowKnee) = makeupG_DB;
+  gain_DB(belowKnee) = outG_DB;
   gain_DB(aboveKnee) = thresh_DB + ...
     (env_DB(aboveKnee) - thresh_DB) / ratio + ...
-    makeupG_DB - env_DB(aboveKnee);
+    outG_DB - env_DB(aboveKnee);
   # Prevent division by zero
   kneeW_DB(kneeW_DB==0) = 0.000001;
   gain_DB(withinKnee) = (1/ratio-1) * ...
     (env_DB(withinKnee) - thresh_DB + kneeW_DB/2).^2 / ...
-    (2*kneeW_DB) + makeupG_DB;
+    (2*kneeW_DB) + outG_DB;
 
   gain = 10.^(gain_DB/20);
 end
@@ -173,7 +172,7 @@ CURRENT_TEST = "Compressor2, mono compression PT1 - sinewave - asymetric attack
 x2 = sin(2*pi*300/fs*(1:1:20*fs)).';
 remove_all_tracks();
 x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
-aud_do("DynamicCompressor: Threshold=-6 Algorithm=1 CompressBy=0 Ratio=2.0 AttackTime=1.0 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0 MakeupGain=0\n");
+aud_do("DynamicCompressor: Threshold=-6 Algorithm=1 CompressBy=0 Ratio=2.0 AttackTime=1.0 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0 OutputGain=0\n");
 y = import_from_aud(1);
 
 do_test_equ(settled(y, fs, 1), ...
@@ -184,22 +183,22 @@ do_test_equ(settled(y, fs, 1), ...
 CURRENT_TEST = "Compressor2, mono asymmetric lookaround max";
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.1 KneeWidth=5 MakeupGain=50\n");
+aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.1 KneeWidth=5 OutputGain=1\n");
 y = import_from_aud(1);
 
 do_test_equ(settled(y, fs, 0.6), ...
   comp_gain(settled(env_PT1(lookaround_max(x, fs, 0.2, 0.1), fs, 0.3, 1), fs, 0.6), ...
-  -17, 1.2, 5, 50).*settled(x, fs, 0.6));
+  -17, 1.2, 5, 1).*settled(x, fs, 0.6));
 
 ## Test Compressor, mono lookaround RMS
 CURRENT_TEST = "Compressor2, mono asymmetric lookaround RMS";
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.2 KneeWidth=3 MakeupGain=80\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.2 KneeWidth=3 OutputGain=2\n");
 y = import_from_aud(1);
 
 do_test_equ(settled(y, fs, 2), ...
-  comp_gain(settled(env_PT1(lookaround_RMS(x, fs, 0.1, 0.2), fs, 1), fs, 2), -20, 3, 3, 80) ...
+  comp_gain(settled(env_PT1(lookaround_RMS(x, fs, 0.1, 0.2), fs, 1), fs, 2), -20, 3, 3, 2) ...
   .*settled(x, fs, 2));
 
 ## Test Compressor, mono lookaround max with selection
@@ -208,13 +207,13 @@ remove_all_tracks();
 x = export_to_aud(x1, fs);
 
 aud_do("Select: Start=2 End=5 Mode=Set\n");
-aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\n");
+aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 OutputGain=0.5\n");
 y = import_from_aud(1);
 x = x(2*fs+1:5*fs);
 
 do_test_equ(settled(y, fs, 0.1), ...
   comp_gain(settled(env_PT1(lookaround_max(x, fs, 0.2, 0.2), fs, 0.3, 1), fs, 0.1), ...
-  -17, 1.2, 5, 50).*settled(x, fs, 0.1));
+  -17, 1.2, 5, 0.5).*settled(x, fs, 0.1));
 
 ## Test Compressor, mono, ultra short attack time
 CURRENT_TEST = "Compressor2, mono, ultra short attack time";
@@ -287,20 +286,20 @@ do_test_equ(settled(y(:,2), fs, 1), ...
 CURRENT_TEST = "Compressor2, stereo lookaround max";
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\n");
+aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 OutputGain=1\n");
 y = import_from_aud(2);
 
 do_test_equ(settled(y, fs, 0.6), ...
   comp_gain(settled(env_PT1(lookaround_max(x, fs, 0.2, 0.2), fs, 0.3, 1), fs, 0.6), ...
-  -17, 1.2, 5, 50).*settled(x, fs, 0.6));
+  -17, 1.2, 5, 1).*settled(x, fs, 0.6));
 
 ## Test Compressor, stereo lookaround RMS
 CURRENT_TEST = "Compressor2, stereo lookaround RMS";
 remove_all_tracks();
 x = export_to_aud(x1, fs);
-aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.1 KneeWidth=3 CompressBy=1 MakeupGain=60\n");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.1 KneeWidth=3 CompressBy=1 OutputGain=1.3\n");
 y = import_from_aud(2);
 
 do_test_equ(settled(y, fs, 2.5), ...
-  comp_gain(settled(env_PT1(lookaround_RMS(x, fs, 0.1, 0.1), fs, 1), fs, 2.5), -20, 3, 3, 60) ...
+  comp_gain(settled(env_PT1(lookaround_RMS(x, fs, 0.1, 0.1), fs, 1), fs, 2.5), -20, 3, 3, 1.3) ...
   .*settled(x, fs, 2.5));

From 06f6aa123dabe785e4f9cb17175d717217b4a9c4 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sun, 28 Feb 2021 10:51:32 +0100
Subject: [PATCH 31/33] Add factory presets to dynamic compressor effect.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 57 +++++++++++++++++++++++++++++++++++++
 src/effects/Compressor2.h   |  2 ++
 2 files changed, 59 insertions(+)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index 98a2a65ae..e0da5115f 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -91,6 +91,29 @@ Param( LookaheadTime,  double,  wxT("LookaheadTime"),      0.0,     0.0,     10.
 Param( LookbehindTime, double,  wxT("LookbehindTime"),     0.1,     0.0,     10.0,   200.0 );
 Param( OutputGain,     double,  wxT("OutputGain"),         0.0,     0.0,     50.0,    10.0 );
 
+struct FactoryPreset
+{
+   const TranslatableString name;
+   int algorithm;
+   int compressBy;
+   bool stereoInd;
+   double thresholdDB;
+   double ratio;
+   double kneeWidthDB;
+   double attackTime;
+   double releaseTime;
+   double lookaheadTime;
+   double lookbehindTime;
+   double outputGainDB;
+};
+
+static const FactoryPreset FactoryPresets[] =
+{
+   { XO("Dynamic Reduction"), kEnvPT1, kAmplitude, false, -40, 2.5, 6, 0.3, 0.3, 0.5, 0.5, 23 },
+   { XO("Peak Reduction"), kEnvPT1, kAmplitude, false, -10, 10, 0, 0.001, 0.05, 0, 0, 0 },
+   { XO("Analog Limiter"), kEnvPT1, kAmplitude, false, -6, 100, 6, 0.0001, 0.0001, 0, 0, 0 }
+};
+
 inline int ScaleToPrecision(double scale)
 {
    return ceil(log10(scale));
@@ -736,6 +759,40 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms)
    return true;
 }
 
+RegistryPaths EffectCompressor2::GetFactoryPresets()
+{
+   RegistryPaths names;
+
+   for (size_t i = 0; i < WXSIZEOF(FactoryPresets); i++)
+      names.push_back( FactoryPresets[i].name.Translation() );
+
+   return names;
+}
+
+bool EffectCompressor2::LoadFactoryPreset(int id)
+{
+   if (id < 0 || id >= int(WXSIZEOF(FactoryPresets)))
+      return false;
+
+   const FactoryPreset* preset = &FactoryPresets[id];
+
+   mAlgorithm = preset->algorithm;
+   mCompressBy = preset->compressBy;
+   mStereoInd = preset->stereoInd;
+
+   mThresholdDB = preset->thresholdDB;
+   mRatio = preset->ratio;
+   mKneeWidthDB = preset->kneeWidthDB;
+   mAttackTime = preset->attackTime;
+   mReleaseTime = preset->releaseTime;
+   mLookaheadTime = preset->lookaheadTime;
+   mLookbehindTime = preset->lookbehindTime;
+   mOutputGainDB = preset->outputGainDB;
+
+   TransferDataToWindow();
+   return true;
+}
+
 // Effect implementation
 
 bool EffectCompressor2::CheckWhetherSkipEffect()
diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h
index f1be14686..c4c1159c9 100644
--- a/src/effects/Compressor2.h
+++ b/src/effects/Compressor2.h
@@ -197,6 +197,8 @@ public:
    bool DefineParams( ShuttleParams & S ) override;
    bool GetAutomationParameters(CommandParameters & parms) override;
    bool SetAutomationParameters(CommandParameters & parms) override;
+   RegistryPaths GetFactoryPresets() override;
+   bool LoadFactoryPreset(int id) override;
 
    // Effect implementation
 

From 9a22a6921123e46f1aaeee74e92745880f7a9d3e Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Fri, 5 Mar 2021 16:05:15 +0100
Subject: [PATCH 32/33] Disable Dynamic Compressor realtime support for now.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index e0da5115f..bc7eb0742 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -607,7 +607,7 @@ EffectType EffectCompressor2::GetType()
 bool EffectCompressor2::SupportsRealtime()
 {
 #if defined(EXPERIMENTAL_REALTIME_AUDACITY_EFFECTS)
-   return true;
+   return false;
 #else
    return false;
 #endif

From d7fea9475d0e1df18c03af300ca2aa3c64fc2393 Mon Sep 17 00:00:00 2001
From: Max Maisel <max.maisel@posteo.de>
Date: Sun, 16 May 2021 15:15:55 +0200
Subject: [PATCH 33/33] Remove removed header files from new cpp files.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
---
 src/effects/Compressor2.cpp    | 2 --
 src/widgets/Plot.cpp           | 2 --
 src/widgets/SliderTextCtrl.cpp | 2 --
 3 files changed, 6 deletions(-)

diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp
index bc7eb0742..745456c78 100644
--- a/src/effects/Compressor2.cpp
+++ b/src/effects/Compressor2.cpp
@@ -14,7 +14,6 @@
 *//*******************************************************************/
 
 
-#include "../Audacity.h" // for rint from configwin.h
 #include "Compressor2.h"
 
 #include <math.h>
@@ -24,7 +23,6 @@
 #include <wx/valgen.h>
 
 #include "../AColor.h"
-#include "../Internat.h"
 #include "../Prefs.h"
 #include "../ProjectFileManager.h"
 #include "../Shuttle.h"
diff --git a/src/widgets/Plot.cpp b/src/widgets/Plot.cpp
index fb1c1bea0..21a33e9c3 100644
--- a/src/widgets/Plot.cpp
+++ b/src/widgets/Plot.cpp
@@ -14,8 +14,6 @@
 *//*******************************************************************/
 
 
-#include "../Audacity.h"
-#include "audacity/Types.h"
 #include "Plot.h"
 #include "Ruler.h"
 #include "../AColor.h"
diff --git a/src/widgets/SliderTextCtrl.cpp b/src/widgets/SliderTextCtrl.cpp
index 837152d89..faa1af8ae 100644
--- a/src/widgets/SliderTextCtrl.cpp
+++ b/src/widgets/SliderTextCtrl.cpp
@@ -14,8 +14,6 @@
 *//*******************************************************************/
 
 
-#include "../Audacity.h"
-#include "audacity/Types.h"
 #include "SliderTextCtrl.h"
 
 #include <wx/defs.h>