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

Implement compressor2 offline processing.

Signed-off-by: Max Maisel <max.maisel@posteo.de>
This commit is contained in:
Max Maisel 2020-05-27 16:39:19 +02:00
parent 94e21d8f1c
commit 6941c66de8
2 changed files with 434 additions and 26 deletions

View File

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

View File

@ -75,8 +75,6 @@ class EnvelopeDetector
float ProcessSample(float value);
size_t GetBlockSize() const;
protected:
static const int TAU_FACTOR = 5;
size_t mPos;
std::vector<float> mLookaheadBuffer;
std::vector<float> mProcessingBuffer;
@ -88,7 +86,8 @@ class EnvelopeDetector
class ExpFitEnvelopeDetector : public EnvelopeDetector
{
public:
ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime);
ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime,
size_t buffer_size = 0);
private:
double mAttackFactor;
@ -101,7 +100,7 @@ class Pt1EnvelopeDetector : public EnvelopeDetector
{
public:
Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime,
bool correctGain = true);
size_t buffer_size = 0, bool correctGain = true);
private:
double mGainCorrection;
@ -111,6 +110,27 @@ class Pt1EnvelopeDetector : public EnvelopeDetector
virtual void Follow();
};
struct PipelineBuffer
{
public:
sampleCount trackPos;
size_t trackSize;
size_t size;
inline float* operator[](size_t idx)
{ return mBlockBuffer[idx].get(); }
void pad_to(size_t len, float value, bool stereo);
void swap(PipelineBuffer& other);
void init(size_t size, bool stereo);
inline size_t capacity() const { return mCapacity; }
void free();
private:
size_t mCapacity;
Floats mBlockBuffer[2];
};
class EffectCompressor2 final : public Effect
{
public:
@ -148,6 +168,24 @@ private:
// EffectCompressor2 implementation
void InitGainCalculation();
double CompressorGain(double env);
std::unique_ptr<SamplePreprocessor> InitPreprocessor(
double rate, bool preview = false);
std::unique_ptr<EnvelopeDetector> InitEnvelope(
double rate, size_t blockSize = 0, bool preview = false);
size_t CalcBufferSize(size_t sampleRate);
void AllocPipeline();
void FreePipeline();
void SwapPipeline();
bool ProcessOne(TrackIterRange<WaveTrack> range);
bool LoadPipeline(TrackIterRange<WaveTrack> range, size_t len);
void FillPipeline();
void ProcessPipeline();
inline float EnvelopeSample(PipelineBuffer& pbuf, size_t rp);
inline void CompressSample(float env, size_t wp);
bool PipelineHasData();
void DrainPipeline();
void StorePipeline(TrackIterRange<WaveTrack> range);
bool UpdateProgress();
void OnUpdateUI(wxCommandEvent & evt);
@ -155,6 +193,21 @@ private:
void UpdateCompressorPlot();
void UpdateResponsePlot();
static const int TAU_FACTOR = 5;
static const size_t MIN_BUFFER_CAPACITY = 1048576; // 1MB
static const size_t PIPELINE_DEPTH = 4;
PipelineBuffer mPipeline[PIPELINE_DEPTH];
double mCurT0;
double mCurT1;
double mProgressVal;
double mTrackLen;
bool mProcStereo;
std::unique_ptr<SamplePreprocessor> mPreproc;
std::unique_ptr<EnvelopeDetector> mEnvelope;
int mAlgorithm;
int mCompressBy;
bool mStereoInd;
@ -170,8 +223,10 @@ private:
double mDryWetPct;
// cached intermediate values
double mDryWet;
double mMakeupGain;
double mMakeupGainDB;
size_t mLookaheadLength;
static const size_t RESPONSE_PLOT_SAMPLES = 200;
static const size_t RESPONSE_PLOT_TIME = 5;