diff --git a/scripts/debug/compressor2_buffers.m b/scripts/debug/compressor2_buffers.m new file mode 100644 index 000000000..f20c85b12 --- /dev/null +++ b/scripts/debug/compressor2_buffers.m @@ -0,0 +1,25 @@ +%% Debug Compressor v2 pipeline buffers +buffer_ids = [1,2,3,4,5]; +prefix = '/tmp'; + +figure(1); +for k = 1:length(buffer_ids) + subplot(length(buffer_ids), 1, k) + bfile = fopen(sprintf('%s/envbuf.%d.bin', prefix, buffer_ids(k))); + env = fread(bfile, 'float').'; + bfile = fopen(sprintf('%s/blockbuf.%d.bin', prefix, buffer_ids(k))); + block_raw = fread(bfile, 'float').'; + + sizes = reshape(block_raw(1:12), 3, 4); + capacity = (1:4).*sizes(3,:); + track_size = horzcat(0, capacity(1:3)) + sizes(1,:); + block = block_raw(13:end); + + plot(block, 'b', 'linewidth', 3); + hold on; + plot(circshift(env, length(env)/3), 'r'); + stem(capacity, ones(1, length(capacity)), 'g'); + stem(track_size, 1.5.*ones(1, length(capacity)), 'b'); + ylim([-2 2]); + hold off; +end \ No newline at end of file diff --git a/scripts/debug/compressor2_trace.m b/scripts/debug/compressor2_trace.m new file mode 100644 index 000000000..f7b2721d8 --- /dev/null +++ b/scripts/debug/compressor2_trace.m @@ -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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fd01514b6..ed9884fa4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/ShuttleGui.cpp b/src/ShuttleGui.cpp index 82ab555c5..48f5e78b2 100644 --- a/src/ShuttleGui.cpp +++ b/src/ShuttleGui.cpp @@ -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(); diff --git a/src/ShuttleGui.h b/src/ShuttleGui.h index d35d928cf..5bd72d9a8 100644 --- a/src/ShuttleGui.h +++ b/src/ShuttleGui.h @@ -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); diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp new file mode 100644 index 000000000..745456c78 --- /dev/null +++ b/src/effects/Compressor2.cpp @@ -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 +#include + +#include +#include + +#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 +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 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() + + (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( + 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( + 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( + 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(&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(&mCompressBy) + .AddChoice(XO("Compress based on:"), + Msgids(kCompressByStrings, nBy), + mCompressBy); + mPreprocCtrl->SetMinSize(box_size); + } + S.EndMultiColumn(); + S.EndVerticalLay(); + S.EndHorizontalLay(); + + S.Validator(&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 EffectCompressor2::InitPreprocessor( + double rate, bool preview) +{ + size_t window_size = CalcWindowLength(rate); + if(mCompressBy == kAmplitude) + return std::unique_ptr(safenew + SlidingMaxPreprocessor(window_size)); + else + return std::unique_ptr(safenew + SlidingRmsPreprocessor(window_size, preview ? 1.0 : 2.0)); +} + +std::unique_ptr EffectCompressor2::InitEnvelope( + double rate, size_t blockSize, bool preview) +{ + if(mAlgorithm == kExpFit) + return std::unique_ptr(safenew + ExpFitEnvelopeDetector(rate, mAttackTime, mReleaseTime, blockSize)); + else + return std::unique_ptr(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() + &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 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 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 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 preproc; + std::unique_ptr 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 guard(mRealtimeMutex); + size_t window_size = CalcWindowLength(mSampleRate); + mLookaheadLength = CalcLookaheadLength(mSampleRate); + mPreproc->SetWindowSize(window_size); + mEnvelope->SetParams(mSampleRate, mAttackTime, mReleaseTime); +} diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h new file mode 100644 index 000000000..c4c1159c9 --- /dev/null +++ b/src/effects/Compressor2.h @@ -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 +#include +#include +#include +#include +#include + +#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 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 mWindow; + std::vector 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 mLookaheadBuffer; + std::vector mProcessingBuffer; + std::vector 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 InitPreprocessor( + double rate, bool preview = false); + std::unique_ptr 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 range); + bool LoadPipeline(TrackIterRange 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 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 mPreproc; + std::unique_ptr 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 diff --git a/src/widgets/Plot.cpp b/src/widgets/Plot.cpp new file mode 100644 index 000000000..21a33e9c3 --- /dev/null +++ b/src/widgets/Plot.cpp @@ -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 +#include +#include + +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(safenew Ruler); + m_xruler->SetOrientation(wxHORIZONTAL); + m_xruler->SetFormat(static_cast(xformat)); + m_xruler->SetUnits(xlabel); + m_xruler->SetFlip(true); + + m_yruler = std::unique_ptr(safenew Ruler); + m_yruler->SetOrientation(wxVERTICAL); + m_yruler->SetFormat(static_cast(yformat)); + m_yruler->SetUnits(ylabel); +} + +void Plot::OnPaint(wxPaintEvent & evt) +{ + wxPaintDC dc(this); + + int width, height; + GetSize(&width, &height); + +#if defined(__WXMSW__) + dc.Clear(); +#endif + + // Ruler + int w = 0; + int h = 0; + + m_xruler->SetBounds(0, 0, width, height); + m_xruler->SetRange(m_xmin, m_xmax); + m_xruler->GetMaxSize(NULL, &h); + + m_yruler->SetBounds(0, 0, width, height); + m_yruler->SetRange(m_ymax, m_ymin); + m_yruler->GetMaxSize(&w, NULL); + + m_xruler->SetBounds(w, height - h, width, height); + m_yruler->SetBounds(0, 0, w, height - h); + + m_xruler->SetTickColour( theTheme.Colour( clrGraphLabels )); + m_yruler->SetTickColour( theTheme.Colour( clrGraphLabels )); + + wxRect border; + border.x = w; + border.y = 0; + border.width = width - w; + border.height = height - h + 1; + + dc.SetBrush(*wxWHITE_BRUSH); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(border); + + m_xruler->DrawGrid(dc, border.height, true, true, border.x, border.y); + m_yruler->DrawGrid(dc, border.width, true, true, border.x, border.y); + + for(const auto& plot : m_plots) + { + wxASSERT(plot.xdata.size() == plot.ydata.size()); + if(plot.xdata.size() == 0) + continue; + dc.SetPen(*plot.pen); + + size_t xsize = plot.xdata.size(); + for(size_t i = 1; i < xsize; ++i) + { + AColor::Line(dc, + XToScreen(plot.xdata[i-1], border), + YToScreen(plot.ydata[i-1], border), + XToScreen(plot.xdata[i], border), + YToScreen(plot.ydata[i], border)); + } + } + + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen(*wxBLACK_PEN); + dc.DrawRectangle(border); + m_xruler->Draw(dc); + m_yruler->Draw(dc); +} + +void Plot::OnSize(wxSizeEvent & evt) +{ + Refresh(false); +} + +int Plot::XToScreen(float x, wxRect& rect) +{ + return rect.x + lrint((x-m_xmin)*rect.width/(m_xmax-m_xmin)); +} + +int Plot::YToScreen(float y, wxRect& rect) +{ + return rect.y + rect.height - lrint((y-m_ymin)*rect.height/(m_ymax-m_ymin)); +} + +BEGIN_EVENT_TABLE(Plot, wxPanelWrapper) + EVT_PAINT(Plot::OnPaint) + EVT_SIZE(Plot::OnSize) +END_EVENT_TABLE() diff --git a/src/widgets/Plot.h b/src/widgets/Plot.h new file mode 100644 index 000000000..86253f956 --- /dev/null +++ b/src/widgets/Plot.h @@ -0,0 +1,58 @@ +/********************************************************************** + + Audacity: A Digital Audio Editor + + Plot.h + + Max Maisel + + This class is a generic plot. + +**********************************************************************/ + +#ifndef __AUDACITY_PLOT__ +#define __AUDACITY_PLOT__ + +#include "wxPanelWrapper.h" // to inherit + +#include "MemoryX.h" + +class Ruler; + +struct PlotData +{ + std::unique_ptr pen; + std::vector xdata; + std::vector 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 m_plots; + std::unique_ptr m_xruler, m_yruler; + + int XToScreen(float x, wxRect& rect); + int YToScreen(float y, wxRect& rect); + + DECLARE_EVENT_TABLE() +}; + +#endif diff --git a/src/widgets/SliderTextCtrl.cpp b/src/widgets/SliderTextCtrl.cpp new file mode 100644 index 000000000..faa1af8ae --- /dev/null +++ b/src/widgets/SliderTextCtrl.cpp @@ -0,0 +1,167 @@ +/********************************************************************** + + Audacity: A Digital Audio Editor + + SliderTextCtrl.cpp + + Max Maisel + +*******************************************************************//** + +\class SliderTextCtrl +\brief A slider with connected text box. + +*//*******************************************************************/ + + +#include "SliderTextCtrl.h" + +#include +#include +#include +#include +#include +#include + +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::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 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() diff --git a/src/widgets/SliderTextCtrl.h b/src/widgets/SliderTextCtrl.h new file mode 100644 index 000000000..b2c0d15da --- /dev/null +++ b/src/widgets/SliderTextCtrl.h @@ -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 diff --git a/tests/octave/.gitignore b/tests/octave/.gitignore new file mode 100644 index 000000000..d8dd7532a --- /dev/null +++ b/tests/octave/.gitignore @@ -0,0 +1 @@ +*.wav diff --git a/tests/octave/compressor2_test.m b/tests/octave/compressor2_test.m new file mode 100644 index 000000000..46404c188 --- /dev/null +++ b/tests/octave/compressor2_test.m @@ -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)); diff --git a/tests/octave/loudness_test.m b/tests/octave/loudness_test.m index 43a9ea74d..ac53616c4 100644 --- a/tests/octave/loudness_test.m +++ b/tests/octave/loudness_test.m @@ -125,20 +125,12 @@ end ## Test Loudness LUFS mode: block to short and all silent CURRENT_TEST = "Loudness LUFS mode, short silent block"; fs= 44100; -x = zeros(ceil(fs*0.35), 2); -audiowrite(TMP_FILENAME, x, fs); -if EXPORT_TEST_SIGNALS - audiowrite(cstrcat(pwd(), "/Loudness-LUFS-silence-test.wav"), x, fs); -end +x1 = zeros(ceil(fs*0.35), 2); remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs, "Loudness-LUFS-silence-test.wav"); aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=1 NormalizeTo=0 StereoIndependent=0\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(2); do_test_equ(y, x, "identity"); ## Test Loudness LUFS mode: stereo dependent @@ -146,76 +138,50 @@ CURRENT_TEST = "Loudness LUFS mode, keep DC and stereo balance"; randn("seed", 1); # Include some silence in the test signal to test loudness gating # and vary the overall loudness over time. -x = [0.1*randn(15*fs, 2).', zeros(5*fs, 2).', 0.1*randn(15*fs, 2).'].'; -x(:,1) = x(:,1) .* sin(2*pi/fs/35*(1:1:35*fs)).' .* 1.2; -x(:,2) = x(:,2) .* sin(2*pi/fs/35*(1:1:35*fs)).'; -audiowrite(TMP_FILENAME, x, fs); -if EXPORT_TEST_SIGNALS - audiowrite(cstrcat(pwd(), "/Loudness-LUFS-stereo-test.wav"), x, fs); -end +x1 = [0.1*randn(15*fs, 2).', zeros(5*fs, 2).', 0.1*randn(15*fs, 2).'].'; +x1(:,1) = x1(:,1) .* sin(2*pi/fs/35*(1:1:35*fs)).' .* 1.2; +x1(:,2) = x1(:,2) .* sin(2*pi/fs/35*(1:1:35*fs)).'; remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs, "Loudness-LUFS-stereo-test.wav"); aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=1 NormalizeTo=0 StereoIndependent=0\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(2); do_test_equ(calc_LUFS(y, fs), -23, "loudness", LUFS_epsilon); do_test_neq(calc_LUFS(y(:,1), fs), calc_LUFS(y(:,2), fs), "stereo balance", 1); ## Test Loudness LUFS mode, stereo independent CURRENT_TEST = "Loudness LUFS mode, stereo independence"; -audiowrite(TMP_FILENAME, x, fs); remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs); aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=0 NormalizeTo=0 StereoIndependent=1\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(2); # Independently processed stereo channels have half the target loudness. do_test_equ(calc_LUFS(y(:,1), fs), -26, "channel 1 loudness", LUFS_epsilon); do_test_equ(calc_LUFS(y(:,2), fs), -26, "channel 2 loudness", LUFS_epsilon); ## Test Loudness LUFS mode: mono as mono CURRENT_TEST = "Test Loudness LUFS mode: mono as mono"; -x = x(:,1); -audiowrite(TMP_FILENAME, x, fs); -if EXPORT_TEST_SIGNALS - audiowrite(cstrcat(pwd(), "/Loudness-LUFS-mono-test.wav"), x, fs); -end +x1 = x1(:,1); remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs, "Loudness-LUFS-mono-test.wav"); aud_do("LoudnessNormalization: LUFSLevel=-26 DualMono=0 NormalizeTo=0 StereoIndependent=1\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=1\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(1); do_test_equ(calc_LUFS(y, fs), -26, "loudness", LUFS_epsilon); ## Test Loudness LUFS mode: mono as dual-mono CURRENT_TEST = "Test Loudness LUFS mode: mono as dual-mono"; -audiowrite(TMP_FILENAME, x, fs); - remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs); aud_do("LoudnessNormalization: LUFSLevel=-26 DualMono=1 NormalizeTo=0 StereoIndependent=0\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=1\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(1); # This shall be 3 LU quieter as it is compared to strict spec. do_test_equ(calc_LUFS(y, fs), -29, "loudness", LUFS_epsilon); ## Test Loudness LUFS mode: multi-rate project CURRENT_TEST = "Test Loudness LUFS mode: multi-rate project"; -audiowrite(TMP_FILENAME, x, fs); +audiowrite(TMP_FILENAME, x1, fs); +x = audioread(TMP_FILENAME); remove_all_tracks(); aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); @@ -228,6 +194,7 @@ audiowrite(TMP_FILENAME, x1, fs1); if EXPORT_TEST_SIGNALS audiowrite(cstrcat(pwd(), "/Loudness-LUFS-stereo-test-8kHz.wav"), x1, fs1); end +x1 = audioread(TMP_FILENAME); aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); select_tracks(0, 100); @@ -255,36 +222,22 @@ do_test_neq(calc_LUFS(y1(:,1), fs), calc_LUFS(y1(:,2), fs), "stereo balance trac CURRENT_TEST = "Loudness RMS mode, stereo independent"; randn("seed", 1); fs= 44100; -x = 0.1*randn(30*fs, 2); -x(:,1) = x(:,1) * 0.6; -audiowrite(TMP_FILENAME, x, fs); -if EXPORT_TEST_SIGNALS - audiowrite(cstrcat(pwd(), "/Loudness-RMS-test.wav"), x, fs); -end +x1 = 0.1*randn(30*fs, 2); +x1(:,1) = x1(:,1) * 0.6; remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs, "Loudness-RMS-test.wav"); aud_do("LoudnessNormalization: RMSLevel=-20 DualMono=0 NormalizeTo=1 StereoIndependent=1\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(2); do_test_equ(20*log10(sqrt(sum(y(:,1).*y(:,1)/length(y)))), -20, "channel 1 RMS"); do_test_equ(20*log10(sqrt(sum(y(:,2).*y(:,2)/length(y)))), -20, "channel 2 RMS"); ## Test Loudness RMS mode: stereo dependent CURRENT_TEST = "Loudness RMS mode, stereo dependent"; -audiowrite(TMP_FILENAME, x, fs); - remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs); aud_do("LoudnessNormalization: RMSLevel=-22 DualMono=1 NormalizeTo=1 StereoIndependent=0\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(2); # Stereo RMS must be calculated in quadratic domain. do_test_equ(20*log10(sqrt(sum(rms(y).^2)/size(y)(2))), -22, "RMS"); do_test_neq(20*log10(rms(y(:,1))), 20*log10(rms(y(:,2))), "stereo balance", 1); diff --git a/tests/octave/run_test.m b/tests/octave/run_test.m index d14810334..3496ddd7f 100755 --- a/tests/octave/run_test.m +++ b/tests/octave/run_test.m @@ -29,10 +29,13 @@ if nargin == 2 end ## Initialization and helper functions +global TMP_FILENAME; +global EXPORT_TEST_SIGNALS; UID=num2str(getuid()); PIPE_TO_PATH=strcat("/tmp/audacity_script_pipe.to.", UID); PIPE_FROM_PATH=strcat("/tmp/audacity_script_pipe.from.", UID); TMP_FILENAME=strcat(pwd(), "/tmp.wav"); +EXPORT_TEST_SIGNALS = false; printf("Open scripting pipes, this may freeze if Audacity does not run...\n"); @@ -74,6 +77,27 @@ function select_tracks(num, count) aud_do(sprintf("SelectTracks: Track=%d TrackCount=%d Mode=Set\n", num, count)); end +function x_in = import_from_aud(channels) + global TMP_FILENAME; + aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=", ... + num2str(channels), "\n")); + system("sync"); + x_in = audioread(TMP_FILENAME); +end + +function x_out = export_to_aud(x, fs, name = "") + global TMP_FILENAME; + global EXPORT_TEST_SIGNALS; + audiowrite(TMP_FILENAME, x, fs); + if EXPORT_TEST_SIGNALS && length(name) != 0 + audiowrite(cstrcat(pwd(), "/", name), x, fs); + end + # Read it back to avoid quantization-noise in tests + x_out = audioread(TMP_FILENAME); + aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); + select_tracks(0, 100); +end + ## Float equal comparison helper function [ret] = float_eq(x, y, eps=0.001) ret = abs(x - y) < eps; @@ -99,41 +123,43 @@ function plot_failure(x, y) plot(x, 'r') hold on plot(y, 'b') - plot(log10(abs(x-y)), 'g') + delta = abs(x-y); + max(delta) + plot(log10(delta), 'g') hold off legend("Audacity", "Octave", "log-delta", "location", "southeast") input("Press enter to continue", "s") end -function do_test_equ(x, y, msg, eps=0.001, skip = false) +function do_test_equ(x, y, msg = "", eps = 0.001, skip = false) cmp = all(all(float_eq(x, y, eps))); if do_test(cmp, msg, skip) == 0 plot_failure(x, y); end end -function do_test_neq(x, y, msg, eps=0.001, skip = false) +function do_test_neq(x, y, msg = "", eps = 0.001, skip = false) cmp = all(all(!float_eq(x, y, eps))); if do_test(cmp, msg, skip) == 0 plot_failure(x, y); end end -function do_test_gte(x, y, msg, skip = false) +function do_test_gte(x, y, msg = "", skip = false) cmp = all(all(x >= y)); if do_test(cmp, msg, skip) == 0 plot_failure(x, y); end end -function do_test_lte(x, y, msg, skip = false) +function do_test_lte(x, y, msg = "", skip = false) cmp = all(all(x <= y)); if do_test(cmp, msg, skip) == 0 plot_failure(x, y); end end -function result = do_test(result, msg, skip = false) +function result = do_test(result, msg = "", skip = false) global TESTS_RUN; global TESTS_FAILED; global TESTS_SKIPPED;