~tenacity/tenacity

f06ac9bd969ed14ab4cd91b05e8bd5be676484ed — Panagiotis Vasilopoulos 10 months ago e1b9071 + d7fea94
Merge pull request #186 from mmmaisel/realtime-compressor2

Merge Max Maisel's branch that adds their Dynamic Compressor
effect to Tenacity. The commit has been already polished extensively,
as it was originally intended to be merged in Audacity.

Signed-off-by: Panagiotis Vasilopoulos <hello@alwayslivid.com>
Reference-to: https://github.com/tenacityteam/tenacity/pull/186
A scripts/debug/compressor2_buffers.m => scripts/debug/compressor2_buffers.m +25 -0
@@ 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

A scripts/debug/compressor2_trace.m => scripts/debug/compressor2_trace.m +61 -0
@@ 0,0 1,61 @@
## plot realtime trace data from Compressor2 effect

stereo = true;
bfile = fopen("/tmp/audio.out");

if stereo
  width = 14;
else
  width = 12;
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.output_gain_DB = raw_data(:,8);

if stereo
  data.in = horzcat(raw_data(:,9), raw_data(:,10));
  data.env = raw_data(:,11);
  data.gain = raw_data(:,12);
  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);
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.output_gain_DB, '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", ...
    "out_gain", "env*100", "gain*50");
else
  legend("in*100", "out*100", "threshold", "ratio", ...
    "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ...
    "out_gain", "env*100", "gain*50");
end

M src/CMakeLists.txt => src/CMakeLists.txt +6 -0
@@ 433,6 433,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


@@ 948,6 950,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


@@ 955,6 959,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

M src/ShuttleGui.cpp => src/ShuttleGui.cpp +55 -0
@@ 121,6 121,9 @@ for registering for changes.
#include "widgets/wxTextCtrlWrapper.h"
#include "AllThemeResources.h"

#include "widgets/Plot.h"
#include "widgets/SliderTextCtrl.h"

#if wxUSE_ACCESSIBILITY
#include "widgets/WindowAccessible.h"
#endif


@@ 613,6 616,31 @@ wxSlider * ShuttleGuiBase::AddSlider(
   return pSlider;
}

SliderTextCtrl* ShuttleGuiBase::AddSliderTextCtrl(
   const TranslatableString &Prompt, double pos, double Max, double Min,
   int precision, double* value, double scale, double offset)
{
   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, offset, 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)
{


@@ 750,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();

M src/ShuttleGui.h => src/ShuttleGui.h +11 -0
@@ 28,6 28,8 @@
class ChoiceSetting;

class wxArrayStringEx;
class Plot;
class SliderTextCtrl;


const int nMaxNestedSizers = 20;


@@ 263,6 265,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, double offset = 0);

   // Pass the same initValue to the sequence of calls to AddRadioButton and
   // AddRadioButtonToGroup.
   // The radio button is filled if selector == initValue


@@ 343,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);

A src/effects/Compressor2.cpp => src/effects/Compressor2.cpp +1765 -0
@@ 0,0 1,1765 @@
/**********************************************************************

  Audacity: A Digital Audio Editor

  Compressor2.cpp

  Max Maisel

*******************************************************************//**

\class EffectCompressor2
\brief An Effect which reduces the dynamic level.

*//*******************************************************************/


#include "Compressor2.h"

#include <math.h>
#include <numeric>

#include <wx/intl.h>
#include <wx/valgen.h>

#include "../AColor.h"
#include "../Prefs.h"
#include "../ProjectFileManager.h"
#include "../Shuttle.h"
#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"

//#define DEBUG_COMPRESSOR2_DUMP_BUFFERS
//#define DEBUG_COMPRESSOR2_ENV
//#define DEBUG_COMPRESSOR2_TRACE
//#define DEBUG_COMPRESSOR2_TRACE2

#if defined(DEBUG_COMPRESSOR2_DUMP_BUFFERS) or defined(DEBUG_COMPRESSOR2_TRACE2)
#include <fstream>
int buf_num;
std::fstream debugfile;
#endif

enum kAlgorithms
{
   kExpFit,
   kEnvPT1,
   nAlgos
};

static const ComponentInterfaceSymbol kAlgorithmStrings[nAlgos] =
{
   { XO("Exponential-Fit") },
   { XO("Analog Model") }
};

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,    20.0 );
Param( KneeWidth,      double,  wxT("KneeWidth"),         10.0,     0.0,     20.0,    10.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( 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));
}

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)
   EVT_SLIDERTEXT(wxID_ANY, EffectCompressor2::OnUpdateUI)
END_EVENT_TABLE()

const ComponentInterfaceSymbol EffectCompressor2::Symbol
{ XO("Dynamic Compressor") };

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);
}

void SlidingRmsPreprocessor::Reset(float level)
{
   mSum = (level / mGain) * (level / mGain) * float(mWindow.size());
   mPos = 0;
   mInsertCount = 0;
   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)
   {
      // 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(fabs(value));
}

float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR)
{
   return DoProcessSample((fabs(valueL) + fabs(valueR)) / 2.0);
}

void SlidingMaxPreprocessor::Reset(float value)
{
   mPos = 0;
   std::fill(mWindow.begin(), mWindow.end(), value);
   std::fill(mMaxes.begin(), mMaxes.end(), value);
}

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();
   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]);
}

EnvelopeDetector::EnvelopeDetector(size_t buffer_size)
   : mPos(0),
   mInitialCondition(0),
   mInitialBlockSize(0),
   mLookaheadBuffer(buffer_size, 0),
   mProcessingBuffer(buffer_size, 0),
   mProcessedBuffer(buffer_size, 0)
{
}

float EnvelopeDetector::AttackFactor()
{
   return 0;
}
float EnvelopeDetector::DecayFactor()
{
   return 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;
}

void EnvelopeDetector::CalcInitialCondition(float value)
{
}

size_t EnvelopeDetector::GetBlockSize() const
{
   wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
   wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size());
   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)
{
   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)
{
   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));
}

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, size_t bufferSize,
   bool correctGain)
   : EnvelopeDetector(bufferSize),
   mCorrectGain(correctGain)
{
   SetParams(rate, attackTime, releaseTime);
}

float Pt1EnvelopeDetector::AttackFactor()
{
   return mAttackFactor;
}
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)
{
   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);
   else
      mGainCorrection = 1.0;

   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)
{
   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()
{
   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;
   }
}

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);
   if(stereo)
      mBlockBuffer[1].reinit(capacity);
   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()
{
   mBlockBuffer[0].reset();
   mBlockBuffer[1].reset();
}

EffectCompressor2::EffectCompressor2()
   : mIgnoreGuiEvents(false),
   mAlgorithmCtrl(0),
   mPreprocCtrl(0),
   mAttackTimeCtrl(0),
   mLookaheadTimeCtrl(0)
{
   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;
   mOutputGainDB = DEF_OutputGain;

   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"Dynamic_Compressor";
}

// EffectDefinitionInterface implementation

EffectType EffectCompressor2::GetType()
{
   return EffectTypeProcess;
}

bool EffectCompressor2::SupportsRealtime()
{
#if defined(EXPERIMENTAL_REALTIME_AUDACITY_EFFECTS)
   return false;
#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);

   mProgressVal = 0;
#ifdef DEBUG_COMPRESSOR2_TRACE2
   debugfile.close();
   debugfile.open("/tmp/audio.out", std::ios::trunc | std::ios::out);
#endif

   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);
#ifdef DEBUG_COMPRESSOR2_TRACE2
   debugfile.close();
#endif
   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 )
{
   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(mOutputGainDB, OutputGain);

   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_OutputGain, mOutputGainDB);

   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(OutputGain);

   mAlgorithm = Algorithm;
   mCompressBy = CompressBy;
   mStereoInd = StereoInd;

   mThresholdDB = Threshold;
   mRatio = Ratio;
   mKneeWidthDB = KneeWidth;
   mAttackTime = AttackTime;
   mReleaseTime = ReleaseTime;
   mLookaheadTime = LookaheadTime;
   mLookbehindTime = LookbehindTime;
   mOutputGainDB = OutputGain;

   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()
{
   return false;
}

bool EffectCompressor2::Startup()
{
   wxString base = wxT("/Effects/Compressor2/");
   // 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;
      mOutputGainDB = DEF_OutputGain;

      SaveUserPreset(GetCurrentSettingsGroup());

      gPrefs->Flush();
   }
   return true;
}

bool EffectCompressor2::Process()
{
   // Iterate over each track
   this->CopyInputTracks(); // Set up mOutputTracks.
   bool bGoodResult = true;

   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))
   {
      // 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;

      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();
#ifdef DEBUG_COMPRESSOR2_TRACE2
   debugfile.close();
#endif
   return bGoodResult;
}

void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
{
   S.SetBorder(10);

   S.StartHorizontalLay(wxEXPAND, 1);
   {
      PlotData* plot;

      S.StartVerticalLay();
      S.AddVariableText(XO("Envelope dependent gain"), 0,
         wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
      mGainPlot = S.MinSize( { 400, 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));
      plot->xdata.resize(61);
      plot->ydata.resize(61);
      std::iota(plot->xdata.begin(), plot->xdata.end(), -60);

      S.EndVerticalLay();
      S.StartVerticalLay();

      S.AddVariableText(XO("Compressor step response"), 0,
         wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
      mResponsePlot = S.MinSize( { 400, 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>(
         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.1, 0.1, 1, 1, 0.1, 0.1};

      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);
      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.EndVerticalLay();
   }
   S.EndHorizontalLay();

   S.SetBorder(5);

   S.StartStatic(XO("Algorithm"));
   {
      wxSize box_size;
      int width;

      S.StartHorizontalLay(wxEXPAND, 1);
      S.StartVerticalLay(1);
      S.StartMultiColumn(2, wxALIGN_LEFT);
      {
         S.SetStretchyCol(1);

         mAlgorithmCtrl = S.Validator<wxGenericValidator>(&mAlgorithm)
            .AddChoice(XO("Envelope Algorithm:"),
               Msgids(kAlgorithmStrings, nAlgos),
               mAlgorithm);

         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.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.00001XX").GetWidth();
      SliderTextCtrl* ctrl = nullptr;

      S.StartHorizontalLay(wxEXPAND, true);
      S.StartVerticalLay(1);
      S.StartMultiColumn(3, wxEXPAND);
      {
         S.SetStretchyCol(1);

         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("Output Gain:"), true,
            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
         ctrl = S.Name(XO("Output Gain"))
            .Style(SliderTextCtrl::HORIZONTAL)
            .AddSliderTextCtrl({}, DEF_OutputGain, MAX_OutputGain,
               MIN_OutputGain, ScaleToPrecision(SCL_OutputGain),
               &mOutputGainDB);
         ctrl->SetMinTextboxWidth(textbox_width);
         S.AddVariableText(XO("dB"), 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:"), true,
            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
         mAttackTimeCtrl = S.Name(XO("Attack"))
            .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
            .AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime,
               MIN_AttackTime, ScaleToPrecision(SCL_AttackTime),
               &mAttackTime, SCL_AttackTime / 100, 0.033);
         mAttackTimeCtrl->SetMinTextboxWidth(textbox_width);
         S.AddVariableText(XO("s"), true,
            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);

         S.AddVariableText(XO("Release:"), true,
            wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
         ctrl = S.Name(XO("Release"))
            .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
            .AddSliderTextCtrl({}, DEF_ReleaseTime, MAX_ReleaseTime,
               MIN_ReleaseTime, ScaleToPrecision(SCL_ReleaseTime),
               &mReleaseTime, SCL_ReleaseTime / 100, 0.033);
         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);
         mLookaheadTimeCtrl = S.Name(XO("Lookahead Time"))
            .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
            .AddSliderTextCtrl({}, DEF_LookaheadTime, MAX_LookaheadTime,
               MIN_LookaheadTime, ScaleToPrecision(SCL_LookaheadTime),
               &mLookaheadTime, SCL_LookaheadTime / 10);
         mLookaheadTimeCtrl->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, SCL_LookbehindTime / 10);
         ctrl->SetMinTextboxWidth(textbox_width);
         S.AddVariableText(XO("s"), true,
            wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
      }
      S.EndMultiColumn();
      S.EndVerticalLay();
      S.EndHorizontalLay();
   }
   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;
}

bool EffectCompressor2::TransferDataFromWindow()
{
   if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
   {
      return false;
   }
   return true;
}

// EffectCompressor2 implementation

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 DB_TO_LINEAR(mOutputGainDB);
   }
   else if(kneeCond >= mKneeWidthDB)
   {
      // Above threshold: apply compression and make-up gain
      return DB_TO_LINEAR(mThresholdDB +
         (envDB - mThresholdDB) / mRatio + mOutputGainDB - 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) + mOutputGainDB);
   }
}

std::unique_ptr<SamplePreprocessor> EffectCompressor2::InitPreprocessor(
   double rate, bool preview)
{
   size_t window_size = CalcWindowLength(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(double sampleRate)
{
   size_t capacity;
   mLookaheadLength = CalcLookaheadLength(sampleRate);
   capacity = mLookaheadLength +
      size_t(float(TAU_FACTOR) * (1.0 + mAttackTime) * sampleRate);
   if(capacity < MIN_BUFFER_CAPACITY)
      capacity = MIN_BUFFER_CAPACITY;
   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()
{
   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::AllocRealtimePipeline()
{
   mLookaheadLength = CalcLookaheadLength(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)
      mPipeline[i].free();
}

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

   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,
/// 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;

#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

   bool first = true;
   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();

      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(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
         ProcessPipeline();

      // Increment s one blockfull of samples
      pos += blockLen;

      if(!UpdateProgress())
          return false;
   }

   // 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();
      if(!UpdateProgress())
          return false;
   }

   while(PipelineHasData())
   {
      StorePipeline(range);
      SwapPipeline();
      DrainPipeline();
      if(!UpdateProgress())
          return false;
   }
#ifdef DEBUG_COMPRESSOR2_TRACE
   std::cerr << "StoreLastBlock\n" << std::flush;
#endif
   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;
#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)
   {
      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()
{
#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);

   size_t length = mPipeline[PIPELINE_DEPTH-1].size;
   for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp)
   {
      if(rp < length)
         EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp);
      else
         EnvelopeSample(mPipeline[PIPELINE_DEPTH-1], rp % length);
   }
}

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)
         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::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)
{
   return mEnvelope->ProcessSample(PreprocSample(pbuf, rp));
}

inline void EffectCompressor2::CompressSample(float env, size_t wp)
{
   float gain = CompressorGain(env);

#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 OutputGainDB = mOutputGainDB;

   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*)&OutputGainDB, 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;
   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;

#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()
{
   for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
   {
      if(mPipeline[i].size != 0)
         return true;
   }
   return false;
}

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
         // TODO: correct end condition
         env = mEnvelope->ProcessSample(mPreproc->ProcessSample(0.0));
      CompressSample(env, wp);
   }
}

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)
   {
      // 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;
}

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)
      TransferDataFromWindow();
   UpdateUI();
}

void EffectCompressor2::UpdateUI()
{
   UpdateCompressorPlot();
   UpdateResponsePlot();
   if(mEnvelope.get() != nullptr)
      UpdateRealtimeParams();
}

void EffectCompressor2::UpdateCompressorPlot()
{
   PlotData* plot;
   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(mOutputGainDB, MIN_OutputGain, MAX_OutputGain))
       return;

   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])));

   mGainPlot->SetName(XO("Compressor gain reduction: %.1f dB").
      Format(plot->ydata[xsize-1]));
   mGainPlot->Refresh(false);
}

void EffectCompressor2::UpdateResponsePlot()
{
   PlotData* plot;
   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;

   size_t lookahead_size = CalcLookaheadLength(plot_rate);
   lookahead_size -= (lookahead_size > 0);
   ssize_t block_size = float(TAU_FACTOR) * (mAttackTime + 1.0) * plot_rate;

   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;

   ssize_t xsize = plot->xdata.size();
   for(ssize_t i = -lookahead_size; i < 2*block_size; ++i)
   {
      if(i < step_start || i > step_stop)
         envelope->ProcessSample(preproc->ProcessSample(0.1));
      else
         envelope->ProcessSample(preproc->ProcessSample(1));
   }

   for(ssize_t i = 0; i < xsize; ++i)
   {
      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);
}

void EffectCompressor2::UpdateRealtimeParams()
{
   std::lock_guard<std::mutex> guard(mRealtimeMutex);
   size_t window_size = CalcWindowLength(mSampleRate);
   mLookaheadLength = CalcLookaheadLength(mSampleRate);
   mPreproc->SetWindowSize(window_size);
   mEnvelope->SetParams(mSampleRate, mAttackTime, mReleaseTime);
}

A src/effects/Compressor2.h => src/effects/Compressor2.h +294 -0
@@ 0,0 1,294 @@
/**********************************************************************

  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 Plot;
class ShuttleGui;
class SliderTextCtrl;

class SamplePreprocessor
{
   public:
      virtual float ProcessSample(float value) = 0;
      virtual float ProcessSample(float valueL, float valueR) = 0;
      virtual void Reset(float value = 0) = 0;
      virtual void SetWindowSize(size_t windowSize) = 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);
      virtual void Reset(float value = 0);
      virtual void SetWindowSize(size_t windowSize);

      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);
      virtual void Reset(float value = 0);
      virtual void SetWindowSize(size_t windowSize);

   private:
      std::vector<float> mWindow;
      std::vector<float> mMaxes;
      size_t mPos;

      inline float DoProcessSample(float value);
};

class EnvelopeDetector
{
   public:
      EnvelopeDetector(size_t buffer_size);

      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; }

      virtual void Reset(float value = 0) = 0;
      virtual void SetParams(float sampleRate, float attackTime,
         float releaseTime) = 0;

      virtual float AttackFactor();
      virtual float DecayFactor();

   protected:
      size_t mPos;
      float mInitialCondition;
      size_t mInitialBlockSize;
      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,
         size_t buffer_size);

      virtual void Reset(float value);
      virtual void SetParams(float sampleRate, float attackTime,
         float releaseTime);

   private:
      double mAttackFactor;
      double mReleaseFactor;

      virtual void Follow();
};

class Pt1EnvelopeDetector : public EnvelopeDetector
{
   public:
      Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime,
         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();
      virtual float DecayFactor();

   private:
      bool mCorrectGain;
      double mGainCorrection;
      double mAttackFactor;
      double mReleaseFactor;

      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);
      void fill(float value, bool stereo);
      inline size_t capacity() const { return mCapacity; }
      void free();

   private:
      size_t mCapacity;
      Floats mBlockBuffer[2];
};

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;
   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;
   RegistryPaths GetFactoryPresets() override;
   bool LoadFactoryPreset(int id) 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
   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(double sampleRate);

   inline size_t CalcLookaheadLength(double rate);
   inline size_t CalcWindowLength(double rate);

   void AllocPipeline();
   void AllocRealtimePipeline();
   void FreePipeline();
   void SwapPipeline();
   bool ProcessOne(TrackIterRange<WaveTrack> range);
   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();
   void DrainPipeline();
   void StorePipeline(TrackIterRange<WaveTrack> range);

   bool UpdateProgress();
   void OnUpdateUI(wxCommandEvent & evt);
   void UpdateUI();
   void UpdateCompressorPlot();
   void UpdateResponsePlot();
   void UpdateRealtimeParams();

   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::mutex mRealtimeMutex;
   std::unique_ptr<SamplePreprocessor> mPreproc;
   std::unique_ptr<EnvelopeDetector> mEnvelope;

   int    mAlgorithm;
   int    mCompressBy;
   bool   mStereoInd;

   double    mThresholdDB;
   double    mRatio;
   double    mKneeWidthDB;
   double    mAttackTime;
   double    mReleaseTime;
   double    mLookaheadTime;
   double    mLookbehindTime;
   double    mOutputGainDB;

   // cached intermediate values
   size_t mLookaheadLength;

   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;

   bool mIgnoreGuiEvents;
   Plot* mGainPlot;
   Plot* mResponsePlot;
   wxChoice* mAlgorithmCtrl;
   wxChoice* mPreprocCtrl;
   SliderTextCtrl* mAttackTimeCtrl;
   SliderTextCtrl* mLookaheadTimeCtrl;

   DECLARE_EVENT_TABLE()
};

#endif

A src/widgets/Plot.cpp => src/widgets/Plot.cpp +134 -0
@@ 0,0 1,134 @@
/**********************************************************************

   Audacity: A Digital Audio Editor

   Plot.cpp

   Max Maisel

*******************************************************************//**

\class Plot
\brief A customizable generic plot widget.

*//*******************************************************************/


#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()

A src/widgets/Plot.h => src/widgets/Plot.h +58 -0
@@ 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

A src/widgets/SliderTextCtrl.cpp => src/widgets/SliderTextCtrl.cpp +167 -0
@@ 0,0 1,167 @@
/**********************************************************************

   Audacity: A Digital Audio Editor

   SliderTextCtrl.cpp

   Max Maisel

*******************************************************************//**

\class SliderTextCtrl
\brief A slider with connected text box.

*//*******************************************************************/


#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,
   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;
   m_int = style & INT;
   m_value = value;
   m_min = min;
   m_max = max;
   m_zero = -std::numeric_limits<double>::infinity();
   m_offset = offset;

   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 + m_offset);

      if(value <= 0.0)
         value = m_zero;
      else
         value = log10(value + m_offset);
      max = log10(max + m_offset);
   }

   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_offset);
   }
   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_offset;
         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()

A src/widgets/SliderTextCtrl.h => src/widgets/SliderTextCtrl.h +79 -0
@@ 0,0 1,79 @@
/**********************************************************************

   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, double offset = 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;
      double m_offset;
      wxString m_format;

      DECLARE_EVENT_TABLE()
};

#endif

A tests/octave/.gitignore => tests/octave/.gitignore +1 -0
@@ 0,0 1,1 @@
*.wav

A tests/octave/compressor2_test.m => tests/octave/compressor2_test.m +305 -0
@@ 0,0 1,305 @@
## 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, outG_DB)
  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) = outG_DB;
  gain_DB(aboveKnee) = thresh_DB + ...
    (env_DB(aboveKnee) - thresh_DB) / ratio + ...
    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) + outG_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("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.
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("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), ...
  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("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
# 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("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), ...
  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("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), ...
  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("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), ...
  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("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, 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 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, 2) ...
  .*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("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, 0.5).*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("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
# (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("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), ...
  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("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), ...
  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("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), ...
  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("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), ...
  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("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, 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 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, 1.3) ...
  .*settled(x, fs, 2.5));

M tests/octave/loudness_test.m => tests/octave/loudness_test.m +24 -71
@@ 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);

M tests/octave/run_test.m => tests/octave/run_test.m +32 -6
@@ 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;