1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-04-29 23:29:41 +02:00
audacity/src/effects/Compressor2.cpp
Max Maisel 1d7b143c91 Debugging helper code for Compressor2 effect.
Signed-off-by: Max Maisel <max.maisel@posteo.de>
2021-10-02 09:30:29 +02:00

1356 lines
42 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
Compressor2.cpp
Max Maisel
*******************************************************************//**
\class EffectCompressor2
\brief An Effect which reduces the dynamic level.
*//*******************************************************************/
#include "../Audacity.h" // for rint from configwin.h
#include "Compressor2.h"
#include <math.h>
#include <numeric>
#include <wx/intl.h>
#include <wx/valgen.h>
#include "../AColor.h"
#include "../Internat.h"
#include "../Prefs.h"
#include "../ProjectFileManager.h"
#include "../Shuttle.h"
#include "../ShuttleGui.h"
#include "../WaveTrack.h"
#include "../widgets/valnum.h"
#include "../widgets/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
#ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS
#include <fstream>
int buf_num;
#endif
enum kAlgorithms
{
kExpFit,
kEnvPT1,
nAlgos
};
static const ComponentInterfaceSymbol kAlgorithmStrings[nAlgos] =
{
{ XO("Exponential-Fit") },
{ XO("Analog Model (PT1)") }
};
enum kCompressBy
{
kAmplitude,
kRMS,
nBy
};
static const ComponentInterfaceSymbol kCompressByStrings[nBy] =
{
{ XO("peak amplitude") },
{ XO("RMS") }
};
// Define keys, defaults, minimums, and maximums for the effect parameters
//
// Name Type Key Def Min Max Scale
Param( Algorithm, int, wxT("Algorithm"), kEnvPT1, 0, nAlgos-1, 1 );
Param( CompressBy, int, wxT("CompressBy"), kAmplitude, 0, nBy-1, 1 );
Param( StereoInd, bool, wxT("StereoIndependent"), false, false, true, 1 );
Param( Threshold, double, wxT("Threshold"), -12.0, -60.0, -1.0, 1.0 );
Param( Ratio, double, wxT("Ratio"), 2.0, 1.1, 100.0, 10.0 );
Param( KneeWidth, double, wxT("KneeWidth"), 10.0, 0.0, 20.0, 10.0 );
Param( AttackTime, double, wxT("AttackTime"), 0.2, 0.00001, 30.0, 20000.0 );
Param( ReleaseTime, double, wxT("ReleaseTime"), 1.0, 0.00001, 30.0, 20000.0 );
Param( LookaheadTime, double, wxT("LookaheadTime"), 0.0, 0.0, 10.0, 200.0 );
Param( LookbehindTime, double, wxT("LookbehindTime"), 0.1, 0.0, 10.0, 200.0 );
Param( MakeupGain, double, wxT("MakeupGain"), 0.0, 0.0, 100.0, 1.0 );
Param( DryWet, double, wxT("DryWet"), 100.0, 0.0, 100.0, 1.0 );
inline int ScaleToPrecision(double scale)
{
return ceil(log10(scale));
}
BEGIN_EVENT_TABLE(EffectCompressor2, wxEvtHandler)
EVT_CHECKBOX(wxID_ANY, EffectCompressor2::OnUpdateUI)
EVT_CHOICE(wxID_ANY, EffectCompressor2::OnUpdateUI)
EVT_SLIDERTEXT(wxID_ANY, EffectCompressor2::OnUpdateUI)
END_EVENT_TABLE()
const ComponentInterfaceSymbol EffectCompressor2::Symbol
{ XO("Compressor v2") };
namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; }
SlidingRmsPreprocessor::SlidingRmsPreprocessor(size_t windowSize, float gain)
: mSum(0),
mGain(gain),
mWindow(windowSize, 0),
mPos(0),
mInsertCount(0)
{
}
float SlidingRmsPreprocessor::ProcessSample(float value)
{
return DoProcessSample(value * value);
}
float SlidingRmsPreprocessor::ProcessSample(float valueL, float valueR)
{
return DoProcessSample((valueL * valueL + valueR * valueR) / 2.0);
}
float SlidingRmsPreprocessor::DoProcessSample(float value)
{
if(mInsertCount > REFRESH_WINDOW_EVERY)
{
// Update RMS sum directly from the circle buffer every
// REFRESH_WINDOW_EVERY samples to avoid accumulation of rounding errors.
mWindow[mPos] = value;
Refresh();
}
else
{
// Calculate current level from root-mean-squared of
// circular buffer ("RMS").
mSum -= mWindow[mPos];
mWindow[mPos] = value;
mSum += mWindow[mPos];
++mInsertCount;
}
// Also refresh if there are severe rounding errors that
// caused mRMSSum to be negative.
if(mSum < 0)
Refresh();
mPos = (mPos + 1) % mWindow.size();
// Multiply by gain (usually two) to approximately correct peak level
// of standard audio (avoid clipping).
return mGain * sqrt(mSum/float(mWindow.size()));
}
void SlidingRmsPreprocessor::Refresh()
{
// Recompute the RMS sum periodically to prevent accumulation
// of rounding errors during long waveforms.
mSum = 0;
for(const auto& sample : mWindow)
mSum += sample;
mInsertCount = 0;
}
SlidingMaxPreprocessor::SlidingMaxPreprocessor(size_t windowSize)
: mWindow(windowSize, 0),
mMaxes(windowSize, 0),
mPos(0)
{
}
float SlidingMaxPreprocessor::ProcessSample(float value)
{
return DoProcessSample(fabs(value));
}
float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR)
{
return DoProcessSample((fabs(valueL) + fabs(valueR)) / 2.0);
}
float SlidingMaxPreprocessor::DoProcessSample(float value)
{
size_t oldHead = (mPos-1) % mWindow.size();
size_t currentHead = mPos;
size_t nextHead = (mPos+1) % mWindow.size();
mWindow[mPos] = value;
mMaxes[mPos] = std::max(value, mMaxes[oldHead]);
if(mPos % ((mWindow.size()+1)/2) == 0)
{
mMaxes[mPos] = mWindow[mPos];
for(size_t i = 1; i < mWindow.size(); ++i)
{
size_t pos1 = (mPos-i+mWindow.size()) % mWindow.size();
size_t pos2 = (mPos-i+mWindow.size()+1) % mWindow.size();
mMaxes[pos1] = std::max(mWindow[pos1], mMaxes[pos2]);
}
}
mPos = nextHead;
return std::max(mMaxes[currentHead], mMaxes[nextHead]);
}
EnvelopeDetector::EnvelopeDetector(size_t buffer_size)
: mPos(0),
mLookaheadBuffer(buffer_size, 0),
mProcessingBuffer(buffer_size, 0),
mProcessedBuffer(buffer_size, 0)
{
}
float EnvelopeDetector::ProcessSample(float value)
{
float retval = mProcessedBuffer[mPos];
mLookaheadBuffer[mPos++] = value;
if(mPos == mProcessingBuffer.size())
{
Follow();
mPos = 0;
mProcessedBuffer.swap(mProcessingBuffer);
mLookaheadBuffer.swap(mProcessingBuffer);
}
return retval;
}
size_t EnvelopeDetector::GetBlockSize() const
{
wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size());
return mLookaheadBuffer.size();
}
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)
{
mAttackFactor = exp(-1.0 / (rate * attackTime));
mReleaseFactor = exp(-1.0 / (rate * releaseTime));
}
void ExpFitEnvelopeDetector::Follow()
{
/*
"Follow"ing algorithm by Roger B. Dannenberg, taken from
Nyquist. His description follows. -DMM
Description: this is a sophisticated envelope follower.
The input is an envelope, e.g. something produced with
the AVG function. The purpose of this function is to
generate a smooth envelope that is generally not less
than the input signal. In other words, we want to "ride"
the peaks of the signal with a smooth function. The
algorithm is as follows: keep a current output value
(called the "value"). The value is allowed to increase
by at most rise_factor and decrease by at most fall_factor.
Therefore, the next value should be between
value * rise_factor and value * fall_factor. If the input
is in this range, then the next value is simply the input.
If the input is less than value * fall_factor, then the
next value is just value * fall_factor, which will be greater
than the input signal. If the input is greater than value *
rise_factor, then we compute a rising envelope that meets
the input value by working backwards in time, changing the
previous values to input / rise_factor, input / rise_factor^2,
input / rise_factor^3, etc. until this NEW envelope intersects
the previously computed values. There is only a limited buffer
in which we can work backwards, so if the NEW envelope does not
intersect the old one, then make yet another pass, this time
from the oldest buffered value forward, increasing on each
sample by rise_factor to produce a maximal envelope. This will
still be less than the input.
The value has a lower limit of floor to make sure value has a
reasonable positive value from which to begin an attack.
*/
wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size());
// First apply a peak detect with the requested release rate.
size_t buffer_size = mProcessingBuffer.size();
double env = mProcessedBuffer[buffer_size-1];
for(size_t i = 0; i < buffer_size; ++i)
{
env *= mReleaseFactor;
if(mProcessingBuffer[i] > env)
env = mProcessingBuffer[i];
mProcessingBuffer[i] = env;
}
// Preprocess lookahead buffer as well.
for(size_t i = 0; i < buffer_size; ++i)
{
env *= mReleaseFactor;
if(mLookaheadBuffer[i] > env)
env = mLookaheadBuffer[i];
mLookaheadBuffer[i] = env;
}
// Next do the same process in reverse direction to get the
// requested attack rate and preprocess lookahead buffer.
for(ssize_t i = buffer_size - 1; i >= 0; --i)
{
env *= mAttackFactor;
if(mLookaheadBuffer[i] < env)
mLookaheadBuffer[i] = env;
else
env = mLookaheadBuffer[i];
}
for(ssize_t i = buffer_size - 1; i >= 0; --i)
{
if(mProcessingBuffer[i] < env * mAttackFactor)
{
env *= mAttackFactor;
mProcessingBuffer[i] = env;
}
else if(mProcessingBuffer[i] > env)
// Intersected the previous envelope buffer, so we are finished
return;
else
; // Do nothing if we are on a plateau from peak look-around
}
}
Pt1EnvelopeDetector::Pt1EnvelopeDetector(
float rate, float attackTime, float releaseTime, size_t bufferSize,
bool correctGain)
: EnvelopeDetector(bufferSize)
{
// Approximate peak amplitude correction factor.
if(correctGain)
mGainCorrection = 1.0 + exp(attackTime / 30.0);
else
mGainCorrection = 1.0;
mAttackFactor = 1.0 / (attackTime * rate);
mReleaseFactor = 1.0 / (releaseTime * rate);
}
void Pt1EnvelopeDetector::Follow()
{
wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size());
// Simulate analog compressor with PT1 characteristic.
size_t buffer_size = mProcessingBuffer.size();
float level = mProcessedBuffer[buffer_size-1] / mGainCorrection;
for(size_t i = 0; i < buffer_size; ++i)
{
if(mProcessingBuffer[i] >= level)
level += mAttackFactor * (mProcessingBuffer[i] - level);
else
level += mReleaseFactor * (mProcessingBuffer[i] - level);
mProcessingBuffer[i] = level * mGainCorrection;
}
}
void PipelineBuffer::pad_to(size_t len, float value, bool stereo)
{
if(size < len)
{
size = len;
std::fill(mBlockBuffer[0].get() + trackSize,
mBlockBuffer[0].get() + size, value);
if(stereo)
std::fill(mBlockBuffer[1].get() + trackSize,
mBlockBuffer[1].get() + size, value);
}
}
void PipelineBuffer::swap(PipelineBuffer& other)
{
std::swap(trackPos, other.trackPos);
std::swap(trackSize, other.trackSize);
std::swap(size, other.size);
std::swap(mBlockBuffer[0], other.mBlockBuffer[0]);
std::swap(mBlockBuffer[1], other.mBlockBuffer[1]);
}
void PipelineBuffer::init(size_t capacity, bool stereo)
{
trackPos = 0;
trackSize = 0;
size = 0;
mCapacity = capacity;
mBlockBuffer[0].reinit(capacity);
std::fill(mBlockBuffer[0].get(), mBlockBuffer[0].get() + capacity, 0);
if(stereo)
{
mBlockBuffer[1].reinit(capacity);
std::fill(mBlockBuffer[1].get(), mBlockBuffer[1].get() + capacity, 0);
}
}
void PipelineBuffer::free()
{
mBlockBuffer[0].reset();
mBlockBuffer[1].reset();
}
EffectCompressor2::EffectCompressor2()
: mIgnoreGuiEvents(false)
{
mAlgorithm = DEF_Algorithm;
mCompressBy = DEF_CompressBy;
mStereoInd = DEF_StereoInd;
mThresholdDB = DEF_Threshold;
mRatio = DEF_Ratio; // positive number > 1.0
mKneeWidthDB = DEF_KneeWidth;
mAttackTime = DEF_AttackTime; // seconds
mReleaseTime = DEF_ReleaseTime; // seconds
mLookaheadTime = DEF_LookaheadTime;
mLookbehindTime = DEF_LookbehindTime;
mMakeupGainPct = DEF_MakeupGain;
mDryWetPct = DEF_DryWet;
SetLinearEffectFlag(false);
}
EffectCompressor2::~EffectCompressor2()
{
}
// ComponentInterface implementation
ComponentInterfaceSymbol EffectCompressor2::GetSymbol()
{
return Symbol;
}
TranslatableString EffectCompressor2::GetDescription()
{
return XO("Reduces the dynamic of one or more tracks");
}
ManualPageID EffectCompressor2::ManualPage()
{
return L"Compressor2";
}
// EffectDefinitionInterface implementation
EffectType EffectCompressor2::GetType()
{
return EffectTypeProcess;
}
// EffectClientInterface implementation
bool EffectCompressor2::DefineParams( ShuttleParams & S )
{
S.SHUTTLE_PARAM(mAlgorithm, Algorithm);
S.SHUTTLE_PARAM(mCompressBy, CompressBy);
S.SHUTTLE_PARAM(mStereoInd, StereoInd);
S.SHUTTLE_PARAM(mThresholdDB, Threshold);
S.SHUTTLE_PARAM(mRatio, Ratio);
S.SHUTTLE_PARAM(mKneeWidthDB, KneeWidth);
S.SHUTTLE_PARAM(mAttackTime, AttackTime);
S.SHUTTLE_PARAM(mReleaseTime, ReleaseTime);
S.SHUTTLE_PARAM(mLookaheadTime, LookaheadTime);
S.SHUTTLE_PARAM(mLookbehindTime, LookbehindTime);
S.SHUTTLE_PARAM(mMakeupGainPct, MakeupGain);
S.SHUTTLE_PARAM(mDryWetPct, DryWet);
return true;
}
bool EffectCompressor2::GetAutomationParameters(CommandParameters & parms)
{
parms.Write(KEY_Algorithm, mAlgorithm);
parms.Write(KEY_CompressBy, mCompressBy);
parms.Write(KEY_StereoInd, mStereoInd);
parms.Write(KEY_Threshold, mThresholdDB);
parms.Write(KEY_Ratio, mRatio);
parms.Write(KEY_KneeWidth, mKneeWidthDB);
parms.Write(KEY_AttackTime, mAttackTime);
parms.Write(KEY_ReleaseTime, mReleaseTime);
parms.Write(KEY_LookaheadTime, mLookaheadTime);
parms.Write(KEY_LookbehindTime, mLookbehindTime);
parms.Write(KEY_MakeupGain, mMakeupGainPct);
parms.Write(KEY_DryWet, mDryWetPct);
return true;
}
bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms)
{
ReadAndVerifyInt(Algorithm);
ReadAndVerifyInt(CompressBy);
ReadAndVerifyBool(StereoInd);
ReadAndVerifyDouble(Threshold);
ReadAndVerifyDouble(Ratio);
ReadAndVerifyDouble(KneeWidth);
ReadAndVerifyDouble(AttackTime);
ReadAndVerifyDouble(ReleaseTime);
ReadAndVerifyDouble(LookaheadTime);
ReadAndVerifyDouble(LookbehindTime);
ReadAndVerifyDouble(MakeupGain);
ReadAndVerifyDouble(DryWet);
mAlgorithm = Algorithm;
mCompressBy = CompressBy;
mStereoInd = StereoInd;
mThresholdDB = Threshold;
mRatio = Ratio;
mKneeWidthDB = KneeWidth;
mAttackTime = AttackTime;
mReleaseTime = ReleaseTime;
mLookaheadTime = LookaheadTime;
mLookbehindTime = LookbehindTime;
mMakeupGainPct = MakeupGain;
mDryWetPct = DryWet;
return true;
}
// 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;
mMakeupGainPct = DEF_MakeupGain;
mDryWetPct = DEF_DryWet;
SaveUserPreset(GetCurrentSettingsGroup());
gPrefs->Flush();
}
return true;
}
bool EffectCompressor2::Process()
{
// Iterate over each track
this->CopyInputTracks(); // Set up mOutputTracks.
bool bGoodResult = true;
AllocPipeline();
mProgressVal = 0;
for(auto track : mOutputTracks->Selected<WaveTrack>()
+ (mStereoInd ? &Track::Any : &Track::IsLeader))
{
// Get start and end times from track
// PRL: No accounting for multiple channels ?
double trackStart = track->GetStartTime();
double trackEnd = track->GetEndTime();
// Set the current bounds to whichever left marker is
// greater and whichever right marker is less:
mCurT0 = mT0 < trackStart? trackStart: mT0;
mCurT1 = mT1 > trackEnd? trackEnd: mT1;
// Get the track rate
mSampleRate = track->GetRate();
auto range = mStereoInd
? TrackList::SingletonRange(track)
: TrackList::Channels(track);
mProcStereo = range.size() > 1;
InitGainCalculation();
mPreproc = InitPreprocessor(mSampleRate);
mEnvelope = InitEnvelope(mSampleRate, mPipeline[0].capacity());
if(!ProcessOne(range))
{
// Processing failed -> abort
bGoodResult = false;
break;
}
}
this->ReplaceProcessedTracks(bGoodResult);
mPreproc.reset(nullptr);
mEnvelope.reset(nullptr);
FreePipeline();
return bGoodResult;
}
void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
{
S.SetBorder(10);
S.StartHorizontalLay(wxEXPAND, true);
{
PlotData* plot;
mGainPlot = S.MinSize( { 200, 200 } )
.AddPlot({}, -60, 0, -60, 0, XO("dB"), XO("dB"),
Ruler::LinearDBFormat, Ruler::LinearDBFormat);
plot = mGainPlot->GetPlotData(0);
plot->pen = std::unique_ptr<wxPen>(
safenew wxPen(AColor::WideEnvelopePen));
plot->xdata.resize(61);
plot->ydata.resize(61);
std::iota(plot->xdata.begin(), plot->xdata.end(), -60);
mResponsePlot = S.MinSize( { 200, 200 } )
.AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""),
Ruler::IntFormat, Ruler::RealFormat, 2);
plot = mResponsePlot->GetPlotData(0);
plot->pen = std::unique_ptr<wxPen>(
safenew wxPen(AColor::WideEnvelopePen));
plot->xdata = {0, RESPONSE_PLOT_STEP_START, RESPONSE_PLOT_STEP_START,
RESPONSE_PLOT_STEP_STOP, RESPONSE_PLOT_STEP_STOP, 5};
plot->ydata = {0, 0, 1, 1, 0, 0};
plot = 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.EndHorizontalLay();
S.SetBorder(5);
S.StartStatic(XO("Algorithm"));
{
S.StartMultiColumn(2, wxALIGN_LEFT);
{
S.SetStretchyCol(1);
wxChoice* ctrl = nullptr;
ctrl = S.Validator<wxGenericValidator>(&mAlgorithm)
.AddChoice(XO("Envelope Algorithm:"),
Msgids(kAlgorithmStrings, nAlgos),
mAlgorithm);
wxSize box_size = ctrl->GetMinSize();
int width = S.GetParent()->GetTextExtent(wxString::Format(
"%sxxxx", kAlgorithmStrings[nAlgos-1].Translation())).GetWidth();
box_size.SetWidth(width);
ctrl->SetMinSize(box_size);
ctrl = S.Validator<wxGenericValidator>(&mCompressBy)
.AddChoice(XO("Compress based on:"),
Msgids(kCompressByStrings, nBy),
mCompressBy);
ctrl->SetMinSize(box_size);
S.Validator<wxGenericValidator>(&mStereoInd)
.AddCheckBox(XO("Compress stereo channels independently"),
DEF_StereoInd);
}
S.EndMultiColumn();
}
S.EndStatic();
S.StartStatic(XO("Compressor"));
{
S.StartMultiColumn(3, wxEXPAND);
{
S.SetStretchyCol(1);
int textbox_width = S.GetParent()->GetTextExtent("0.000001").GetWidth();
SliderTextCtrl* ctrl = nullptr;
S.AddVariableText(XO("Threshold:"), true,
wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
ctrl = S.Name(XO("Threshold"))
.Style(SliderTextCtrl::HORIZONTAL)
.AddSliderTextCtrl({}, DEF_Threshold, MAX_Threshold,
MIN_Threshold, ScaleToPrecision(SCL_Threshold), &mThresholdDB);
ctrl->SetMinTextboxWidth(textbox_width);
S.AddVariableText(XO("dB"), true,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
S.AddVariableText(XO("Ratio:"), true,
wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
ctrl = S.Name(XO("Ratio"))
.Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
.AddSliderTextCtrl({}, DEF_Ratio, MAX_Ratio, MIN_Ratio,
ScaleToPrecision(SCL_Ratio), &mRatio);
/* i18n-hint: Unless your language has a different convention for ratios,
* like 8:1, leave as is.*/
ctrl->SetMinTextboxWidth(textbox_width);
S.AddVariableText(XO(":1"), true,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
S.AddVariableText(XO("Knee Width:"), true,
wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
ctrl = S.Name(XO("Knee Width"))
.Style(SliderTextCtrl::HORIZONTAL)
.AddSliderTextCtrl({}, DEF_KneeWidth, MAX_KneeWidth,
MIN_KneeWidth, ScaleToPrecision(SCL_KneeWidth),
&mKneeWidthDB);
ctrl->SetMinTextboxWidth(textbox_width);
S.AddVariableText(XO("dB"), true,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
S.AddVariableText(XO("Attack Time:"), true,
wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
ctrl = S.Name(XO("Attack Time"))
.Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
.AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime,
MIN_AttackTime, ScaleToPrecision(SCL_AttackTime),
&mAttackTime, SCL_AttackTime / 1000);
ctrl->SetMinTextboxWidth(textbox_width);
S.AddVariableText(XO("s"), true,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
S.AddVariableText(XO("Release Time:"), true,
wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
ctrl = S.Name(XO("Release Time"))
.Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
.AddSliderTextCtrl({}, DEF_ReleaseTime, MAX_ReleaseTime,
MIN_ReleaseTime, ScaleToPrecision(SCL_ReleaseTime),
&mReleaseTime, SCL_ReleaseTime / 1000);
ctrl->SetMinTextboxWidth(textbox_width);
S.AddVariableText(XO("s"), true,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
S.AddVariableText(XO("Lookahead Time:"), true,
wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
ctrl = S.Name(XO("Lookahead Time"))
.Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
.AddSliderTextCtrl({}, DEF_LookaheadTime, MAX_LookaheadTime,
MIN_LookaheadTime, ScaleToPrecision(SCL_LookaheadTime),
&mLookaheadTime);
ctrl->SetMinTextboxWidth(textbox_width);
S.AddVariableText(XO("s"), true,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
S.AddVariableText(XO("Hold Time:"), true,
wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
ctrl = S.Name(XO("Hold Time"))
.Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
.AddSliderTextCtrl({}, DEF_LookbehindTime, MAX_LookbehindTime,
MIN_LookbehindTime, ScaleToPrecision(SCL_LookbehindTime),
&mLookbehindTime);
ctrl->SetMinTextboxWidth(textbox_width);
S.AddVariableText(XO("s"), true,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
/* i18n-hint: Make-up, i.e. correct for any reduction, rather than fabricate it.*/
S.AddVariableText(XO("Make-up Gain:"), true,
wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
ctrl = S.Name(XO("Make-up Gain"))
.Style(SliderTextCtrl::HORIZONTAL)
.AddSliderTextCtrl({}, DEF_MakeupGain, MAX_MakeupGain,
MIN_MakeupGain, ScaleToPrecision(SCL_MakeupGain),
&mMakeupGainPct);
ctrl->SetMinTextboxWidth(textbox_width);
S.AddVariableText(XO("%"), true,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
S.AddVariableText(XO("Dry/Wet:"), true,
wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
ctrl = S.Name(XO("Dry/Wet"))
.Style(SliderTextCtrl::HORIZONTAL)
.AddSliderTextCtrl({}, DEF_DryWet, MAX_DryWet,
MIN_DryWet, ScaleToPrecision(SCL_DryWet),
&mDryWetPct);
ctrl->SetMinTextboxWidth(textbox_width);
S.AddVariableText(XO("%"), true,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
}
S.EndMultiColumn();
}
S.EndVerticalLay();
}
bool EffectCompressor2::TransferDataToWindow()
{
// Transferring data to window causes spurious UpdateUI events
// which would reset the UI values to the previous value.
// This guard lets the program ignore them.
mIgnoreGuiEvents = true;
if (!mUIParent->TransferDataToWindow())
{
mIgnoreGuiEvents = false;
return false;
}
UpdateUI();
mIgnoreGuiEvents = false;
return true;
}
bool EffectCompressor2::TransferDataFromWindow()
{
if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
{
return false;
}
return true;
}
// EffectCompressor2 implementation
void EffectCompressor2::InitGainCalculation()
{
mDryWet = mDryWetPct / 100.0;
mMakeupGainDB = mMakeupGainPct / 100.0 *
-(mThresholdDB * (1.0 - 1.0 / mRatio));
mMakeupGain = DB_TO_LINEAR(mMakeupGainDB);
}
double EffectCompressor2::CompressorGain(double env)
{
double kneeCond;
double envDB = LINEAR_TO_DB(env);
// envDB can become NaN is env is exactly zero.
// As solution, use a very low dB value to prevent NaN propagation.
if(isnan(envDB))
envDB = -200;
kneeCond = 2.0 * (envDB - mThresholdDB);
if(kneeCond < -mKneeWidthDB)
{
// Below threshold: only apply make-up gain
return mMakeupGain;
}
else if(kneeCond >= mKneeWidthDB)
{
// Above threshold: apply compression and make-up gain
return DB_TO_LINEAR(mThresholdDB +
(envDB - mThresholdDB) / mRatio + mMakeupGainDB - envDB);
}
else
{
// Within knee: apply interpolated compression and make-up gain
return DB_TO_LINEAR(
(1.0 / mRatio - 1.0)
* pow(envDB - mThresholdDB + mKneeWidthDB / 2.0, 2)
/ (2.0 * mKneeWidthDB) + mMakeupGainDB);
}
}
std::unique_ptr<SamplePreprocessor> EffectCompressor2::InitPreprocessor(
double rate, bool preview)
{
size_t window_size =
std::max(1, int(round((mLookaheadTime + mLookbehindTime) * rate)));
if(mCompressBy == kAmplitude)
return std::unique_ptr<SamplePreprocessor>(safenew
SlidingMaxPreprocessor(window_size));
else
return std::unique_ptr<SamplePreprocessor>(safenew
SlidingRmsPreprocessor(window_size, preview ? 1.0 : 2.0));
}
std::unique_ptr<EnvelopeDetector> EffectCompressor2::InitEnvelope(
double rate, size_t blockSize, bool preview)
{
if(mAlgorithm == kExpFit)
return std::unique_ptr<EnvelopeDetector>(safenew
ExpFitEnvelopeDetector(rate, mAttackTime, mReleaseTime, blockSize));
else
return std::unique_ptr<EnvelopeDetector>(safenew
Pt1EnvelopeDetector(rate, mAttackTime, mReleaseTime, blockSize,
!preview && mCompressBy != kAmplitude));
}
size_t EffectCompressor2::CalcBufferSize(size_t sampleRate)
{
size_t capacity;
mLookaheadLength =
std::max(0, int(round(mLookaheadTime * sampleRate)));
capacity = mLookaheadLength +
size_t(float(TAU_FACTOR) * (1.0 + mAttackTime) * sampleRate);
if(capacity < MIN_BUFFER_CAPACITY)
capacity = MIN_BUFFER_CAPACITY;
return capacity;
}
/// Get required buffer size for the largest whole track and allocate buffers.
/// This reduces the amount of allocations required.
void EffectCompressor2::AllocPipeline()
{
bool stereoTrackFound = false;
double maxSampleRate = 0;
size_t capacity;
mProcStereo = false;
for(auto track : mOutputTracks->Selected<WaveTrack>() + &Track::Any)
{
maxSampleRate = std::max(maxSampleRate, track->GetRate());
// There is a stereo track
if(track->IsLeader())
stereoTrackFound = true;
}
// Initiate a processing quad-buffer. This buffer will (most likely)
// be shorter than the length of the track being processed.
stereoTrackFound = stereoTrackFound && !mStereoInd;
capacity = CalcBufferSize(maxSampleRate);
for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
mPipeline[i].init(capacity, stereoTrackFound);
}
void EffectCompressor2::FreePipeline()
{
for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
mPipeline[i].free();
}
void EffectCompressor2::SwapPipeline()
{
#ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS
wxString blockname = wxString::Format("/tmp/blockbuf.%d.bin", buf_num);
std::cerr << "Writing to " << blockname << "\n" << std::flush;
std::fstream blockbuffer = std::fstream();
blockbuffer.open(blockname, std::ios::binary | std::ios::out);
for(size_t i = 0; i < PIPELINE_DEPTH; ++i) {
float val = mPipeline[i].trackSize;
blockbuffer.write((char*)&val, sizeof(float));
val = mPipeline[i].size;
blockbuffer.write((char*)&val, sizeof(float));
val = mPipeline[i].capacity();
blockbuffer.write((char*)&val, sizeof(float));
}
for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
blockbuffer.write((char*)mPipeline[i][0], mPipeline[i].capacity() * sizeof(float));
wxString envname = wxString::Format("/tmp/envbuf.%d.bin", buf_num++);
std::cerr << "Writing to " << envname << "\n" << std::flush;
std::fstream envbuffer = std::fstream();
envbuffer.open(envname, std::ios::binary | std::ios::out);
envbuffer.write((char*)mEnvelope->GetBuffer(0),
mEnvelope->GetBlockSize() * sizeof(float));
envbuffer.write((char*)mEnvelope->GetBuffer(1),
mEnvelope->GetBlockSize() * sizeof(float));
envbuffer.write((char*)mEnvelope->GetBuffer(2),
mEnvelope->GetBlockSize() * sizeof(float));
std::cerr << "PipelineState: ";
for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
std::cerr << !!mPipeline[i].size;
std::cerr << " ";
for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
std::cerr << !!mPipeline[i].trackSize;
std::cerr << "\ntrackSize: ";
for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
std::cerr << mPipeline[i].trackSize << " ";
std::cerr << "\ntrackPos: ";
for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
std::cerr << mPipeline[i].trackPos.as_size_t() << " ";
std::cerr << "\nsize: ";
for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
std::cerr << mPipeline[i].size << " ";
std::cerr << "\n" << std::flush;
#endif
++mProgressVal;
for(size_t i = 0; i < PIPELINE_DEPTH-1; ++i)
mPipeline[i].swap(mPipeline[i+1]);
#ifdef DEBUG_COMPRESSOR2_TRACE
std::cerr << "\n";
#endif
}
/// ProcessOne() takes a track, transforms it to bunch of buffer-blocks,
/// 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
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(mPipeline[0].size == 0)
FillPipeline();
else
ProcessPipeline();
// Increment s one blockfull of samples
pos += blockLen;
}
// 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();
}
while(PipelineHasData())
{
StorePipeline(range);
SwapPipeline();
DrainPipeline();
}
#ifdef DEBUG_COMPRESSOR2_TRACE
std::cerr << "StoreLastBlock\n" << std::flush;
#endif
StorePipeline(range);
// Return true because the effect processing succeeded ... unless cancelled
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)
{
// TODO: correct initial conditions
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::EnvelopeSample(PipelineBuffer& pbuf, size_t rp)
{
float preprocessed;
if(mProcStereo)
preprocessed = mPreproc->ProcessSample(pbuf[0][rp], pbuf[1][rp]);
else
preprocessed = mPreproc->ProcessSample(pbuf[0][rp]);
return mEnvelope->ProcessSample(preprocessed);
}
inline void EffectCompressor2::CompressSample(float env, size_t wp)
{
float gain = (1.0 - mDryWet) + CompressorGain(env) * mDryWet;
#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;
}
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;
}
void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt))
{
if(!mIgnoreGuiEvents)
TransferDataFromWindow();
UpdateUI();
}
void EffectCompressor2::UpdateUI()
{
UpdateCompressorPlot();
UpdateResponsePlot();
}
void EffectCompressor2::UpdateCompressorPlot()
{
PlotData* plot;
plot = mGainPlot->GetPlotData(0);
wxASSERT(plot->xdata.size() == plot->ydata.size());
InitGainCalculation();
size_t xsize = plot->xdata.size();
for(size_t i = 0; i < xsize; ++i)
plot->ydata[i] = plot->xdata[i] +
LINEAR_TO_DB(CompressorGain(DB_TO_LINEAR(plot->xdata[i])));
// XXX: accessibility but fails with TranslatableString required
// mGainPlot->SetName(wxString::Format(
// "Compressor gain reduction: %.1f dB", plot->ydata[xsize-1]));
mGainPlot->Refresh(false);
}
void EffectCompressor2::UpdateResponsePlot()
{
PlotData* plot;
plot = mResponsePlot->GetPlotData(1);
wxASSERT(plot->xdata.size() == plot->ydata.size());
std::unique_ptr<SamplePreprocessor> preproc;
std::unique_ptr<EnvelopeDetector> envelope;
float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME;
size_t lookahead_size =
std::max(0, int(round(mLookaheadTime * plot_rate)));
ssize_t block_size = float(TAU_FACTOR) * (mAttackTime + 1.0) * plot_rate;
preproc = InitPreprocessor(plot_rate, true);
envelope = InitEnvelope(plot_rate, block_size, true);
ssize_t step_start = RESPONSE_PLOT_STEP_START * plot_rate - lookahead_size;
ssize_t step_stop = RESPONSE_PLOT_STEP_STOP * plot_rate - lookahead_size;
ssize_t xsize = plot->xdata.size();
for(ssize_t i = -lookahead_size; i < 2*block_size; ++i)
{
if(i < step_start || i > step_stop)
envelope->ProcessSample(preproc->ProcessSample(0));
else
envelope->ProcessSample(preproc->ProcessSample(1));
}
for(ssize_t i = 0; i < xsize; ++i)
plot->ydata[i] = envelope->ProcessSample(preproc->ProcessSample(0));
mResponsePlot->Refresh(false);
}