1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-04-29 15:19:44 +02:00

Implement ExpFit and Pt1 envelope detectors including preview.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
This commit is contained in:
Max Maisel 2020-02-22 10:15:16 +01:00
parent 61e88b39ab
commit 94e21d8f1c
2 changed files with 209 additions and 8 deletions

View File

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

View File

@ -67,6 +67,50 @@ class SlidingMaxPreprocessor : public SamplePreprocessor
inline float DoProcessSample(float value);
};
class EnvelopeDetector
{
public:
EnvelopeDetector(size_t buffer_size);
float ProcessSample(float value);
size_t GetBlockSize() const;
protected:
static const int TAU_FACTOR = 5;
size_t mPos;
std::vector<float> mLookaheadBuffer;
std::vector<float> mProcessingBuffer;
std::vector<float> mProcessedBuffer;
virtual void Follow() = 0;
};
class ExpFitEnvelopeDetector : public EnvelopeDetector
{
public:
ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime);
private:
double mAttackFactor;
double mReleaseFactor;
virtual void Follow();
};
class Pt1EnvelopeDetector : public EnvelopeDetector
{
public:
Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime,
bool correctGain = true);
private:
double mGainCorrection;
double mAttackFactor;
double mReleaseFactor;
virtual void Follow();
};
class EffectCompressor2 final : public Effect
{
public: