A scripts/debug/compressor2_buffers.m => scripts/debug/compressor2_buffers.m +25 -0
@@ 0,0 1,25 @@
+%% Debug Compressor v2 pipeline buffers
+buffer_ids = [1,2,3,4,5];
+prefix = '/tmp';
+
+figure(1);
+for k = 1:length(buffer_ids)
+ subplot(length(buffer_ids), 1, k)
+ bfile = fopen(sprintf('%s/envbuf.%d.bin', prefix, buffer_ids(k)));
+ env = fread(bfile, 'float').';
+ bfile = fopen(sprintf('%s/blockbuf.%d.bin', prefix, buffer_ids(k)));
+ block_raw = fread(bfile, 'float').';
+
+ sizes = reshape(block_raw(1:12), 3, 4);
+ capacity = (1:4).*sizes(3,:);
+ track_size = horzcat(0, capacity(1:3)) + sizes(1,:);
+ block = block_raw(13:end);
+
+ plot(block, 'b', 'linewidth', 3);
+ hold on;
+ plot(circshift(env, length(env)/3), 'r');
+ stem(capacity, ones(1, length(capacity)), 'g');
+ stem(track_size, 1.5.*ones(1, length(capacity)), 'b');
+ ylim([-2 2]);
+ hold off;
+end<
\ No newline at end of file
A scripts/debug/compressor2_trace.m => scripts/debug/compressor2_trace.m +61 -0
@@ 0,0 1,61 @@
+## plot realtime trace data from Compressor2 effect
+
+stereo = true;
+bfile = fopen("/tmp/audio.out");
+
+if stereo
+ width = 14;
+else
+ width = 12;
+end
+
+raw_data = reshape(fread(bfile, 'float'), width, []).';
+
+data = struct;
+data.threshold_DB = raw_data(:,1);
+data.ratio = raw_data(:,2);
+data.kneewidth_DB = raw_data(:,3);
+data.attack_time = raw_data(:,4);
+data.release_time = raw_data(:,5);
+data.lookahead_time = raw_data(:,6);
+data.lookbehind_time = raw_data(:,7);
+data.output_gain_DB = raw_data(:,8);
+
+if stereo
+ data.in = horzcat(raw_data(:,9), raw_data(:,10));
+ data.env = raw_data(:,11);
+ data.gain = raw_data(:,12);
+ data.out = horzcat(raw_data(:,13), raw_data(:,14));
+else
+ data.in = raw_data(:,9);
+ data.env = raw_data(:,10);
+ data.gain = raw_data(:,11);
+ data.out = raw_data(:,12);
+end
+
+figure(1);
+plot(data.in.*100, 'b');
+hold on;
+plot(data.out.*100, 'g');
+plot(data.threshold_DB, 'r');
+plot(data.ratio, 'r');
+plot(data.kneewidth_DB, 'r');
+plot(data.attack_time.*10, 'c', "linewidth", 2);
+plot(data.release_time.*10, 'c', "linewidth", 2);
+plot(data.lookahead_time, 'm');
+plot(data.lookbehind_time, 'm');
+plot(data.output_gain_DB, 'r');
+plot(data.env.*100, 'k', "linewidth", 2);
+plot(data.gain.*50, 'k', "linestyle", '--');
+hold off;
+grid;
+
+if stereo
+ legend("in*100", "in*100", "out*100", "out*100", "threshold", "ratio", ...
+ "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ...
+ "out_gain", "env*100", "gain*50");
+else
+ legend("in*100", "out*100", "threshold", "ratio", ...
+ "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ...
+ "out_gain", "env*100", "gain*50");
+end
M src/CMakeLists.txt => src/CMakeLists.txt +6 -0
@@ 433,6 433,8 @@ list( APPEND SOURCES PRIVATE
effects/ClickRemoval.h
effects/Compressor.cpp
effects/Compressor.h
+ effects/Compressor2.cpp
+ effects/Compressor2.h
effects/Contrast.cpp
effects/Contrast.h
effects/Distortion.cpp
@@ 948,6 950,8 @@ list( APPEND SOURCES PRIVATE
widgets/Overlay.h
widgets/OverlayPanel.cpp
widgets/OverlayPanel.h
+ widgets/Plot.cpp
+ widgets/Plot.h
widgets/PopupMenuTable.cpp
widgets/PopupMenuTable.h
widgets/ProgressDialog.cpp
@@ 955,6 959,8 @@ list( APPEND SOURCES PRIVATE
widgets/ReadOnlyText.h
widgets/Ruler.cpp
widgets/Ruler.h
+ widgets/SliderTextCtrl.cpp
+ widgets/SliderTextCtrl.h
widgets/UnwritableLocationErrorDialog.cpp
widgets/UnwritableLocationErrorDialog.h
widgets/Warning.cpp
M src/ShuttleGui.cpp => src/ShuttleGui.cpp +55 -0
@@ 121,6 121,9 @@ for registering for changes.
#include "widgets/wxTextCtrlWrapper.h"
#include "AllThemeResources.h"
+#include "widgets/Plot.h"
+#include "widgets/SliderTextCtrl.h"
+
#if wxUSE_ACCESSIBILITY
#include "widgets/WindowAccessible.h"
#endif
@@ 613,6 616,31 @@ wxSlider * ShuttleGuiBase::AddSlider(
return pSlider;
}
+SliderTextCtrl* ShuttleGuiBase::AddSliderTextCtrl(
+ const TranslatableString &Prompt, double pos, double Max, double Min,
+ int precision, double* value, double scale, double offset)
+{
+ HandleOptionality( Prompt );
+ AddPrompt( Prompt );
+ UseUpId();
+ if( mShuttleMode != eIsCreating )
+ return wxDynamicCast(wxWindow::FindWindowById( miId, mpDlg), SliderTextCtrl);
+ SliderTextCtrl * pSlider;
+ mpWind = pSlider = safenew SliderTextCtrl(GetParent(), miId,
+ pos, Min, Max, precision, scale, offset, wxDefaultPosition, wxDefaultSize,
+ GetStyle( SliderTextCtrl::HORIZONTAL ),
+ value
+ );
+#if wxUSE_ACCESSIBILITY
+ // so that name can be set on a standard control
+ mpWind->SetAccessible(safenew WindowAccessible(mpWind));
+#endif
+ mpWind->SetName(wxStripMenuCodes(Prompt.Translation()));
+ miProp=1;
+ UpdateSizers();
+ return pSlider;
+}
+
wxSpinCtrl * ShuttleGuiBase::AddSpinCtrl(
const TranslatableString &Prompt, int Value, int Max, int Min)
{
@@ 750,6 778,33 @@ void ShuttleGuiBase::AddConstTextBox(
UpdateSizers();
}
+Plot* ShuttleGuiBase::AddPlot( const TranslatableString &Prompt,
+ double x_min, double x_max, double y_min, double y_max,
+ const TranslatableString& x_label, const TranslatableString& y_label,
+ int x_format, int y_format, int count)
+{
+ HandleOptionality( Prompt );
+ AddPrompt( Prompt );
+ UseUpId();
+ if( mShuttleMode != eIsCreating )
+ return wxDynamicCast(wxWindow::FindWindowById(miId, mpDlg), Plot);
+ Plot* pPlot;
+ mpWind = pPlot = safenew Plot(GetParent(), miId,
+ x_min, x_max, y_min, y_max, x_label, y_label,
+ x_format, y_format, count,
+ wxDefaultPosition, wxDefaultSize,
+ GetStyle( SliderTextCtrl::HORIZONTAL )
+ );
+#if wxUSE_ACCESSIBILITY
+ // so that name can be set on a standard control
+ mpWind->SetAccessible(safenew WindowAccessible(mpWind));
+#endif
+ mpWind->SetName(wxStripMenuCodes(Prompt.Translation()));
+ miProp=1;
+ UpdateSizers();
+ return pPlot;
+}
+
wxListBox * ShuttleGuiBase::AddListBox(const wxArrayStringEx &choices)
{
UseUpId();
M src/ShuttleGui.h => src/ShuttleGui.h +11 -0
@@ 28,6 28,8 @@
class ChoiceSetting;
class wxArrayStringEx;
+class Plot;
+class SliderTextCtrl;
const int nMaxNestedSizers = 20;
@@ 263,6 265,10 @@ public:
int Value, int Max, int Min);
wxTreeCtrl * AddTree();
+ SliderTextCtrl* AddSliderTextCtrl(
+ const TranslatableString &Prompt, double pos, double Max, double Min = 0,
+ int precision = 2, double* value = NULL, double scale = 0, double offset = 0);
+
// Pass the same initValue to the sequence of calls to AddRadioButton and
// AddRadioButtonToGroup.
// The radio button is filled if selector == initValue
@@ 343,6 349,11 @@ public:
void AddConstTextBox(
const TranslatableString &Caption, const TranslatableString & Value );
+ Plot* AddPlot( const TranslatableString &Prompt,
+ double x_min, double x_max, double y_min, double y_max,
+ const TranslatableString& x_label, const TranslatableString& y_label,
+ int x_format = 1, int y_format = 1, int count = 1 );
+
//-- Start and end functions. These are used for sizer, or other window containers
// and create the appropriate widget.
void StartHorizontalLay(int PositionFlags=wxALIGN_CENTRE, int iProp=1);
A src/effects/Compressor2.cpp => src/effects/Compressor2.cpp +1765 -0
@@ 0,0 1,1765 @@
+/**********************************************************************
+
+ Audacity: A Digital Audio Editor
+
+ Compressor2.cpp
+
+ Max Maisel
+
+*******************************************************************//**
+
+\class EffectCompressor2
+\brief An Effect which reduces the dynamic level.
+
+*//*******************************************************************/
+
+
+#include "Compressor2.h"
+
+#include <math.h>
+#include <numeric>
+
+#include <wx/intl.h>
+#include <wx/valgen.h>
+
+#include "../AColor.h"
+#include "../Prefs.h"
+#include "../ProjectFileManager.h"
+#include "../Shuttle.h"
+#include "../ShuttleGui.h"
+#include "../WaveTrack.h"
+#include "../widgets/valnum.h"
+#include "../widgets/Plot.h"
+#include "../widgets/ProgressDialog.h"
+#include "../widgets/Ruler.h"
+#include "../widgets/SliderTextCtrl.h"
+
+#include "LoadEffects.h"
+
+//#define DEBUG_COMPRESSOR2_DUMP_BUFFERS
+//#define DEBUG_COMPRESSOR2_ENV
+//#define DEBUG_COMPRESSOR2_TRACE
+//#define DEBUG_COMPRESSOR2_TRACE2
+
+#if defined(DEBUG_COMPRESSOR2_DUMP_BUFFERS) or defined(DEBUG_COMPRESSOR2_TRACE2)
+#include <fstream>
+int buf_num;
+std::fstream debugfile;
+#endif
+
+enum kAlgorithms
+{
+ kExpFit,
+ kEnvPT1,
+ nAlgos
+};
+
+static const ComponentInterfaceSymbol kAlgorithmStrings[nAlgos] =
+{
+ { XO("Exponential-Fit") },
+ { XO("Analog Model") }
+};
+
+enum kCompressBy
+{
+ kAmplitude,
+ kRMS,
+ nBy
+};
+
+static const ComponentInterfaceSymbol kCompressByStrings[nBy] =
+{
+ { XO("peak amplitude") },
+ { XO("RMS") }
+};
+
+// Define keys, defaults, minimums, and maximums for the effect parameters
+//
+// Name Type Key Def Min Max Scale
+Param( Algorithm, int, wxT("Algorithm"), kEnvPT1, 0, nAlgos-1, 1 );
+Param( CompressBy, int, wxT("CompressBy"), kAmplitude, 0, nBy-1, 1 );
+Param( StereoInd, bool, wxT("StereoIndependent"), false, false, true, 1 );
+
+Param( Threshold, double, wxT("Threshold"), -12.0, -60.0, -1.0, 1.0 );
+Param( Ratio, double, wxT("Ratio"), 2.0, 1.1, 100.0, 20.0 );
+Param( KneeWidth, double, wxT("KneeWidth"), 10.0, 0.0, 20.0, 10.0 );
+Param( AttackTime, double, wxT("AttackTime"), 0.2, 0.0001, 30.0, 2000.0 );
+Param( ReleaseTime, double, wxT("ReleaseTime"), 1.0, 0.0001, 30.0, 2000.0 );
+Param( LookaheadTime, double, wxT("LookaheadTime"), 0.0, 0.0, 10.0, 200.0 );
+Param( LookbehindTime, double, wxT("LookbehindTime"), 0.1, 0.0, 10.0, 200.0 );
+Param( OutputGain, double, wxT("OutputGain"), 0.0, 0.0, 50.0, 10.0 );
+
+struct FactoryPreset
+{
+ const TranslatableString name;
+ int algorithm;
+ int compressBy;
+ bool stereoInd;
+ double thresholdDB;
+ double ratio;
+ double kneeWidthDB;
+ double attackTime;
+ double releaseTime;
+ double lookaheadTime;
+ double lookbehindTime;
+ double outputGainDB;
+};
+
+static const FactoryPreset FactoryPresets[] =
+{
+ { XO("Dynamic Reduction"), kEnvPT1, kAmplitude, false, -40, 2.5, 6, 0.3, 0.3, 0.5, 0.5, 23 },
+ { XO("Peak Reduction"), kEnvPT1, kAmplitude, false, -10, 10, 0, 0.001, 0.05, 0, 0, 0 },
+ { XO("Analog Limiter"), kEnvPT1, kAmplitude, false, -6, 100, 6, 0.0001, 0.0001, 0, 0, 0 }
+};
+
+inline int ScaleToPrecision(double scale)
+{
+ return ceil(log10(scale));
+}
+
+inline bool IsInRange(double val, double min, double max)
+{
+ return val >= min && val <= max;
+}
+
+BEGIN_EVENT_TABLE(EffectCompressor2, wxEvtHandler)
+ EVT_CHECKBOX(wxID_ANY, EffectCompressor2::OnUpdateUI)
+ EVT_CHOICE(wxID_ANY, EffectCompressor2::OnUpdateUI)
+ EVT_SLIDERTEXT(wxID_ANY, EffectCompressor2::OnUpdateUI)
+END_EVENT_TABLE()
+
+const ComponentInterfaceSymbol EffectCompressor2::Symbol
+{ XO("Dynamic Compressor") };
+
+namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; }
+
+SlidingRmsPreprocessor::SlidingRmsPreprocessor(size_t windowSize, float gain)
+ : mSum(0),
+ mGain(gain),
+ mWindow(windowSize, 0),
+ mPos(0),
+ mInsertCount(0)
+{
+}
+
+float SlidingRmsPreprocessor::ProcessSample(float value)
+{
+ return DoProcessSample(value * value);
+}
+
+float SlidingRmsPreprocessor::ProcessSample(float valueL, float valueR)
+{
+ return DoProcessSample((valueL * valueL + valueR * valueR) / 2.0);
+}
+
+void SlidingRmsPreprocessor::Reset(float level)
+{
+ mSum = (level / mGain) * (level / mGain) * float(mWindow.size());
+ mPos = 0;
+ mInsertCount = 0;
+ std::fill(mWindow.begin(), mWindow.end(), 0);
+}
+
+void SlidingRmsPreprocessor::SetWindowSize(size_t windowSize)
+{
+ mWindow.resize(windowSize);
+ Reset();
+}
+
+float SlidingRmsPreprocessor::DoProcessSample(float value)
+{
+ if(mInsertCount > REFRESH_WINDOW_EVERY)
+ {
+ // Update RMS sum directly from the circle buffer every
+ // REFRESH_WINDOW_EVERY samples to avoid accumulation of rounding errors.
+ mWindow[mPos] = value;
+ Refresh();
+ }
+ else
+ {
+ // Calculate current level from root-mean-squared of
+ // circular buffer ("RMS").
+ mSum -= mWindow[mPos];
+ mWindow[mPos] = value;
+ mSum += mWindow[mPos];
+ ++mInsertCount;
+ }
+
+ // Also refresh if there are severe rounding errors that
+ // caused mRMSSum to be negative.
+ if(mSum < 0)
+ Refresh();
+
+ mPos = (mPos + 1) % mWindow.size();
+
+ // Multiply by gain (usually two) to approximately correct peak level
+ // of standard audio (avoid clipping).
+ return mGain * sqrt(mSum/float(mWindow.size()));
+}
+
+void SlidingRmsPreprocessor::Refresh()
+{
+ // Recompute the RMS sum periodically to prevent accumulation
+ // of rounding errors during long waveforms.
+ mSum = 0;
+ for(const auto& sample : mWindow)
+ mSum += sample;
+ mInsertCount = 0;
+}
+
+SlidingMaxPreprocessor::SlidingMaxPreprocessor(size_t windowSize)
+ : mWindow(windowSize, 0),
+ mMaxes(windowSize, 0),
+ mPos(0)
+{
+}
+
+float SlidingMaxPreprocessor::ProcessSample(float value)
+{
+ return DoProcessSample(fabs(value));
+}
+
+float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR)
+{
+ return DoProcessSample((fabs(valueL) + fabs(valueR)) / 2.0);
+}
+
+void SlidingMaxPreprocessor::Reset(float value)
+{
+ mPos = 0;
+ std::fill(mWindow.begin(), mWindow.end(), value);
+ std::fill(mMaxes.begin(), mMaxes.end(), value);
+}
+
+void SlidingMaxPreprocessor::SetWindowSize(size_t windowSize)
+{
+ mWindow.resize(windowSize);
+ mMaxes.resize(windowSize);
+ Reset();
+}
+
+float SlidingMaxPreprocessor::DoProcessSample(float value)
+{
+ size_t oldHead = (mPos-1) % mWindow.size();
+ size_t currentHead = mPos;
+ size_t nextHead = (mPos+1) % mWindow.size();
+ mWindow[mPos] = value;
+ mMaxes[mPos] = std::max(value, mMaxes[oldHead]);
+
+ if(mPos % ((mWindow.size()+1)/2) == 0)
+ {
+ mMaxes[mPos] = mWindow[mPos];
+ for(size_t i = 1; i < mWindow.size(); ++i)
+ {
+ size_t pos1 = (mPos-i+mWindow.size()) % mWindow.size();
+ size_t pos2 = (mPos-i+mWindow.size()+1) % mWindow.size();
+ mMaxes[pos1] = std::max(mWindow[pos1], mMaxes[pos2]);
+ }
+ }
+ mPos = nextHead;
+ return std::max(mMaxes[currentHead], mMaxes[nextHead]);
+}
+
+EnvelopeDetector::EnvelopeDetector(size_t buffer_size)
+ : mPos(0),
+ mInitialCondition(0),
+ mInitialBlockSize(0),
+ mLookaheadBuffer(buffer_size, 0),
+ mProcessingBuffer(buffer_size, 0),
+ mProcessedBuffer(buffer_size, 0)
+{
+}
+
+float EnvelopeDetector::AttackFactor()
+{
+ return 0;
+}
+float EnvelopeDetector::DecayFactor()
+{
+ return 0;
+}
+
+float EnvelopeDetector::ProcessSample(float value)
+{
+ float retval = mProcessedBuffer[mPos];
+ mLookaheadBuffer[mPos++] = value;
+ if(mPos == mProcessingBuffer.size())
+ {
+ Follow();
+ mPos = 0;
+ mProcessedBuffer.swap(mProcessingBuffer);
+ mLookaheadBuffer.swap(mProcessingBuffer);
+ }
+ return retval;
+}
+
+void EnvelopeDetector::CalcInitialCondition(float value)
+{
+}
+
+size_t EnvelopeDetector::GetBlockSize() const
+{
+ wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
+ wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size());
+ return mLookaheadBuffer.size();
+}
+
+const float* EnvelopeDetector::GetBuffer(int idx) const
+{
+ if(idx == 0)
+ return mProcessedBuffer.data();
+ else if(idx == 1)
+ return mProcessingBuffer.data();
+ else if(idx == 2)
+ return mLookaheadBuffer.data();
+ else
+ wxASSERT(false);
+ return nullptr;
+}
+
+ExpFitEnvelopeDetector::ExpFitEnvelopeDetector(
+ float rate, float attackTime, float releaseTime, size_t bufferSize)
+ : EnvelopeDetector(bufferSize)
+{
+ SetParams(rate, attackTime, releaseTime);
+}
+
+void ExpFitEnvelopeDetector::Reset(float value)
+{
+ std::fill(mProcessedBuffer.begin(), mProcessedBuffer.end(), value);
+ std::fill(mProcessingBuffer.begin(), mProcessingBuffer.end(), value);
+ std::fill(mLookaheadBuffer.begin(), mLookaheadBuffer.end(), value);
+}
+
+void ExpFitEnvelopeDetector::SetParams(
+ float sampleRate, float attackTime, float releaseTime)
+{
+ attackTime = std::max(attackTime, 1.0f / sampleRate);
+ releaseTime = std::max(releaseTime, 1.0f / sampleRate);
+ mAttackFactor = exp(-1.0 / (sampleRate * attackTime));
+ mReleaseFactor = exp(-1.0 / (sampleRate * releaseTime));
+}
+
+void ExpFitEnvelopeDetector::Follow()
+{
+ /*
+ "Follow"ing algorithm by Roger B. Dannenberg, taken from
+ Nyquist. His description follows. -DMM
+
+ Description: this is a sophisticated envelope follower.
+ The input is an envelope, e.g. something produced with
+ the AVG function. The purpose of this function is to
+ generate a smooth envelope that is generally not less
+ than the input signal. In other words, we want to "ride"
+ the peaks of the signal with a smooth function. The
+ algorithm is as follows: keep a current output value
+ (called the "value"). The value is allowed to increase
+ by at most rise_factor and decrease by at most fall_factor.
+ Therefore, the next value should be between
+ value * rise_factor and value * fall_factor. If the input
+ is in this range, then the next value is simply the input.
+ If the input is less than value * fall_factor, then the
+ next value is just value * fall_factor, which will be greater
+ than the input signal. If the input is greater than value *
+ rise_factor, then we compute a rising envelope that meets
+ the input value by working backwards in time, changing the
+ previous values to input / rise_factor, input / rise_factor^2,
+ input / rise_factor^3, etc. until this NEW envelope intersects
+ the previously computed values. There is only a limited buffer
+ in which we can work backwards, so if the NEW envelope does not
+ intersect the old one, then make yet another pass, this time
+ from the oldest buffered value forward, increasing on each
+ sample by rise_factor to produce a maximal envelope. This will
+ still be less than the input.
+
+ The value has a lower limit of floor to make sure value has a
+ reasonable positive value from which to begin an attack.
+ */
+ wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
+ wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size());
+
+ // First apply a peak detect with the requested release rate.
+ size_t buffer_size = mProcessingBuffer.size();
+ double env = mProcessedBuffer[buffer_size-1];
+ for(size_t i = 0; i < buffer_size; ++i)
+ {
+ env *= mReleaseFactor;
+ if(mProcessingBuffer[i] > env)
+ env = mProcessingBuffer[i];
+ mProcessingBuffer[i] = env;
+ }
+ // Preprocess lookahead buffer as well.
+ for(size_t i = 0; i < buffer_size; ++i)
+ {
+ env *= mReleaseFactor;
+ if(mLookaheadBuffer[i] > env)
+ env = mLookaheadBuffer[i];
+ mLookaheadBuffer[i] = env;
+ }
+
+ // Next do the same process in reverse direction to get the
+ // requested attack rate and preprocess lookahead buffer.
+ for(ssize_t i = buffer_size - 1; i >= 0; --i)
+ {
+ env *= mAttackFactor;
+ if(mLookaheadBuffer[i] < env)
+ mLookaheadBuffer[i] = env;
+ else
+ env = mLookaheadBuffer[i];
+ }
+ for(ssize_t i = buffer_size - 1; i >= 0; --i)
+ {
+ if(mProcessingBuffer[i] < env * mAttackFactor)
+ {
+ env *= mAttackFactor;
+ mProcessingBuffer[i] = env;
+ }
+ else if(mProcessingBuffer[i] > env)
+ // Intersected the previous envelope buffer, so we are finished
+ return;
+ else
+ ; // Do nothing if we are on a plateau from peak look-around
+ }
+}
+
+Pt1EnvelopeDetector::Pt1EnvelopeDetector(
+ float rate, float attackTime, float releaseTime, size_t bufferSize,
+ bool correctGain)
+ : EnvelopeDetector(bufferSize),
+ mCorrectGain(correctGain)
+{
+ SetParams(rate, attackTime, releaseTime);
+}
+
+float Pt1EnvelopeDetector::AttackFactor()
+{
+ return mAttackFactor;
+}
+float Pt1EnvelopeDetector::DecayFactor()
+{
+ return mReleaseFactor;
+}
+
+void Pt1EnvelopeDetector::Reset(float value)
+{
+ value *= mGainCorrection;
+ std::fill(mProcessedBuffer.begin(), mProcessedBuffer.end(), value);
+ std::fill(mProcessingBuffer.begin(), mProcessingBuffer.end(), value);
+ std::fill(mLookaheadBuffer.begin(), mLookaheadBuffer.end(), value);
+}
+
+void Pt1EnvelopeDetector::SetParams(
+ float sampleRate, float attackTime, float releaseTime)
+{
+ attackTime = std::max(attackTime, 1.0f / sampleRate);
+ releaseTime = std::max(releaseTime, 1.0f / sampleRate);
+
+ // Approximate peak amplitude correction factor.
+ if(mCorrectGain)
+ mGainCorrection = 1.0 + exp(attackTime / 30.0);
+ else
+ mGainCorrection = 1.0;
+
+ mAttackFactor = 1.0 / (attackTime * sampleRate);
+ mReleaseFactor = 1.0 / (releaseTime * sampleRate);
+ mInitialBlockSize = std::min(size_t(sampleRate * sqrt(attackTime)), mLookaheadBuffer.size());
+}
+
+void Pt1EnvelopeDetector::CalcInitialCondition(float value)
+{
+ mLookaheadBuffer[mPos++] = value;
+ if(mPos == mInitialBlockSize)
+ {
+ float level = 0;
+ for(size_t i = 0; i < mPos; ++i)
+ {
+ if(mLookaheadBuffer[i] >= level)
+ if(i < mInitialBlockSize / 5)
+ level += 5 * mAttackFactor * (mLookaheadBuffer[i] - level);
+ else
+ level += mAttackFactor * (mLookaheadBuffer[i] - level);
+ else
+ level += mReleaseFactor * (mLookaheadBuffer[i] - level);
+ }
+ mInitialCondition = level;
+ mPos = 0;
+ }
+}
+
+void Pt1EnvelopeDetector::Follow()
+{
+ wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size());
+ wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size());
+
+ // Simulate analog compressor with PT1 characteristic.
+ size_t buffer_size = mProcessingBuffer.size();
+ float level = mProcessedBuffer[buffer_size-1] / mGainCorrection;
+ for(size_t i = 0; i < buffer_size; ++i)
+ {
+ if(mProcessingBuffer[i] >= level)
+ level += mAttackFactor * (mProcessingBuffer[i] - level);
+ else
+ level += mReleaseFactor * (mProcessingBuffer[i] - level);
+ mProcessingBuffer[i] = level * mGainCorrection;
+ }
+}
+
+void PipelineBuffer::pad_to(size_t len, float value, bool stereo)
+{
+ if(size < len)
+ {
+ size = len;
+ std::fill(mBlockBuffer[0].get() + trackSize,
+ mBlockBuffer[0].get() + size, value);
+ if(stereo)
+ std::fill(mBlockBuffer[1].get() + trackSize,
+ mBlockBuffer[1].get() + size, value);
+ }
+}
+
+void PipelineBuffer::swap(PipelineBuffer& other)
+{
+ std::swap(trackPos, other.trackPos);
+ std::swap(trackSize, other.trackSize);
+ std::swap(size, other.size);
+ std::swap(mBlockBuffer[0], other.mBlockBuffer[0]);
+ std::swap(mBlockBuffer[1], other.mBlockBuffer[1]);
+}
+
+void PipelineBuffer::init(size_t capacity, bool stereo)
+{
+ trackPos = 0;
+ trackSize = 0;
+ size = 0;
+ mCapacity = capacity;
+ mBlockBuffer[0].reinit(capacity);
+ if(stereo)
+ mBlockBuffer[1].reinit(capacity);
+ fill(0, stereo);
+}
+
+void PipelineBuffer::fill(float value, bool stereo)
+{
+ std::fill(mBlockBuffer[0].get(), mBlockBuffer[0].get() + mCapacity, value);
+ if(stereo)
+ std::fill(mBlockBuffer[1].get(), mBlockBuffer[1].get() + mCapacity, value);
+}
+
+void PipelineBuffer::free()
+{
+ mBlockBuffer[0].reset();
+ mBlockBuffer[1].reset();
+}
+
+EffectCompressor2::EffectCompressor2()
+ : mIgnoreGuiEvents(false),
+ mAlgorithmCtrl(0),
+ mPreprocCtrl(0),
+ mAttackTimeCtrl(0),
+ mLookaheadTimeCtrl(0)
+{
+ mAlgorithm = DEF_Algorithm;
+ mCompressBy = DEF_CompressBy;
+ mStereoInd = DEF_StereoInd;
+
+ mThresholdDB = DEF_Threshold;
+ mRatio = DEF_Ratio; // positive number > 1.0
+ mKneeWidthDB = DEF_KneeWidth;
+ mAttackTime = DEF_AttackTime; // seconds
+ mReleaseTime = DEF_ReleaseTime; // seconds
+ mLookaheadTime = DEF_LookaheadTime;
+ mLookbehindTime = DEF_LookbehindTime;
+ mOutputGainDB = DEF_OutputGain;
+
+ SetLinearEffectFlag(false);
+}
+
+EffectCompressor2::~EffectCompressor2()
+{
+}
+
+// ComponentInterface implementation
+
+ComponentInterfaceSymbol EffectCompressor2::GetSymbol()
+{
+ return Symbol;
+}
+
+TranslatableString EffectCompressor2::GetDescription()
+{
+ return XO("Reduces the dynamic of one or more tracks");
+}
+
+ManualPageID EffectCompressor2::ManualPage()
+{
+ return L"Dynamic_Compressor";
+}
+
+// EffectDefinitionInterface implementation
+
+EffectType EffectCompressor2::GetType()
+{
+ return EffectTypeProcess;
+}
+
+bool EffectCompressor2::SupportsRealtime()
+{
+#if defined(EXPERIMENTAL_REALTIME_AUDACITY_EFFECTS)
+ return false;
+#else
+ return false;
+#endif
+}
+
+unsigned EffectCompressor2::GetAudioInCount()
+{
+ return 2;
+}
+
+unsigned EffectCompressor2::GetAudioOutCount()
+{
+ return 2;
+}
+
+bool EffectCompressor2::RealtimeInitialize()
+{
+ SetBlockSize(512);
+ AllocRealtimePipeline();
+ mAlgorithmCtrl->Enable(false);
+ mPreprocCtrl->Enable(false);
+ mLookaheadTimeCtrl->Enable(false);
+ if(mAlgorithm == kExpFit)
+ mAttackTimeCtrl->Enable(false);
+ return true;
+}
+
+bool EffectCompressor2::RealtimeAddProcessor(
+ unsigned WXUNUSED(numChannels), float sampleRate)
+{
+ mSampleRate = sampleRate;
+ mProcStereo = true;
+ mPreproc = InitPreprocessor(mSampleRate);
+ mEnvelope = InitEnvelope(mSampleRate, mPipeline[0].size);
+
+ mProgressVal = 0;
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+ debugfile.close();
+ debugfile.open("/tmp/audio.out", std::ios::trunc | std::ios::out);
+#endif
+
+ return true;
+}
+
+bool EffectCompressor2::RealtimeFinalize()
+{
+ mPreproc.reset(nullptr);
+ mEnvelope.reset(nullptr);
+ FreePipeline();
+ mAlgorithmCtrl->Enable(true);
+ mPreprocCtrl->Enable(true);
+ mLookaheadTimeCtrl->Enable(true);
+ if(mAlgorithm == kExpFit)
+ mAttackTimeCtrl->Enable(true);
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+ debugfile.close();
+#endif
+ return true;
+}
+
+size_t EffectCompressor2::RealtimeProcess(
+ int group, float **inbuf, float **outbuf, size_t numSamples)
+{
+ std::lock_guard<std::mutex> guard(mRealtimeMutex);
+ const size_t j = PIPELINE_DEPTH-1;
+ for(size_t i = 0; i < numSamples; ++i)
+ {
+ if(mPipeline[j].trackSize == mPipeline[j].size)
+ {
+ ProcessPipeline();
+ mPipeline[j].trackSize = 0;
+ SwapPipeline();
+ }
+
+ outbuf[0][i] = mPipeline[j][0][mPipeline[j].trackSize];
+ outbuf[1][i] = mPipeline[j][1][mPipeline[j].trackSize];
+ mPipeline[j][0][mPipeline[j].trackSize] = inbuf[0][i];
+ mPipeline[j][1][mPipeline[j].trackSize] = inbuf[1][i];
+ ++mPipeline[j].trackSize;
+ }
+ return numSamples;
+}
+
+// EffectClientInterface implementation
+bool EffectCompressor2::DefineParams( ShuttleParams & S )
+{
+ S.SHUTTLE_PARAM(mAlgorithm, Algorithm);
+ S.SHUTTLE_PARAM(mCompressBy, CompressBy);
+ S.SHUTTLE_PARAM(mStereoInd, StereoInd);
+
+ S.SHUTTLE_PARAM(mThresholdDB, Threshold);
+ S.SHUTTLE_PARAM(mRatio, Ratio);
+ S.SHUTTLE_PARAM(mKneeWidthDB, KneeWidth);
+ S.SHUTTLE_PARAM(mAttackTime, AttackTime);
+ S.SHUTTLE_PARAM(mReleaseTime, ReleaseTime);
+ S.SHUTTLE_PARAM(mLookaheadTime, LookaheadTime);
+ S.SHUTTLE_PARAM(mLookbehindTime, LookbehindTime);
+ S.SHUTTLE_PARAM(mOutputGainDB, OutputGain);
+
+ return true;
+}
+
+bool EffectCompressor2::GetAutomationParameters(CommandParameters & parms)
+{
+ parms.Write(KEY_Algorithm, mAlgorithm);
+ parms.Write(KEY_CompressBy, mCompressBy);
+ parms.Write(KEY_StereoInd, mStereoInd);
+
+ parms.Write(KEY_Threshold, mThresholdDB);
+ parms.Write(KEY_Ratio, mRatio);
+ parms.Write(KEY_KneeWidth, mKneeWidthDB);
+ parms.Write(KEY_AttackTime, mAttackTime);
+ parms.Write(KEY_ReleaseTime, mReleaseTime);
+ parms.Write(KEY_LookaheadTime, mLookaheadTime);
+ parms.Write(KEY_LookbehindTime, mLookbehindTime);
+ parms.Write(KEY_OutputGain, mOutputGainDB);
+
+ return true;
+}
+
+bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms)
+{
+ ReadAndVerifyInt(Algorithm);
+ ReadAndVerifyInt(CompressBy);
+ ReadAndVerifyBool(StereoInd);
+
+ ReadAndVerifyDouble(Threshold);
+ ReadAndVerifyDouble(Ratio);
+ ReadAndVerifyDouble(KneeWidth);
+ ReadAndVerifyDouble(AttackTime);
+ ReadAndVerifyDouble(ReleaseTime);
+ ReadAndVerifyDouble(LookaheadTime);
+ ReadAndVerifyDouble(LookbehindTime);
+ ReadAndVerifyDouble(OutputGain);
+
+ mAlgorithm = Algorithm;
+ mCompressBy = CompressBy;
+ mStereoInd = StereoInd;
+
+ mThresholdDB = Threshold;
+ mRatio = Ratio;
+ mKneeWidthDB = KneeWidth;
+ mAttackTime = AttackTime;
+ mReleaseTime = ReleaseTime;
+ mLookaheadTime = LookaheadTime;
+ mLookbehindTime = LookbehindTime;
+ mOutputGainDB = OutputGain;
+
+ return true;
+}
+
+RegistryPaths EffectCompressor2::GetFactoryPresets()
+{
+ RegistryPaths names;
+
+ for (size_t i = 0; i < WXSIZEOF(FactoryPresets); i++)
+ names.push_back( FactoryPresets[i].name.Translation() );
+
+ return names;
+}
+
+bool EffectCompressor2::LoadFactoryPreset(int id)
+{
+ if (id < 0 || id >= int(WXSIZEOF(FactoryPresets)))
+ return false;
+
+ const FactoryPreset* preset = &FactoryPresets[id];
+
+ mAlgorithm = preset->algorithm;
+ mCompressBy = preset->compressBy;
+ mStereoInd = preset->stereoInd;
+
+ mThresholdDB = preset->thresholdDB;
+ mRatio = preset->ratio;
+ mKneeWidthDB = preset->kneeWidthDB;
+ mAttackTime = preset->attackTime;
+ mReleaseTime = preset->releaseTime;
+ mLookaheadTime = preset->lookaheadTime;
+ mLookbehindTime = preset->lookbehindTime;
+ mOutputGainDB = preset->outputGainDB;
+
+ TransferDataToWindow();
+ return true;
+}
+
+// Effect implementation
+
+bool EffectCompressor2::CheckWhetherSkipEffect()
+{
+ return false;
+}
+
+bool EffectCompressor2::Startup()
+{
+ wxString base = wxT("/Effects/Compressor2/");
+ // Load the old "current" settings
+ if (gPrefs->Exists(base))
+ {
+ mAlgorithm = DEF_Algorithm;
+ mCompressBy = DEF_CompressBy;
+ mStereoInd = DEF_StereoInd;
+
+ mThresholdDB = DEF_Threshold;
+ mRatio = DEF_Ratio; // positive number > 1.0
+ mKneeWidthDB = DEF_KneeWidth;
+ mAttackTime = DEF_AttackTime; // seconds
+ mReleaseTime = DEF_ReleaseTime; // seconds
+ mLookaheadTime = DEF_LookaheadTime;
+ mLookbehindTime = DEF_LookbehindTime;
+ mOutputGainDB = DEF_OutputGain;
+
+ SaveUserPreset(GetCurrentSettingsGroup());
+
+ gPrefs->Flush();
+ }
+ return true;
+}
+
+bool EffectCompressor2::Process()
+{
+ // Iterate over each track
+ this->CopyInputTracks(); // Set up mOutputTracks.
+ bool bGoodResult = true;
+
+ AllocPipeline();
+ mProgressVal = 0;
+
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+ debugfile.close();
+ debugfile.open("/tmp/audio.out", std::ios::trunc | std::ios::out);
+#endif
+
+ for(auto track : mOutputTracks->Selected<WaveTrack>()
+ + (mStereoInd ? &Track::Any : &Track::IsLeader))
+ {
+ // Get start and end times from track
+ // PRL: No accounting for multiple channels ?
+ double trackStart = track->GetStartTime();
+ double trackEnd = track->GetEndTime();
+
+ // Set the current bounds to whichever left marker is
+ // greater and whichever right marker is less:
+ mCurT0 = mT0 < trackStart? trackStart: mT0;
+ mCurT1 = mT1 > trackEnd? trackEnd: mT1;
+
+ // Get the track rate
+ mSampleRate = track->GetRate();
+
+ auto range = mStereoInd
+ ? TrackList::SingletonRange(track)
+ : TrackList::Channels(track);
+
+ mProcStereo = range.size() > 1;
+
+ mPreproc = InitPreprocessor(mSampleRate);
+ mEnvelope = InitEnvelope(mSampleRate, mPipeline[0].capacity());
+
+ if(!ProcessOne(range))
+ {
+ // Processing failed -> abort
+ bGoodResult = false;
+ break;
+ }
+ }
+
+ this->ReplaceProcessedTracks(bGoodResult);
+ mPreproc.reset(nullptr);
+ mEnvelope.reset(nullptr);
+ FreePipeline();
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+ debugfile.close();
+#endif
+ return bGoodResult;
+}
+
+void EffectCompressor2::PopulateOrExchange(ShuttleGui & S)
+{
+ S.SetBorder(10);
+
+ S.StartHorizontalLay(wxEXPAND, 1);
+ {
+ PlotData* plot;
+
+ S.StartVerticalLay();
+ S.AddVariableText(XO("Envelope dependent gain"), 0,
+ wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
+ mGainPlot = S.MinSize( { 400, 200 } )
+ .AddPlot({}, -60, 0, -60, 0, XO("dB"), XO("dB"),
+ Ruler::LinearDBFormat, Ruler::LinearDBFormat);
+
+ plot = mGainPlot->GetPlotData(0);
+ plot->pen = std::unique_ptr<wxPen>(
+ safenew wxPen(AColor::WideEnvelopePen));
+ plot->xdata.resize(61);
+ plot->ydata.resize(61);
+ std::iota(plot->xdata.begin(), plot->xdata.end(), -60);
+
+ S.EndVerticalLay();
+ S.StartVerticalLay();
+
+ S.AddVariableText(XO("Compressor step response"), 0,
+ wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL);
+ mResponsePlot = S.MinSize( { 400, 200 } )
+ .AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""),
+ Ruler::IntFormat, Ruler::RealFormat, 2);
+ mResponsePlot->SetName(XO("Compressor step response plot"));
+
+ plot = mResponsePlot->GetPlotData(0);
+ plot->pen = std::unique_ptr<wxPen>(
+ safenew wxPen(AColor::WideEnvelopePen));
+ plot->xdata = {0, RESPONSE_PLOT_STEP_START, RESPONSE_PLOT_STEP_START,
+ RESPONSE_PLOT_STEP_STOP, RESPONSE_PLOT_STEP_STOP, 5};
+ plot->ydata = {0.1, 0.1, 1, 1, 0.1, 0.1};
+
+ plot = mResponsePlot->GetPlotData(1);
+ plot->pen = std::unique_ptr<wxPen>(
+ safenew wxPen(AColor::WideEnvelopePen));
+ plot->pen->SetColour(wxColor( 230,80,80 )); // Same color as TrackArtist RMS red.
+ plot->pen->SetWidth(2);
+ plot->xdata.resize(RESPONSE_PLOT_SAMPLES+1);
+ plot->ydata.resize(RESPONSE_PLOT_SAMPLES+1);
+ for(size_t x = 0; x < plot->xdata.size(); ++x)
+ plot->xdata[x] = x * float(RESPONSE_PLOT_TIME) / float(RESPONSE_PLOT_SAMPLES);
+ S.EndVerticalLay();
+ }
+ S.EndHorizontalLay();
+
+ S.SetBorder(5);
+
+ S.StartStatic(XO("Algorithm"));
+ {
+ wxSize box_size;
+ int width;
+
+ S.StartHorizontalLay(wxEXPAND, 1);
+ S.StartVerticalLay(1);
+ S.StartMultiColumn(2, wxALIGN_LEFT);
+ {
+ S.SetStretchyCol(1);
+
+ mAlgorithmCtrl = S.Validator<wxGenericValidator>(&mAlgorithm)
+ .AddChoice(XO("Envelope Algorithm:"),
+ Msgids(kAlgorithmStrings, nAlgos),
+ mAlgorithm);
+
+ box_size = mAlgorithmCtrl->GetMinSize();
+ width = S.GetParent()->GetTextExtent(wxString::Format(
+ "%sxxxx", kAlgorithmStrings[nAlgos-1].Translation())).GetWidth();
+ box_size.SetWidth(width);
+ mAlgorithmCtrl->SetMinSize(box_size);
+ }
+ S.EndMultiColumn();
+ S.EndVerticalLay();
+
+ S.AddSpace(15, 0);
+
+ S.StartVerticalLay(1);
+ S.StartMultiColumn(2, wxALIGN_LEFT);
+ {
+ S.SetStretchyCol(1);
+
+ mPreprocCtrl = S.Validator<wxGenericValidator>(&mCompressBy)
+ .AddChoice(XO("Compress based on:"),
+ Msgids(kCompressByStrings, nBy),
+ mCompressBy);
+ mPreprocCtrl->SetMinSize(box_size);
+ }
+ S.EndMultiColumn();
+ S.EndVerticalLay();
+ S.EndHorizontalLay();
+
+ S.Validator<wxGenericValidator>(&mStereoInd)
+ .AddCheckBox(XO("Compress stereo channels independently"),
+ DEF_StereoInd);
+ }
+ S.EndStatic();
+
+ S.StartStatic(XO("Compressor"));
+ {
+ int textbox_width = S.GetParent()->GetTextExtent("10.00001XX").GetWidth();
+ SliderTextCtrl* ctrl = nullptr;
+
+ S.StartHorizontalLay(wxEXPAND, true);
+ S.StartVerticalLay(1);
+ S.StartMultiColumn(3, wxEXPAND);
+ {
+ S.SetStretchyCol(1);
+
+ S.AddVariableText(XO("Threshold:"), true,
+ wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+ ctrl = S.Name(XO("Threshold"))
+ .Style(SliderTextCtrl::HORIZONTAL)
+ .AddSliderTextCtrl({}, DEF_Threshold, MAX_Threshold,
+ MIN_Threshold, ScaleToPrecision(SCL_Threshold), &mThresholdDB);
+ ctrl->SetMinTextboxWidth(textbox_width);
+ S.AddVariableText(XO("dB"), true,
+ wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+ S.AddVariableText(XO("Ratio:"), true,
+ wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+ ctrl = S.Name(XO("Ratio"))
+ .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
+ .AddSliderTextCtrl({}, DEF_Ratio, MAX_Ratio, MIN_Ratio,
+ ScaleToPrecision(SCL_Ratio), &mRatio);
+ /* i18n-hint: Unless your language has a different convention for ratios,
+ * like 8:1, leave as is.*/
+ ctrl->SetMinTextboxWidth(textbox_width);
+ S.AddVariableText(XO(":1"), true,
+ wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+ S.AddVariableText(XO("Knee Width:"), true,
+ wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+ ctrl = S.Name(XO("Knee Width"))
+ .Style(SliderTextCtrl::HORIZONTAL)
+ .AddSliderTextCtrl({}, DEF_KneeWidth, MAX_KneeWidth,
+ MIN_KneeWidth, ScaleToPrecision(SCL_KneeWidth),
+ &mKneeWidthDB);
+ ctrl->SetMinTextboxWidth(textbox_width);
+ S.AddVariableText(XO("dB"), true,
+ wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+ S.AddVariableText(XO("Output Gain:"), true,
+ wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+ ctrl = S.Name(XO("Output Gain"))
+ .Style(SliderTextCtrl::HORIZONTAL)
+ .AddSliderTextCtrl({}, DEF_OutputGain, MAX_OutputGain,
+ MIN_OutputGain, ScaleToPrecision(SCL_OutputGain),
+ &mOutputGainDB);
+ ctrl->SetMinTextboxWidth(textbox_width);
+ S.AddVariableText(XO("dB"), true,
+ wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+ }
+ S.EndMultiColumn();
+ S.EndVerticalLay();
+
+ S.AddSpace(15, 0, 0);
+
+ S.StartHorizontalLay(wxEXPAND, true);
+ S.StartVerticalLay(1);
+ S.StartMultiColumn(3, wxEXPAND);
+ {
+ S.SetStretchyCol(1);
+
+ S.AddVariableText(XO("Attack:"), true,
+ wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+ mAttackTimeCtrl = S.Name(XO("Attack"))
+ .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
+ .AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime,
+ MIN_AttackTime, ScaleToPrecision(SCL_AttackTime),
+ &mAttackTime, SCL_AttackTime / 100, 0.033);
+ mAttackTimeCtrl->SetMinTextboxWidth(textbox_width);
+ S.AddVariableText(XO("s"), true,
+ wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+ S.AddVariableText(XO("Release:"), true,
+ wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+ ctrl = S.Name(XO("Release"))
+ .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
+ .AddSliderTextCtrl({}, DEF_ReleaseTime, MAX_ReleaseTime,
+ MIN_ReleaseTime, ScaleToPrecision(SCL_ReleaseTime),
+ &mReleaseTime, SCL_ReleaseTime / 100, 0.033);
+ ctrl->SetMinTextboxWidth(textbox_width);
+ S.AddVariableText(XO("s"), true,
+ wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+ S.AddVariableText(XO("Lookahead Time:"), true,
+ wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+ mLookaheadTimeCtrl = S.Name(XO("Lookahead Time"))
+ .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
+ .AddSliderTextCtrl({}, DEF_LookaheadTime, MAX_LookaheadTime,
+ MIN_LookaheadTime, ScaleToPrecision(SCL_LookaheadTime),
+ &mLookaheadTime, SCL_LookaheadTime / 10);
+ mLookaheadTimeCtrl->SetMinTextboxWidth(textbox_width);
+ S.AddVariableText(XO("s"), true,
+ wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
+ S.AddVariableText(XO("Hold Time:"), true,
+ wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
+ ctrl = S.Name(XO("Hold Time"))
+ .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG)
+ .AddSliderTextCtrl({}, DEF_LookbehindTime, MAX_LookbehindTime,
+ MIN_LookbehindTime, ScaleToPrecision(SCL_LookbehindTime),
+ &mLookbehindTime, SCL_LookbehindTime / 10);
+ ctrl->SetMinTextboxWidth(textbox_width);
+ S.AddVariableText(XO("s"), true,
+ wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+ }
+ S.EndMultiColumn();
+ S.EndVerticalLay();
+ S.EndHorizontalLay();
+ }
+ S.EndVerticalLay();
+}
+
+bool EffectCompressor2::TransferDataToWindow()
+{
+ // Transferring data to window causes spurious UpdateUI events
+ // which would reset the UI values to the previous value.
+ // This guard lets the program ignore them.
+ mIgnoreGuiEvents = true;
+ if (!mUIParent->TransferDataToWindow())
+ {
+ mIgnoreGuiEvents = false;
+ return false;
+ }
+
+ UpdateUI();
+ mIgnoreGuiEvents = false;
+ return true;
+}
+
+bool EffectCompressor2::TransferDataFromWindow()
+{
+ if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
+ {
+ return false;
+ }
+ return true;
+}
+
+// EffectCompressor2 implementation
+
+double EffectCompressor2::CompressorGain(double env)
+{
+ double kneeCond;
+ double envDB = LINEAR_TO_DB(env);
+
+ // envDB can become NaN is env is exactly zero.
+ // As solution, use a very low dB value to prevent NaN propagation.
+ if(isnan(envDB))
+ envDB = -200;
+
+ kneeCond = 2.0 * (envDB - mThresholdDB);
+ if(kneeCond < -mKneeWidthDB)
+ {
+ // Below threshold: only apply make-up gain
+ return DB_TO_LINEAR(mOutputGainDB);
+ }
+ else if(kneeCond >= mKneeWidthDB)
+ {
+ // Above threshold: apply compression and make-up gain
+ return DB_TO_LINEAR(mThresholdDB +
+ (envDB - mThresholdDB) / mRatio + mOutputGainDB - envDB);
+ }
+ else
+ {
+ // Within knee: apply interpolated compression and make-up gain
+ return DB_TO_LINEAR(
+ (1.0 / mRatio - 1.0)
+ * pow(envDB - mThresholdDB + mKneeWidthDB / 2.0, 2)
+ / (2.0 * mKneeWidthDB) + mOutputGainDB);
+ }
+}
+
+std::unique_ptr<SamplePreprocessor> EffectCompressor2::InitPreprocessor(
+ double rate, bool preview)
+{
+ size_t window_size = CalcWindowLength(rate);
+ if(mCompressBy == kAmplitude)
+ return std::unique_ptr<SamplePreprocessor>(safenew
+ SlidingMaxPreprocessor(window_size));
+ else
+ return std::unique_ptr<SamplePreprocessor>(safenew
+ SlidingRmsPreprocessor(window_size, preview ? 1.0 : 2.0));
+}
+
+std::unique_ptr<EnvelopeDetector> EffectCompressor2::InitEnvelope(
+ double rate, size_t blockSize, bool preview)
+{
+ if(mAlgorithm == kExpFit)
+ return std::unique_ptr<EnvelopeDetector>(safenew
+ ExpFitEnvelopeDetector(rate, mAttackTime, mReleaseTime, blockSize));
+ else
+ return std::unique_ptr<EnvelopeDetector>(safenew
+ Pt1EnvelopeDetector(rate, mAttackTime, mReleaseTime, blockSize,
+ !preview && mCompressBy != kAmplitude));
+}
+
+size_t EffectCompressor2::CalcBufferSize(double sampleRate)
+{
+ size_t capacity;
+ mLookaheadLength = CalcLookaheadLength(sampleRate);
+ capacity = mLookaheadLength +
+ size_t(float(TAU_FACTOR) * (1.0 + mAttackTime) * sampleRate);
+ if(capacity < MIN_BUFFER_CAPACITY)
+ capacity = MIN_BUFFER_CAPACITY;
+ return capacity;
+}
+
+size_t EffectCompressor2::CalcLookaheadLength(double rate)
+{
+ return std::max(0, int(round(mLookaheadTime * rate)));
+}
+
+size_t EffectCompressor2::CalcWindowLength(double rate)
+{
+ return std::max(1, int(round((mLookaheadTime + mLookbehindTime) * rate)));
+}
+
+/// Get required buffer size for the largest whole track and allocate buffers.
+/// This reduces the amount of allocations required.
+void EffectCompressor2::AllocPipeline()
+{
+ bool stereoTrackFound = false;
+ double maxSampleRate = 0;
+ size_t capacity;
+
+ mProcStereo = false;
+
+ for(auto track : mOutputTracks->Selected<WaveTrack>() + &Track::Any)
+ {
+ maxSampleRate = std::max(maxSampleRate, track->GetRate());
+
+ // There is a stereo track
+ if(track->IsLeader())
+ stereoTrackFound = true;
+ }
+
+ // Initiate a processing quad-buffer. This buffer will (most likely)
+ // be shorter than the length of the track being processed.
+ stereoTrackFound = stereoTrackFound && !mStereoInd;
+ capacity = CalcBufferSize(maxSampleRate);
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ mPipeline[i].init(capacity, stereoTrackFound);
+}
+
+void EffectCompressor2::AllocRealtimePipeline()
+{
+ mLookaheadLength = CalcLookaheadLength(mSampleRate);
+ size_t blockSize = std::max(mLookaheadLength, size_t(512));
+ if(mAlgorithm == kExpFit)
+ {
+ size_t riseTime = round(5.0 * (0.1 + mAttackTime)) * mSampleRate;
+ blockSize = std::max(blockSize, riseTime);
+ }
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ {
+ mPipeline[i].init(blockSize, true);
+ mPipeline[i].size = blockSize;
+ }
+}
+
+void EffectCompressor2::FreePipeline()
+{
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ mPipeline[i].free();
+}
+
+void EffectCompressor2::SwapPipeline()
+{
+#ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS
+ wxString blockname = wxString::Format("/tmp/blockbuf.%d.bin", buf_num);
+ std::cerr << "Writing to " << blockname << "\n" << std::flush;
+ std::fstream blockbuffer = std::fstream();
+ blockbuffer.open(blockname, std::ios::binary | std::ios::out);
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i) {
+ float val = mPipeline[i].trackSize;
+ blockbuffer.write((char*)&val, sizeof(float));
+ val = mPipeline[i].size;
+ blockbuffer.write((char*)&val, sizeof(float));
+ val = mPipeline[i].capacity();
+ blockbuffer.write((char*)&val, sizeof(float));
+ }
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ blockbuffer.write((char*)mPipeline[i][0], mPipeline[i].capacity() * sizeof(float));
+
+ wxString envname = wxString::Format("/tmp/envbuf.%d.bin", buf_num++);
+ std::cerr << "Writing to " << envname << "\n" << std::flush;
+ std::fstream envbuffer = std::fstream();
+ envbuffer.open(envname, std::ios::binary | std::ios::out);
+ envbuffer.write((char*)mEnvelope->GetBuffer(0),
+ mEnvelope->GetBlockSize() * sizeof(float));
+ envbuffer.write((char*)mEnvelope->GetBuffer(1),
+ mEnvelope->GetBlockSize() * sizeof(float));
+ envbuffer.write((char*)mEnvelope->GetBuffer(2),
+ mEnvelope->GetBlockSize() * sizeof(float));
+
+ std::cerr << "PipelineState: ";
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ std::cerr << !!mPipeline[i].size;
+ std::cerr << " ";
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ std::cerr << !!mPipeline[i].trackSize;
+
+ std::cerr << "\ntrackSize: ";
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ std::cerr << mPipeline[i].trackSize << " ";
+ std::cerr << "\ntrackPos: ";
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ std::cerr << mPipeline[i].trackPos.as_size_t() << " ";
+ std::cerr << "\nsize: ";
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ std::cerr << mPipeline[i].size << " ";
+ std::cerr << "\n" << std::flush;
+#endif
+
+ for(size_t i = 0; i < PIPELINE_DEPTH-1; ++i)
+ mPipeline[i].swap(mPipeline[i+1]);
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "\n";
+#endif
+}
+
+/// ProcessOne() takes a track, transforms it to bunch of buffer-blocks,
+/// and executes ProcessData, on it...
+bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range)
+{
+ WaveTrack* track = *range.begin();
+
+ // Transform the marker timepoints to samples
+ const auto start = track->TimeToLongSamples(mCurT0);
+ const auto end = track->TimeToLongSamples(mCurT1);
+
+ // Get the length of the buffer (as double). len is
+ // used simply to calculate a progress meter, so it is easier
+ // to make it a double now than it is to do it later
+ mTrackLen = (end - start).as_double();
+
+ // Abort if the right marker is not to the right of the left marker
+ if(mCurT1 <= mCurT0)
+ return false;
+
+ // Go through the track one buffer at a time. s counts which
+ // sample the current buffer starts at.
+ auto pos = start;
+
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "ProcLen: " << (end - start).as_size_t() << "\n" << std::flush;
+ std::cerr << "EnvBlockLen: " << mEnvelope->GetBlockSize() << "\n" << std::flush;
+ std::cerr << "PipeBlockLen: " << mPipeline[0].capacity() << "\n" << std::flush;
+ std::cerr << "LookaheadLen: " << mLookaheadLength << "\n" << std::flush;
+#endif
+
+ bool first = true;
+ mProgressVal = 0;
+#ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS
+ buf_num = 0;
+#endif
+ while(pos < end)
+ {
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "ProcessBlock at: " << pos.as_size_t() << "\n" << std::flush;
+#endif
+ StorePipeline(range);
+ SwapPipeline();
+
+ const size_t remainingLen = (end - pos).as_size_t();
+
+ // Get a block of samples (smaller than the size of the buffer)
+ // Adjust the block size if it is the final block in the track
+ const auto blockLen = limitSampleBufferSize(
+ remainingLen, mPipeline[PIPELINE_DEPTH-1].capacity());
+
+ mPipeline[PIPELINE_DEPTH-1].trackPos = pos;
+ if(!LoadPipeline(range, blockLen))
+ return false;
+
+ if(first)
+ {
+ first = false;
+ size_t sampleCount = mEnvelope->InitialConditionSize();
+ for(size_t i = 0; i < sampleCount; ++i)
+ {
+ size_t rp = i % mPipeline[PIPELINE_DEPTH-1].trackSize;
+ mEnvelope->CalcInitialCondition(
+ PreprocSample(mPipeline[PIPELINE_DEPTH-1], rp));
+ }
+ mPipeline[PIPELINE_DEPTH-2].fill(
+ mEnvelope->InitialCondition(), mProcStereo);
+ mPreproc->Reset();
+ }
+
+ if(mPipeline[0].size == 0)
+ FillPipeline();
+ else
+ ProcessPipeline();
+
+ // Increment s one blockfull of samples
+ pos += blockLen;
+
+ if(!UpdateProgress())
+ return false;
+ }
+
+ // Handle short selections
+ while(mPipeline[1].size == 0)
+ {
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "PaddingLoop: ";
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ std::cerr << !!mPipeline[i].size;
+ std::cerr << " ";
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ std::cerr << !!mPipeline[i].trackSize;
+ std::cerr << "\n" << std::flush;
+#endif
+ SwapPipeline();
+ FillPipeline();
+ if(!UpdateProgress())
+ return false;
+ }
+
+ while(PipelineHasData())
+ {
+ StorePipeline(range);
+ SwapPipeline();
+ DrainPipeline();
+ if(!UpdateProgress())
+ return false;
+ }
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "StoreLastBlock\n" << std::flush;
+#endif
+ StorePipeline(range);
+
+ // Return true because the effect processing succeeded ... unless cancelled
+ return true;
+}
+
+bool EffectCompressor2::LoadPipeline(
+ TrackIterRange<WaveTrack> range, size_t len)
+{
+ sampleCount read_size = -1;
+ sampleCount last_read_size = -1;
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "LoadBlock at: " <<
+ mPipeline[PIPELINE_DEPTH-1].trackPos.as_size_t() <<
+ " with len: " << len << "\n" << std::flush;
+#endif
+ // Get the samples from the track and put them in the buffer
+ int idx = 0;
+ for(auto channel : range)
+ {
+ channel->Get((samplePtr) mPipeline[PIPELINE_DEPTH-1][idx],
+ floatSample, mPipeline[PIPELINE_DEPTH-1].trackPos, len,
+ fillZero, true, &read_size);
+ // WaveTrack::Get returns the amount of read samples excluding zero
+ // filled samples from clip gaps. But in case of stereo tracks with
+ // assymetric gaps it still returns the same number for both channels.
+ //
+ // Fail if we read different sample count from stereo pair tracks.
+ // Ignore this check during first iteration (last_read_size == -1).
+ if(read_size != last_read_size && last_read_size.as_long_long() != -1)
+ return false;
+ mPipeline[PIPELINE_DEPTH-1].trackSize = read_size.as_size_t();
+ mPipeline[PIPELINE_DEPTH-1].size = read_size.as_size_t();
+ ++idx;
+ }
+
+ wxASSERT(mPipeline[PIPELINE_DEPTH-2].trackSize == 0 ||
+ mPipeline[PIPELINE_DEPTH-2].trackSize >=
+ mPipeline[PIPELINE_DEPTH-1].trackSize);
+ return true;
+}
+
+void EffectCompressor2::FillPipeline()
+{
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "FillBlock: " <<
+ !!mPipeline[0].size << !!mPipeline[1].size <<
+ !!mPipeline[2].size << !!mPipeline[3].size <<
+ "\n" << std::flush;
+ std::cerr << " from " << -int(mLookaheadLength)
+ << " to " << mPipeline[PIPELINE_DEPTH-1].size - mLookaheadLength << "\n" << std::flush;
+ std::cerr << "Padding from " << mPipeline[PIPELINE_DEPTH-1].trackSize
+ << " to " << mEnvelope->GetBlockSize() << "\n" << std::flush;
+#endif
+ // TODO: correct end conditions
+ mPipeline[PIPELINE_DEPTH-1].pad_to(mEnvelope->GetBlockSize(), 0, mProcStereo);
+
+ size_t length = mPipeline[PIPELINE_DEPTH-1].size;
+ for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp)
+ {
+ if(rp < length)
+ EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp);
+ else
+ EnvelopeSample(mPipeline[PIPELINE_DEPTH-1], rp % length);
+ }
+}
+
+void EffectCompressor2::ProcessPipeline()
+{
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "ProcessBlock: " <<
+ !!mPipeline[0].size << !!mPipeline[1].size <<
+ !!mPipeline[2].size << !!mPipeline[3].size <<
+ "\n" << std::flush;
+#endif
+ float env;
+ size_t length = mPipeline[0].size;
+
+ for(size_t i = 0; i < PIPELINE_DEPTH-2; ++i)
+ { wxASSERT(mPipeline[0].size == mPipeline[i+1].size); }
+
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "LookaheadLen: " << mLookaheadLength << "\n" << std::flush;
+ std::cerr << "PipeLength: " <<
+ mPipeline[0].size << " " << mPipeline[1].size << " " <<
+ mPipeline[2].size << " " << mPipeline[3].size <<
+ "\n" << std::flush;
+#endif
+
+ for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp)
+ {
+ if(rp < length)
+ env = EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp);
+ else if((rp % length) < mPipeline[PIPELINE_DEPTH-1].size)
+ env = EnvelopeSample(mPipeline[PIPELINE_DEPTH-1], rp % length);
+ else
+ // TODO: correct end condition
+ env = mEnvelope->ProcessSample(mPreproc->ProcessSample(0.0));
+ CompressSample(env, wp);
+ }
+}
+
+inline float EffectCompressor2::PreprocSample(PipelineBuffer& pbuf, size_t rp)
+{
+ if(mProcStereo)
+ return mPreproc->ProcessSample(pbuf[0][rp], pbuf[1][rp]);
+ else
+ return mPreproc->ProcessSample(pbuf[0][rp]);
+}
+
+inline float EffectCompressor2::EnvelopeSample(PipelineBuffer& pbuf, size_t rp)
+{
+ return mEnvelope->ProcessSample(PreprocSample(pbuf, rp));
+}
+
+inline void EffectCompressor2::CompressSample(float env, size_t wp)
+{
+ float gain = CompressorGain(env);
+
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+ float ThresholdDB = mThresholdDB;
+ float Ratio = mRatio;
+ float KneeWidthDB = mKneeWidthDB;
+ float AttackTime = mAttackTime;
+ float ReleaseTime = mReleaseTime;
+ float LookaheadTime = mLookaheadTime;
+ float LookbehindTime = mLookbehindTime;
+ float OutputGainDB = mOutputGainDB;
+
+ debugfile.write((char*)&ThresholdDB, sizeof(float));
+ debugfile.write((char*)&Ratio, sizeof(float));
+ debugfile.write((char*)&KneeWidthDB, sizeof(float));
+ debugfile.write((char*)&AttackTime, sizeof(float));
+ debugfile.write((char*)&ReleaseTime, sizeof(float));
+ debugfile.write((char*)&LookaheadTime, sizeof(float));
+ debugfile.write((char*)&LookbehindTime, sizeof(float));
+ debugfile.write((char*)&OutputGainDB, sizeof(float));
+ debugfile.write((char*)&mPipeline[0][0][wp], sizeof(float));
+ if(mProcStereo)
+ debugfile.write((char*)&mPipeline[0][1][wp], sizeof(float));
+ debugfile.write((char*)&env, sizeof(float));
+ debugfile.write((char*)&gain, sizeof(float));
+#endif
+
+#ifdef DEBUG_COMPRESSOR2_ENV
+ if(wp < 100)
+ mPipeline[0][0][wp] = 0;
+ else
+ mPipeline[0][0][wp] = env;
+#else
+ mPipeline[0][0][wp] = mPipeline[0][0][wp] * gain;
+#endif
+ if(mProcStereo)
+ mPipeline[0][1][wp] = mPipeline[0][1][wp] * gain;
+
+#ifdef DEBUG_COMPRESSOR2_TRACE2
+ debugfile.write((char*)&mPipeline[0][0][wp], sizeof(float));
+ if(mProcStereo)
+ debugfile.write((char*)&mPipeline[0][1][wp], sizeof(float));
+#endif
+}
+
+bool EffectCompressor2::PipelineHasData()
+{
+ for(size_t i = 0; i < PIPELINE_DEPTH; ++i)
+ {
+ if(mPipeline[i].size != 0)
+ return true;
+ }
+ return false;
+}
+
+void EffectCompressor2::DrainPipeline()
+{
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "DrainBlock: " <<
+ !!mPipeline[0].size << !!mPipeline[1].size <<
+ !!mPipeline[2].size << !!mPipeline[3].size <<
+ "\n" << std::flush;
+ bool once = false;
+#endif
+
+ float env;
+ size_t length = mPipeline[0].size;
+ size_t length2 = mPipeline[PIPELINE_DEPTH-2].size;
+
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "LookaheadLen: " << mLookaheadLength << "\n" << std::flush;
+ std::cerr << "PipeLength: " <<
+ mPipeline[0].size << " " << mPipeline[1].size << " " <<
+ mPipeline[2].size << " " << mPipeline[3].size <<
+ "\n" << std::flush;
+#endif
+
+ for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp)
+ {
+ if(rp < length2 && mPipeline[PIPELINE_DEPTH-2].size != 0)
+ {
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ if(!once)
+ {
+ once = true;
+ std::cerr << "Draining overlapping buffer\n" << std::flush;
+ }
+#endif
+ env = EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp);
+ }
+ else
+ // TODO: correct end condition
+ env = mEnvelope->ProcessSample(mPreproc->ProcessSample(0.0));
+ CompressSample(env, wp);
+ }
+}
+
+void EffectCompressor2::StorePipeline(TrackIterRange<WaveTrack> range)
+{
+#ifdef DEBUG_COMPRESSOR2_TRACE
+ std::cerr << "StoreBlock at: " << mPipeline[0].trackPos.as_size_t() <<
+ " with len: " << mPipeline[0].trackSize << "\n" << std::flush;
+#endif
+
+ int idx = 0;
+ for(auto channel : range)
+ {
+ // Copy the newly-changed samples back onto the track.
+ channel->Set((samplePtr) mPipeline[0][idx],
+ floatSample, mPipeline[0].trackPos, mPipeline[0].trackSize);
+ ++idx;
+ }
+ mPipeline[0].trackSize = 0;
+ mPipeline[0].size = 0;
+}
+
+bool EffectCompressor2::UpdateProgress()
+{
+ mProgressVal +=
+ (double(1+mProcStereo) * mPipeline[PIPELINE_DEPTH-1].trackSize)
+ / (double(GetNumWaveTracks()) * mTrackLen);
+ return !TotalProgress(mProgressVal);
+}
+
+void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt))
+{
+ if(!mIgnoreGuiEvents)
+ TransferDataFromWindow();
+ UpdateUI();
+}
+
+void EffectCompressor2::UpdateUI()
+{
+ UpdateCompressorPlot();
+ UpdateResponsePlot();
+ if(mEnvelope.get() != nullptr)
+ UpdateRealtimeParams();
+}
+
+void EffectCompressor2::UpdateCompressorPlot()
+{
+ PlotData* plot;
+ plot = mGainPlot->GetPlotData(0);
+ wxASSERT(plot->xdata.size() == plot->ydata.size());
+
+ if(!IsInRange(mThresholdDB, MIN_Threshold, MAX_Threshold))
+ return;
+ if(!IsInRange(mRatio, MIN_Ratio, MAX_Ratio))
+ return;
+ if(!IsInRange(mKneeWidthDB, MIN_KneeWidth, MAX_KneeWidth))
+ return;
+ if(!IsInRange(mOutputGainDB, MIN_OutputGain, MAX_OutputGain))
+ return;
+
+ size_t xsize = plot->xdata.size();
+ for(size_t i = 0; i < xsize; ++i)
+ plot->ydata[i] = plot->xdata[i] +
+ LINEAR_TO_DB(CompressorGain(DB_TO_LINEAR(plot->xdata[i])));
+
+ mGainPlot->SetName(XO("Compressor gain reduction: %.1f dB").
+ Format(plot->ydata[xsize-1]));
+ mGainPlot->Refresh(false);
+}
+
+void EffectCompressor2::UpdateResponsePlot()
+{
+ PlotData* plot;
+ plot = mResponsePlot->GetPlotData(1);
+ wxASSERT(plot->xdata.size() == plot->ydata.size());
+
+ if(!IsInRange(mAttackTime, MIN_AttackTime, MAX_AttackTime))
+ return;
+ if(!IsInRange(mReleaseTime, MIN_ReleaseTime, MAX_ReleaseTime))
+ return;
+ if(!IsInRange(mLookaheadTime, MIN_LookaheadTime, MAX_LookaheadTime))
+ return;
+ if(!IsInRange(mLookbehindTime, MIN_LookbehindTime, MAX_LookbehindTime))
+ return;
+
+ std::unique_ptr<SamplePreprocessor> preproc;
+ std::unique_ptr<EnvelopeDetector> envelope;
+ float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME;
+
+ size_t lookahead_size = CalcLookaheadLength(plot_rate);
+ lookahead_size -= (lookahead_size > 0);
+ ssize_t block_size = float(TAU_FACTOR) * (mAttackTime + 1.0) * plot_rate;
+
+ preproc = InitPreprocessor(plot_rate, true);
+ envelope = InitEnvelope(plot_rate, block_size, true);
+
+ preproc->Reset(0.1);
+ envelope->Reset(0.1);
+
+ ssize_t step_start = RESPONSE_PLOT_STEP_START * plot_rate - lookahead_size;
+ ssize_t step_stop = RESPONSE_PLOT_STEP_STOP * plot_rate - lookahead_size;
+
+ ssize_t xsize = plot->xdata.size();
+ for(ssize_t i = -lookahead_size; i < 2*block_size; ++i)
+ {
+ if(i < step_start || i > step_stop)
+ envelope->ProcessSample(preproc->ProcessSample(0.1));
+ else
+ envelope->ProcessSample(preproc->ProcessSample(1));
+ }
+
+ for(ssize_t i = 0; i < xsize; ++i)
+ {
+ float x = 1;
+ if(i < RESPONSE_PLOT_STEP_START * plot_rate ||
+ i > RESPONSE_PLOT_STEP_STOP * plot_rate)
+ x = 0.1;
+
+ plot->ydata[i] = x * CompressorGain(
+ envelope->ProcessSample(preproc->ProcessSample(0.1)));
+ }
+
+ mResponsePlot->Refresh(false);
+}
+
+void EffectCompressor2::UpdateRealtimeParams()
+{
+ std::lock_guard<std::mutex> guard(mRealtimeMutex);
+ size_t window_size = CalcWindowLength(mSampleRate);
+ mLookaheadLength = CalcLookaheadLength(mSampleRate);
+ mPreproc->SetWindowSize(window_size);
+ mEnvelope->SetParams(mSampleRate, mAttackTime, mReleaseTime);
+}
A src/effects/Compressor2.h => src/effects/Compressor2.h +294 -0
@@ 0,0 1,294 @@
+/**********************************************************************
+
+ Audacity: A Digital Audio Editor
+
+ Compressor2.h
+
+ Max Maisel (based on Compressor effect)
+
+**********************************************************************/
+
+#ifndef __AUDACITY_EFFECT_COMPRESSOR2__
+#define __AUDACITY_EFFECT_COMPRESSOR2__
+
+#include <wx/checkbox.h>
+#include <wx/choice.h>
+#include <wx/event.h>
+#include <wx/stattext.h>
+#include <wx/string.h>
+#include <wx/textctrl.h>
+
+#include "Effect.h"
+
+class Plot;
+class ShuttleGui;
+class SliderTextCtrl;
+
+class SamplePreprocessor
+{
+ public:
+ virtual float ProcessSample(float value) = 0;
+ virtual float ProcessSample(float valueL, float valueR) = 0;
+ virtual void Reset(float value = 0) = 0;
+ virtual void SetWindowSize(size_t windowSize) = 0;
+};
+
+class SlidingRmsPreprocessor : public SamplePreprocessor
+{
+ public:
+ SlidingRmsPreprocessor(size_t windowSize, float gain = 2.0);
+
+ virtual float ProcessSample(float value);
+ virtual float ProcessSample(float valueL, float valueR);
+ virtual void Reset(float value = 0);
+ virtual void SetWindowSize(size_t windowSize);
+
+ static const size_t REFRESH_WINDOW_EVERY = 1048576; // 1 MB
+
+ private:
+ float mSum;
+ float mGain;
+ std::vector<float> mWindow;
+ size_t mPos;
+ size_t mInsertCount;
+
+ inline float DoProcessSample(float value);
+ void Refresh();
+};
+
+class SlidingMaxPreprocessor : public SamplePreprocessor
+{
+ public:
+ SlidingMaxPreprocessor(size_t windowSize);
+
+ virtual float ProcessSample(float value);
+ virtual float ProcessSample(float valueL, float valueR);
+ virtual void Reset(float value = 0);
+ virtual void SetWindowSize(size_t windowSize);
+
+ private:
+ std::vector<float> mWindow;
+ std::vector<float> mMaxes;
+ size_t mPos;
+
+ inline float DoProcessSample(float value);
+};
+
+class EnvelopeDetector
+{
+ public:
+ EnvelopeDetector(size_t buffer_size);
+
+ float ProcessSample(float value);
+ size_t GetBlockSize() const;
+ const float* GetBuffer(int idx) const;
+
+ virtual void CalcInitialCondition(float value);
+ inline float InitialCondition() const { return mInitialCondition; }
+ inline size_t InitialConditionSize() const { return mInitialBlockSize; }
+
+ virtual void Reset(float value = 0) = 0;
+ virtual void SetParams(float sampleRate, float attackTime,
+ float releaseTime) = 0;
+
+ virtual float AttackFactor();
+ virtual float DecayFactor();
+
+ protected:
+ size_t mPos;
+ float mInitialCondition;
+ size_t mInitialBlockSize;
+ std::vector<float> mLookaheadBuffer;
+ std::vector<float> mProcessingBuffer;
+ std::vector<float> mProcessedBuffer;
+
+ virtual void Follow() = 0;
+};
+
+class ExpFitEnvelopeDetector : public EnvelopeDetector
+{
+ public:
+ ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime,
+ size_t buffer_size);
+
+ virtual void Reset(float value);
+ virtual void SetParams(float sampleRate, float attackTime,
+ float releaseTime);
+
+ private:
+ double mAttackFactor;
+ double mReleaseFactor;
+
+ virtual void Follow();
+};
+
+class Pt1EnvelopeDetector : public EnvelopeDetector
+{
+ public:
+ Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime,
+ size_t buffer_size, bool correctGain = true);
+ virtual void CalcInitialCondition(float value);
+
+ virtual void Reset(float value);
+ virtual void SetParams(float sampleRate, float attackTime,
+ float releaseTime);
+ virtual float AttackFactor();
+ virtual float DecayFactor();
+
+ private:
+ bool mCorrectGain;
+ double mGainCorrection;
+ double mAttackFactor;
+ double mReleaseFactor;
+
+ virtual void Follow();
+};
+
+struct PipelineBuffer
+{
+ public:
+ sampleCount trackPos;
+ size_t trackSize;
+ size_t size;
+
+ inline float* operator[](size_t idx)
+ { return mBlockBuffer[idx].get(); }
+
+ void pad_to(size_t len, float value, bool stereo);
+ void swap(PipelineBuffer& other);
+ void init(size_t size, bool stereo);
+ void fill(float value, bool stereo);
+ inline size_t capacity() const { return mCapacity; }
+ void free();
+
+ private:
+ size_t mCapacity;
+ Floats mBlockBuffer[2];
+};
+
+class EffectCompressor2 final : public Effect
+{
+public:
+ static const ComponentInterfaceSymbol Symbol;
+
+ EffectCompressor2();
+ virtual ~EffectCompressor2();
+
+ // ComponentInterface implementation
+
+ ComponentInterfaceSymbol GetSymbol() override;
+ TranslatableString GetDescription() override;
+ ManualPageID ManualPage() override;
+
+ // EffectDefinitionInterface implementation
+
+ EffectType GetType() override;
+ bool SupportsRealtime() override;
+
+ // EffectClientInterface implementation
+
+ unsigned GetAudioInCount() override;
+ unsigned GetAudioOutCount() override;
+ bool RealtimeInitialize() override;
+ bool RealtimeAddProcessor(unsigned numChannels, float sampleRate) override;
+ bool RealtimeFinalize() override;
+ size_t RealtimeProcess(int group, float **inbuf, float **outbuf,
+ size_t numSamples) override;
+ bool DefineParams( ShuttleParams & S ) override;
+ bool GetAutomationParameters(CommandParameters & parms) override;
+ bool SetAutomationParameters(CommandParameters & parms) override;
+ RegistryPaths GetFactoryPresets() override;
+ bool LoadFactoryPreset(int id) override;
+
+ // Effect implementation
+
+ bool CheckWhetherSkipEffect() override;
+ bool Startup() override;
+ bool Process() override;
+ void PopulateOrExchange(ShuttleGui & S) override;
+ bool TransferDataToWindow() override;
+ bool TransferDataFromWindow() override;
+
+private:
+ // EffectCompressor2 implementation
+ double CompressorGain(double env);
+ std::unique_ptr<SamplePreprocessor> InitPreprocessor(
+ double rate, bool preview = false);
+ std::unique_ptr<EnvelopeDetector> InitEnvelope(
+ double rate, size_t blockSize = 0, bool preview = false);
+ size_t CalcBufferSize(double sampleRate);
+
+ inline size_t CalcLookaheadLength(double rate);
+ inline size_t CalcWindowLength(double rate);
+
+ void AllocPipeline();
+ void AllocRealtimePipeline();
+ void FreePipeline();
+ void SwapPipeline();
+ bool ProcessOne(TrackIterRange<WaveTrack> range);
+ bool LoadPipeline(TrackIterRange<WaveTrack> range, size_t len);
+ void FillPipeline();
+ void ProcessPipeline();
+ inline float PreprocSample(PipelineBuffer& pbuf, size_t rp);
+ inline float EnvelopeSample(PipelineBuffer& pbuf, size_t rp);
+ inline void CompressSample(float env, size_t wp);
+ bool PipelineHasData();
+ void DrainPipeline();
+ void StorePipeline(TrackIterRange<WaveTrack> range);
+
+ bool UpdateProgress();
+ void OnUpdateUI(wxCommandEvent & evt);
+ void UpdateUI();
+ void UpdateCompressorPlot();
+ void UpdateResponsePlot();
+ void UpdateRealtimeParams();
+
+ static const int TAU_FACTOR = 5;
+ static const size_t MIN_BUFFER_CAPACITY = 1048576; // 1MB
+
+ static const size_t PIPELINE_DEPTH = 4;
+ PipelineBuffer mPipeline[PIPELINE_DEPTH];
+
+ double mCurT0;
+ double mCurT1;
+ double mProgressVal;
+ double mTrackLen;
+ bool mProcStereo;
+
+ std::mutex mRealtimeMutex;
+ std::unique_ptr<SamplePreprocessor> mPreproc;
+ std::unique_ptr<EnvelopeDetector> mEnvelope;
+
+ int mAlgorithm;
+ int mCompressBy;
+ bool mStereoInd;
+
+ double mThresholdDB;
+ double mRatio;
+ double mKneeWidthDB;
+ double mAttackTime;
+ double mReleaseTime;
+ double mLookaheadTime;
+ double mLookbehindTime;
+ double mOutputGainDB;
+
+ // cached intermediate values
+ size_t mLookaheadLength;
+
+ static const size_t RESPONSE_PLOT_SAMPLES = 200;
+ static const size_t RESPONSE_PLOT_TIME = 5;
+ static const size_t RESPONSE_PLOT_STEP_START = 2;
+ static const size_t RESPONSE_PLOT_STEP_STOP = 3;
+
+ bool mIgnoreGuiEvents;
+ Plot* mGainPlot;
+ Plot* mResponsePlot;
+ wxChoice* mAlgorithmCtrl;
+ wxChoice* mPreprocCtrl;
+ SliderTextCtrl* mAttackTimeCtrl;
+ SliderTextCtrl* mLookaheadTimeCtrl;
+
+ DECLARE_EVENT_TABLE()
+};
+
+#endif
A src/widgets/Plot.cpp => src/widgets/Plot.cpp +134 -0
@@ 0,0 1,134 @@
+/**********************************************************************
+
+ Audacity: A Digital Audio Editor
+
+ Plot.cpp
+
+ Max Maisel
+
+*******************************************************************//**
+
+\class Plot
+\brief A customizable generic plot widget.
+
+*//*******************************************************************/
+
+
+#include "Plot.h"
+#include "Ruler.h"
+#include "../AColor.h"
+#include "../Theme.h"
+#include "../AllThemeResources.h"
+
+#include <wx/brush.h>
+#include <wx/dcclient.h>
+#include <wx/dcmemory.h>
+
+Plot::Plot(wxWindow *parent, wxWindowID winid,
+ float x_min, float x_max, float y_min, float y_max,
+ const TranslatableString& xlabel, const TranslatableString& ylabel,
+ int xformat, int yformat, int count,
+ const wxPoint& pos, const wxSize& size, long style)
+ :
+ wxPanelWrapper(parent, winid, pos, size, style),
+ m_xmin(x_min), m_xmax(x_max), m_ymin(y_min), m_ymax(y_max),
+ m_plots(count)
+{
+ m_xruler = std::unique_ptr<Ruler>(safenew Ruler);
+ m_xruler->SetOrientation(wxHORIZONTAL);
+ m_xruler->SetFormat(static_cast<Ruler::RulerFormat>(xformat));
+ m_xruler->SetUnits(xlabel);
+ m_xruler->SetFlip(true);
+
+ m_yruler = std::unique_ptr<Ruler>(safenew Ruler);
+ m_yruler->SetOrientation(wxVERTICAL);
+ m_yruler->SetFormat(static_cast<Ruler::RulerFormat>(yformat));
+ m_yruler->SetUnits(ylabel);
+}
+
+void Plot::OnPaint(wxPaintEvent & evt)
+{
+ wxPaintDC dc(this);
+
+ int width, height;
+ GetSize(&width, &height);
+
+#if defined(__WXMSW__)
+ dc.Clear();
+#endif
+
+ // Ruler
+ int w = 0;
+ int h = 0;
+
+ m_xruler->SetBounds(0, 0, width, height);
+ m_xruler->SetRange(m_xmin, m_xmax);
+ m_xruler->GetMaxSize(NULL, &h);
+
+ m_yruler->SetBounds(0, 0, width, height);
+ m_yruler->SetRange(m_ymax, m_ymin);
+ m_yruler->GetMaxSize(&w, NULL);
+
+ m_xruler->SetBounds(w, height - h, width, height);
+ m_yruler->SetBounds(0, 0, w, height - h);
+
+ m_xruler->SetTickColour( theTheme.Colour( clrGraphLabels ));
+ m_yruler->SetTickColour( theTheme.Colour( clrGraphLabels ));
+
+ wxRect border;
+ border.x = w;
+ border.y = 0;
+ border.width = width - w;
+ border.height = height - h + 1;
+
+ dc.SetBrush(*wxWHITE_BRUSH);
+ dc.SetPen(*wxTRANSPARENT_PEN);
+ dc.DrawRectangle(border);
+
+ m_xruler->DrawGrid(dc, border.height, true, true, border.x, border.y);
+ m_yruler->DrawGrid(dc, border.width, true, true, border.x, border.y);
+
+ for(const auto& plot : m_plots)
+ {
+ wxASSERT(plot.xdata.size() == plot.ydata.size());
+ if(plot.xdata.size() == 0)
+ continue;
+ dc.SetPen(*plot.pen);
+
+ size_t xsize = plot.xdata.size();
+ for(size_t i = 1; i < xsize; ++i)
+ {
+ AColor::Line(dc,
+ XToScreen(plot.xdata[i-1], border),
+ YToScreen(plot.ydata[i-1], border),
+ XToScreen(plot.xdata[i], border),
+ YToScreen(plot.ydata[i], border));
+ }
+ }
+
+ dc.SetBrush(*wxTRANSPARENT_BRUSH);
+ dc.SetPen(*wxBLACK_PEN);
+ dc.DrawRectangle(border);
+ m_xruler->Draw(dc);
+ m_yruler->Draw(dc);
+}
+
+void Plot::OnSize(wxSizeEvent & evt)
+{
+ Refresh(false);
+}
+
+int Plot::XToScreen(float x, wxRect& rect)
+{
+ return rect.x + lrint((x-m_xmin)*rect.width/(m_xmax-m_xmin));
+}
+
+int Plot::YToScreen(float y, wxRect& rect)
+{
+ return rect.y + rect.height - lrint((y-m_ymin)*rect.height/(m_ymax-m_ymin));
+}
+
+BEGIN_EVENT_TABLE(Plot, wxPanelWrapper)
+ EVT_PAINT(Plot::OnPaint)
+ EVT_SIZE(Plot::OnSize)
+END_EVENT_TABLE()
A src/widgets/Plot.h => src/widgets/Plot.h +58 -0
@@ 0,0 1,58 @@
+/**********************************************************************
+
+ Audacity: A Digital Audio Editor
+
+ Plot.h
+
+ Max Maisel
+
+ This class is a generic plot.
+
+**********************************************************************/
+
+#ifndef __AUDACITY_PLOT__
+#define __AUDACITY_PLOT__
+
+#include "wxPanelWrapper.h" // to inherit
+
+#include "MemoryX.h"
+
+class Ruler;
+
+struct PlotData
+{
+ std::unique_ptr<wxPen> pen;
+ std::vector<float> xdata;
+ std::vector<float> ydata;
+};
+
+class Plot : public wxPanelWrapper
+{
+ public:
+ Plot(wxWindow *parent, wxWindowID winid,
+ float x_min, float x_max, float y_min, float y_max,
+ const TranslatableString& xlabel, const TranslatableString& ylabel,
+ int xformat = 1, int yformat = 1, //Ruler::RealFormat
+ int count = 1, const wxPoint& pos = wxDefaultPosition,
+ const wxSize& size = wxDefaultSize,
+ long style = wxTAB_TRAVERSAL | wxNO_BORDER);
+
+ inline PlotData* GetPlotData(int id)
+ { return &m_plots[id]; }
+
+ private:
+ void OnPaint(wxPaintEvent & evt);
+ void OnSize(wxSizeEvent & evt);
+
+ float m_xmin, m_xmax;
+ float m_ymin, m_ymax;
+ std::vector<PlotData> m_plots;
+ std::unique_ptr<Ruler> m_xruler, m_yruler;
+
+ int XToScreen(float x, wxRect& rect);
+ int YToScreen(float y, wxRect& rect);
+
+ DECLARE_EVENT_TABLE()
+};
+
+#endif
A src/widgets/SliderTextCtrl.cpp => src/widgets/SliderTextCtrl.cpp +167 -0
@@ 0,0 1,167 @@
+/**********************************************************************
+
+ Audacity: A Digital Audio Editor
+
+ SliderTextCtrl.cpp
+
+ Max Maisel
+
+*******************************************************************//**
+
+\class SliderTextCtrl
+\brief A slider with connected text box.
+
+*//*******************************************************************/
+
+
+#include "SliderTextCtrl.h"
+
+#include <wx/defs.h>
+#include <wx/panel.h>
+#include <wx/sizer.h>
+#include <wx/slider.h>
+#include <wx/textctrl.h>
+#include <wx/valnum.h>
+
+wxDEFINE_EVENT(cEVT_SLIDERTEXT, wxCommandEvent);
+
+SliderTextCtrl::SliderTextCtrl(wxWindow *parent, wxWindowID winid,
+ double value, double min, double max, int precision, double scale,
+ double offset, const wxPoint& pos, const wxSize& size, long style,
+ double* varValue)
+ : wxPanelWrapper(parent, winid, pos, size, wxWS_EX_VALIDATE_RECURSIVELY)
+{
+ m_log = style & LOG;
+ m_int = style & INT;
+ m_value = value;
+ m_min = min;
+ m_max = max;
+ m_zero = -std::numeric_limits<double>::infinity();
+ m_offset = offset;
+
+ if(m_int)
+ {
+ precision = 0;
+ m_format = "%d";
+ }
+ else
+ m_format = wxString::Format("%%.%df", precision);
+
+ if(scale == 0)
+ m_scale = pow(10, precision);
+ else
+ m_scale = scale;
+
+ wxFloatingPointValidator<double> validator(precision, varValue);
+
+ if(m_log)
+ {
+ if(min <= 0.0)
+ {
+ m_zero = -double(precision) - 1.0 / m_scale;
+ min = m_zero;
+ }
+ else
+ min = log10(min + m_offset);
+
+ if(value <= 0.0)
+ value = m_zero;
+ else
+ value = log10(value + m_offset);
+ max = log10(max + m_offset);
+ }
+
+ m_sizer = safenew wxBoxSizer(
+ style & HORIZONTAL ? wxHORIZONTAL : wxVERTICAL);
+ m_slider = safenew wxSlider(this, ID_SLIDER,
+ round(value * m_scale), floor(min * m_scale), ceil(max * m_scale),
+ wxDefaultPosition, wxDefaultSize,
+ style & HORIZONTAL ? wxSL_HORIZONTAL : wxSL_VERTICAL);
+ m_textbox = safenew wxTextCtrl(this, ID_TEXTBOX, wxEmptyString,
+ wxDefaultPosition, wxDefaultSize, 0, validator);
+
+ m_textbox->ChangeValue(FormatValue());
+ m_textbox->Bind(wxEVT_KILL_FOCUS, &SliderTextCtrl::OnKillFocus, this);
+
+ m_sizer->Add(m_slider, 1, wxEXPAND);
+ m_sizer->Add(m_textbox, 0, wxEXPAND);
+
+ SetSizer(m_sizer);
+}
+
+void SliderTextCtrl::SetMinTextboxWidth(int width)
+{
+ wxSize size = GetMinSize();
+ size.SetWidth(width);
+ m_textbox->SetMinSize(size);
+}
+
+double SliderTextCtrl::GetValue() const
+{
+ return m_value;
+}
+
+void SliderTextCtrl::SetValue(double value)
+{
+ m_value = value;
+ m_textbox->ChangeValue(FormatValue());
+}
+
+void SliderTextCtrl::OnTextChange(wxCommandEvent& event)
+{
+ double value;
+ m_textbox->GetValue().ToDouble(&value);
+ m_value = std::min(value, m_max);
+ m_value = std::max(m_value, m_min);
+ if(m_log)
+ {
+ if(m_value == 0.0)
+ value = m_zero;
+ else
+ value = log10(m_value + m_offset);
+ }
+ m_slider->SetValue(round(value * m_scale));
+ event.SetEventType(cEVT_SLIDERTEXT);
+ event.Skip();
+}
+
+void SliderTextCtrl::OnSlider(wxCommandEvent& event)
+{
+ m_value = m_slider->GetValue() / m_scale;
+ if(m_log)
+ {
+ if(m_value <= m_zero)
+ m_value = 0.0;
+ else
+ {
+ m_value = pow(10.0, m_value) - m_offset;
+ m_value = std::max(m_min, m_value);
+ m_value = std::min(m_max, m_value);
+ }
+ }
+ m_textbox->ChangeValue(FormatValue());
+ m_textbox->SetSelection(-1, -1);
+ event.SetEventType(cEVT_SLIDERTEXT);
+ event.Skip();
+}
+
+void SliderTextCtrl::OnKillFocus(wxFocusEvent& _)
+{
+ m_textbox->ChangeValue(FormatValue());
+ wxCommandEvent event(cEVT_SLIDERTEXT, GetId());
+ wxPostEvent(GetParent(), event);
+}
+
+wxString SliderTextCtrl::FormatValue() const
+{
+ int v = m_value;
+ if(m_int)
+ return wxString::Format(m_format, v);
+ else
+ return wxString::Format(m_format, m_value);
+}
+
+BEGIN_EVENT_TABLE(SliderTextCtrl, wxControl)
+ EVT_TEXT(ID_TEXTBOX, SliderTextCtrl::OnTextChange)
+ EVT_SLIDER(ID_SLIDER, SliderTextCtrl::OnSlider)
+END_EVENT_TABLE()
A src/widgets/SliderTextCtrl.h => src/widgets/SliderTextCtrl.h +79 -0
@@ 0,0 1,79 @@
+/**********************************************************************
+
+ Audacity: A Digital Audio Editor
+
+ SliderTextCtrl.h
+
+ Max Maisel
+
+ This class is a custom slider.
+
+**********************************************************************/
+
+#ifndef __AUDACITY_SLIDERTEXTCTRL__
+#define __AUDACITY_SLIDERTEXTCTRL__
+
+#include "wxPanelWrapper.h" // to inherit
+
+class wxSizer;
+class wxSlider;
+class wxTextCtrl;
+
+wxDECLARE_EVENT(cEVT_SLIDERTEXT, wxCommandEvent);
+
+#define EVT_SLIDERTEXT(winid, func) wx__DECLARE_EVT1( \
+ cEVT_SLIDERTEXT, winid, wxCommandEventHandler(func))
+
+class SliderTextCtrl : public wxPanelWrapper
+{
+ public:
+ enum Styles
+ {
+ HORIZONTAL = 1,
+ VERTICAL = 2,
+ LOG = 4,
+ INT = 8,
+ };
+
+ SliderTextCtrl(wxWindow *parent, wxWindowID winid,
+ double value, double min, double max, int precision = 2,
+ double scale = 0, double offset = 0,
+ const wxPoint& pos = wxDefaultPosition,
+ const wxSize& size = wxDefaultSize, long style = HORIZONTAL,
+ double* varValue = NULL);
+
+ void SetMinTextboxWidth(int width);
+
+ double GetValue() const;
+ void SetValue(double value);
+
+ private:
+ void OnTextChange(wxCommandEvent& event);
+ void OnSlider(wxCommandEvent& event);
+ void OnKillFocus(wxFocusEvent& event);
+ wxString FormatValue() const;
+
+ enum
+ {
+ ID_SLIDER = 1,
+ ID_TEXTBOX
+ };
+
+ wxSizer* m_sizer;
+ wxSlider* m_slider;
+ wxTextCtrl* m_textbox;
+
+ bool m_log;
+ bool m_int;
+ double m_value;
+ double m_scale;
+ double m_min;
+ double m_max;
+ double m_zero;
+ double m_offset;
+ wxString m_format;
+
+ DECLARE_EVENT_TABLE()
+};
+
+#endif
A tests/octave/.gitignore => tests/octave/.gitignore +1 -0
A tests/octave/compressor2_test.m => tests/octave/compressor2_test.m +305 -0
@@ 0,0 1,305 @@
+## Audacity Compressor2 effect unit test
+#
+# Max Maisel
+#
+# This tests the Compressor effect with various pseudo-random mono and stereo
+# noise sequences and sinewaves. The test sequences have different amplitudes
+# per channel and sometimes a DC component.
+#
+# Avoid large parameters for AttackTime, ReleaseTime and LookaroundTime in
+# this script as settling behaviour is different and will cause test failure.
+#
+
+pkg load signal;
+pkg load image;
+
+printf("Running Compressor effect tests.\n");
+
+EXPORT_TEST_SIGNALS = true;
+
+## PT1 envelope helper function for symmetric attack and release times.
+function y = env_PT1(x, fs, t_ar, gain = 0)
+ T = 1/(t_ar*fs);
+ si = mean(mean(abs(x(1:fs*t_ar/20,:))));
+ c = (gain != 0) * gain + (gain == 0) * (1.0 + exp(t_ar/30.0));
+ y = c*filter(T, [1 T-1], mean(abs(x), 2), si*c);
+end
+
+## PT1 envelope helper function for asymmetric attack and release times.
+# This function is much slower than the symmetric counterpart.
+function y = env_PT1_asym(x, fs, t_a, t_r, gain = 0)
+ C_a = 1.0 / (fs*t_a);
+ C_r = 1.0 / (fs*t_r);
+ si = mean(mean(abs(x(1:fs*t_a/20,:))));
+ c = (gain != 0) * gain + (gain == 0) * (1.0 + exp(t_a/30.0));
+
+ x_m = mean(abs(x), 2);
+ y = zeros(length(x_m), 1);
+ level = si;
+
+ for k = 1:1:length(x_m)
+ if x_m(k) >= level
+ level = level + C_a * (x_m(k) - level);
+ else
+ level = level + C_r * (x_m(k) - level);
+ end
+ y(k) = c * level;
+ end
+end
+
+## Compressor gain helper function
+function gain = comp_gain(env, thresh_DB, ratio, kneeW_DB, outG_DB)
+ env_DB = 20*log10(env);
+ kneeCond_DB = 2*(env_DB-thresh_DB);
+
+ belowKnee = kneeCond_DB < -kneeW_DB;
+ aboveKnee = kneeCond_DB >= kneeW_DB;
+ # & is element-wise &&
+ withinKnee = (kneeCond_DB >= -kneeW_DB) & (kneeCond_DB < kneeW_DB);
+
+ gain_DB = zeros(size(env));
+ gain_DB(belowKnee) = outG_DB;
+ gain_DB(aboveKnee) = thresh_DB + ...
+ (env_DB(aboveKnee) - thresh_DB) / ratio + ...
+ outG_DB - env_DB(aboveKnee);
+ # Prevent division by zero
+ kneeW_DB(kneeW_DB==0) = 0.000001;
+ gain_DB(withinKnee) = (1/ratio-1) * ...
+ (env_DB(withinKnee) - thresh_DB + kneeW_DB/2).^2 / ...
+ (2*kneeW_DB) + outG_DB;
+
+ gain = 10.^(gain_DB/20);
+end
+
+# Ignore first samples due to settling effects helper function
+function y = settled(x, fs = 44100, tau = 1, both = 0)
+ y = x(round(3*fs*tau):length(x)-round(3*fs*tau*both),:);
+end
+
+# XXX: This Octave function is REALLY slow.
+# Maximum value of n*fs < 10000
+function y = lookaround_RMS(x, fs, n1, n2)
+ kernel = cat(1, zeros(n2*fs,1), ones(n1*fs, 1), ones(n2*fs, 1), zeros(n1*fs, 1));
+ y = zeros(size(x));
+ for i=1:1:size(x)(2)
+ y(:,i) = conv(x(:,i).^2, kernel, 'same')./(n1+n2)/fs;
+ end
+
+ y = 2*sqrt(sum(y, 2)./size(y)(2));
+end
+
+# XXX: This Octave function is REALLY slow.
+# Maximum value of n*fs < 10000
+function y = lookaround_max(x, fs, n1, n2)
+ kernel = cat(1, zeros(n2*fs,1), ones(n1*fs, 1), ones(n2*fs, 1), zeros(n1*fs, 1));
+ x = mean(abs(x), 2);
+ y = imdilate(x, kernel);
+end
+
+################################################################################
+
+## Test Compressor, mono thresholding
+CURRENT_TEST = "Compressor2, mono thresholding";
+fs = 44100;
+
+randn("seed", 1);
+x1 = 0.01*randn(30*fs,1) .* sin(2*pi/fs/35*(1:1:30*fs)).';
+
+remove_all_tracks();
+x = export_to_aud(x1, fs, name = "Compressor-threshold-test.wav");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=0 AttackTime=0.1 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
+y = import_from_aud(1);
+
+# All input samples are below threshold so output must be equal to input.
+do_test_equ(x, y);
+
+## Test Compressor, mono compression PT1 - no lookaround
+CURRENT_TEST = "Compressor2, mono compression PT1";
+x1 = x1.*10;
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2.5 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0.0 LookbehindTime=0.0 KneeWidth=12\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 1), ...
+ comp_gain(settled(env_PT1(x, fs, 0.5, 1), fs, 1), -20, 2.5, 12, 0).* ...
+ settled(x, fs, 1));
+
+## Test Compressor, mono compression PT1 - sinewave - no lookaround
+CURRENT_TEST = "Compressor2, mono compression PT1 - sinewave";
+
+x2 = sin(2*pi*300/fs*(1:1:20*fs)).';
+remove_all_tracks();
+x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
+aud_do("DynamicCompressor: Threshold=-23 Algorithm=1 CompressBy=1 Ratio=2.5 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=12\n");
+y = import_from_aud(1);
+
+# Gain factor 2 because we compress by RMS but do not use lookaround_RMS as
+# lookaround is zero.
+do_test_equ(settled(y, fs, 1), ...
+ comp_gain(settled(2*env_PT1(x, fs, 0.5), fs, 1), -23, 2.5, 12, 0).* ...
+ settled(x, fs, 1));
+
+## Test Compressor, mono compression PT1 - faded sinewave - medium signal
+CURRENT_TEST = "Compressor2, mono compression PT1 - faded sinewave - medium signal";
+
+x2 = sin(2*pi*300/fs*(1:1:50*fs)).' .* horzcat(1:1:25*fs, 25*fs:-1:1).' ./ (25*fs);
+remove_all_tracks();
+x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
+aud_do("DynamicCompressor: Threshold=-10 Algorithm=1 CompressBy=0 Ratio=100 AttackTime=0.01 ReleaseTime=0.01 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 1), ...
+ comp_gain(settled(env_PT1(x, fs, 0.01, 1), fs, 1), -10, 100, 0, 0).* ...
+ settled(x, fs, 1));
+
+## Test Compressor, mono compression PT1 - faded sinewave - 50 sec signal - no lookaround
+CURRENT_TEST = "Compressor2, mono compression PT1 - faded sinewave - long signal";
+
+x2 = vertcat(x2, x2);
+remove_all_tracks();
+x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
+aud_do("DynamicCompressor: Threshold=-10 Algorithm=1 CompressBy=0 Ratio=100 AttackTime=0.01 ReleaseTime=0.01 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 1), ...
+ comp_gain(settled(env_PT1(x, fs, 0.01, 1), fs, 1), -10, 100, 0, 0).* ...
+ settled(x, fs, 1));
+
+## Test Compressor, mono compression PT1 - sinewave - no lookaround - long attack time
+CURRENT_TEST = "Compressor2, mono compression PT1 - sinewave - asymetric attack / release";
+
+x2 = sin(2*pi*300/fs*(1:1:20*fs)).';
+remove_all_tracks();
+x = export_to_aud(x2, fs, "Compressor-mono-sine-test.wav");
+aud_do("DynamicCompressor: Threshold=-6 Algorithm=1 CompressBy=0 Ratio=2.0 AttackTime=1.0 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0 OutputGain=0\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 1), ...
+ comp_gain(settled(env_PT1_asym(x, fs, 1.0, 0.3, 1.0), fs, 1), -6, 2.0, 0, 0).*
+ settled(x, fs, 1));
+
+## Test Compressor, mono lookaround max
+CURRENT_TEST = "Compressor2, mono asymmetric lookaround max";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.1 KneeWidth=5 OutputGain=1\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 0.6), ...
+ comp_gain(settled(env_PT1(lookaround_max(x, fs, 0.2, 0.1), fs, 0.3, 1), fs, 0.6), ...
+ -17, 1.2, 5, 1).*settled(x, fs, 0.6));
+
+## Test Compressor, mono lookaround RMS
+CURRENT_TEST = "Compressor2, mono asymmetric lookaround RMS";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.2 KneeWidth=3 OutputGain=2\n");
+y = import_from_aud(1);
+
+do_test_equ(settled(y, fs, 2), ...
+ comp_gain(settled(env_PT1(lookaround_RMS(x, fs, 0.1, 0.2), fs, 1), fs, 2), -20, 3, 3, 2) ...
+ .*settled(x, fs, 2));
+
+## Test Compressor, mono lookaround max with selection
+CURRENT_TEST = "Compressor2, mono lookaround max with selection";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+
+aud_do("Select: Start=2 End=5 Mode=Set\n");
+aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 OutputGain=0.5\n");
+y = import_from_aud(1);
+x = x(2*fs+1:5*fs);
+
+do_test_equ(settled(y, fs, 0.1), ...
+ comp_gain(settled(env_PT1(lookaround_max(x, fs, 0.2, 0.2), fs, 0.3, 1), fs, 0.1), ...
+ -17, 1.2, 5, 0.5).*settled(x, fs, 0.1));
+
+## Test Compressor, mono, ultra short attack time
+CURRENT_TEST = "Compressor2, mono, ultra short attack time";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.0001 ReleaseTime=0.0001 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
+y = import_from_aud(2);
+
+# XXX: use larger epsilon due to numerical issues
+# (float in audacity vs double in octave vs wav files for exchange)
+do_test_equ(settled(y, fs, 1), ...
+ comp_gain(settled(env_PT1(x, fs, 0.00001, 1), fs), -20, 2, 10, 0) ...
+ .*settled(x, fs, 1), "", 0.15);
+
+## Test Compressor, stereo compression PT1 - no lookaround
+randn("seed", 2);
+x1 = 0.2*randn(35*fs, 2);
+x1(:,1) = x1(:,1) .* sin(2*pi/fs/35*(1:1:35*fs)).';
+x1(:,2) = x1(:,2) .* (sin(2*pi/fs/75*(1:1:35*fs)).' + 0.1);
+
+CURRENT_TEST = "Compressor2, stereo compression PT1";
+remove_all_tracks();
+x = export_to_aud(x1, fs, "Compressor-stereo-test.wav");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y, fs, 1), ...
+ comp_gain(settled(env_PT1(x, fs, 0.5, 1), fs), -20, 2, 10, 0) ...
+ .*settled(x, fs, 1), "stereo dependent");
+
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10 StereoIndependent=1\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y(:,1), fs, 1), ...
+ comp_gain(settled(env_PT1(x(:,1), fs, 0.5, 1), fs, 1), -20, 2, 10, 0) ...
+ .*settled(x(:,1), fs, 1), "channel 1");
+do_test_equ(settled(y(:,2), fs, 1), ...
+ comp_gain(settled(env_PT1(x(:,2), fs, 0.5, 1), fs, 1), -20, 2, 10, 0) ...
+ .*settled(x(:,2), fs, 1), "channel 2");
+
+## Test Compressor, stereo compression PT1 - sinewave
+CURRENT_TEST = "Compressor2, stereo compression PT1 - sinewave";
+x2 = sin(2*pi*300/fs*(1:1:20*fs)).';
+x2 = [x2, sin(2*pi*310/fs*(1:1:20*fs)).'];
+
+remove_all_tracks();
+x = export_to_aud(x2, fs, "Compressor-stereo-sine-test.wav");
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y, fs, 1), ...
+ comp_gain(settled(env_PT1(x, fs, 0.5, 1), fs, 1), -20, 2, 10, 0) ...
+ .*settled(x, fs, 1), "stereo dependent");
+
+remove_all_tracks();
+x = export_to_aud(x2, fs);
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10 StereoIndependent=1\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y(:,1), fs, 1), ...
+ comp_gain(settled(env_PT1(x(:,1), fs, 0.5, 1), fs, 1), -20, 2, 10, 0) ...
+ .*settled(x(:,1), fs, 1), "channel 1");
+do_test_equ(settled(y(:,2), fs, 1), ...
+ comp_gain(settled(env_PT1(x(:,2), fs, 0.5, 1), fs, 1), -20, 2, 10, 0) ...
+ .*settled(x(:,2), fs, 1), "channel 2");
+
+## Test Compressor, stereo lookaround max
+CURRENT_TEST = "Compressor2, stereo lookaround max";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 OutputGain=1\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y, fs, 0.6), ...
+ comp_gain(settled(env_PT1(lookaround_max(x, fs, 0.2, 0.2), fs, 0.3, 1), fs, 0.6), ...
+ -17, 1.2, 5, 1).*settled(x, fs, 0.6));
+
+## Test Compressor, stereo lookaround RMS
+CURRENT_TEST = "Compressor2, stereo lookaround RMS";
+remove_all_tracks();
+x = export_to_aud(x1, fs);
+aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.1 KneeWidth=3 CompressBy=1 OutputGain=1.3\n");
+y = import_from_aud(2);
+
+do_test_equ(settled(y, fs, 2.5), ...
+ comp_gain(settled(env_PT1(lookaround_RMS(x, fs, 0.1, 0.1), fs, 1), fs, 2.5), -20, 3, 3, 1.3) ...
+ .*settled(x, fs, 2.5));
M tests/octave/loudness_test.m => tests/octave/loudness_test.m +24 -71
@@ 125,20 125,12 @@ end
## Test Loudness LUFS mode: block to short and all silent
CURRENT_TEST = "Loudness LUFS mode, short silent block";
fs= 44100;
-x = zeros(ceil(fs*0.35), 2);
-audiowrite(TMP_FILENAME, x, fs);
-if EXPORT_TEST_SIGNALS
- audiowrite(cstrcat(pwd(), "/Loudness-LUFS-silence-test.wav"), x, fs);
-end
+x1 = zeros(ceil(fs*0.35), 2);
remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs, "Loudness-LUFS-silence-test.wav");
aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=1 NormalizeTo=0 StereoIndependent=0\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(2);
do_test_equ(y, x, "identity");
## Test Loudness LUFS mode: stereo dependent
@@ 146,76 138,50 @@ CURRENT_TEST = "Loudness LUFS mode, keep DC and stereo balance";
randn("seed", 1);
# Include some silence in the test signal to test loudness gating
# and vary the overall loudness over time.
-x = [0.1*randn(15*fs, 2).', zeros(5*fs, 2).', 0.1*randn(15*fs, 2).'].';
-x(:,1) = x(:,1) .* sin(2*pi/fs/35*(1:1:35*fs)).' .* 1.2;
-x(:,2) = x(:,2) .* sin(2*pi/fs/35*(1:1:35*fs)).';
-audiowrite(TMP_FILENAME, x, fs);
-if EXPORT_TEST_SIGNALS
- audiowrite(cstrcat(pwd(), "/Loudness-LUFS-stereo-test.wav"), x, fs);
-end
+x1 = [0.1*randn(15*fs, 2).', zeros(5*fs, 2).', 0.1*randn(15*fs, 2).'].';
+x1(:,1) = x1(:,1) .* sin(2*pi/fs/35*(1:1:35*fs)).' .* 1.2;
+x1(:,2) = x1(:,2) .* sin(2*pi/fs/35*(1:1:35*fs)).';
remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs, "Loudness-LUFS-stereo-test.wav");
aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=1 NormalizeTo=0 StereoIndependent=0\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(2);
do_test_equ(calc_LUFS(y, fs), -23, "loudness", LUFS_epsilon);
do_test_neq(calc_LUFS(y(:,1), fs), calc_LUFS(y(:,2), fs), "stereo balance", 1);
## Test Loudness LUFS mode, stereo independent
CURRENT_TEST = "Loudness LUFS mode, stereo independence";
-audiowrite(TMP_FILENAME, x, fs);
remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs);
aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=0 NormalizeTo=0 StereoIndependent=1\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(2);
# Independently processed stereo channels have half the target loudness.
do_test_equ(calc_LUFS(y(:,1), fs), -26, "channel 1 loudness", LUFS_epsilon);
do_test_equ(calc_LUFS(y(:,2), fs), -26, "channel 2 loudness", LUFS_epsilon);
## Test Loudness LUFS mode: mono as mono
CURRENT_TEST = "Test Loudness LUFS mode: mono as mono";
-x = x(:,1);
-audiowrite(TMP_FILENAME, x, fs);
-if EXPORT_TEST_SIGNALS
- audiowrite(cstrcat(pwd(), "/Loudness-LUFS-mono-test.wav"), x, fs);
-end
+x1 = x1(:,1);
remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs, "Loudness-LUFS-mono-test.wav");
aud_do("LoudnessNormalization: LUFSLevel=-26 DualMono=0 NormalizeTo=0 StereoIndependent=1\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=1\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(1);
do_test_equ(calc_LUFS(y, fs), -26, "loudness", LUFS_epsilon);
## Test Loudness LUFS mode: mono as dual-mono
CURRENT_TEST = "Test Loudness LUFS mode: mono as dual-mono";
-audiowrite(TMP_FILENAME, x, fs);
-
remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs);
aud_do("LoudnessNormalization: LUFSLevel=-26 DualMono=1 NormalizeTo=0 StereoIndependent=0\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=1\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(1);
# This shall be 3 LU quieter as it is compared to strict spec.
do_test_equ(calc_LUFS(y, fs), -29, "loudness", LUFS_epsilon);
## Test Loudness LUFS mode: multi-rate project
CURRENT_TEST = "Test Loudness LUFS mode: multi-rate project";
-audiowrite(TMP_FILENAME, x, fs);
+audiowrite(TMP_FILENAME, x1, fs);
+x = audioread(TMP_FILENAME);
remove_all_tracks();
aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
@@ 228,6 194,7 @@ audiowrite(TMP_FILENAME, x1, fs1);
if EXPORT_TEST_SIGNALS
audiowrite(cstrcat(pwd(), "/Loudness-LUFS-stereo-test-8kHz.wav"), x1, fs1);
end
+x1 = audioread(TMP_FILENAME);
aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
select_tracks(0, 100);
@@ 255,36 222,22 @@ do_test_neq(calc_LUFS(y1(:,1), fs), calc_LUFS(y1(:,2), fs), "stereo balance trac
CURRENT_TEST = "Loudness RMS mode, stereo independent";
randn("seed", 1);
fs= 44100;
-x = 0.1*randn(30*fs, 2);
-x(:,1) = x(:,1) * 0.6;
-audiowrite(TMP_FILENAME, x, fs);
-if EXPORT_TEST_SIGNALS
- audiowrite(cstrcat(pwd(), "/Loudness-RMS-test.wav"), x, fs);
-end
+x1 = 0.1*randn(30*fs, 2);
+x1(:,1) = x1(:,1) * 0.6;
remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs, "Loudness-RMS-test.wav");
aud_do("LoudnessNormalization: RMSLevel=-20 DualMono=0 NormalizeTo=1 StereoIndependent=1\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(2);
do_test_equ(20*log10(sqrt(sum(y(:,1).*y(:,1)/length(y)))), -20, "channel 1 RMS");
do_test_equ(20*log10(sqrt(sum(y(:,2).*y(:,2)/length(y)))), -20, "channel 2 RMS");
## Test Loudness RMS mode: stereo dependent
CURRENT_TEST = "Loudness RMS mode, stereo dependent";
-audiowrite(TMP_FILENAME, x, fs);
-
remove_all_tracks();
-aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
-select_tracks(0, 100);
+x = export_to_aud(x1, fs);
aud_do("LoudnessNormalization: RMSLevel=-22 DualMono=1 NormalizeTo=1 StereoIndependent=0\n");
-aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n"));
-system("sync");
-
-y = audioread(TMP_FILENAME);
+y = import_from_aud(2);
# Stereo RMS must be calculated in quadratic domain.
do_test_equ(20*log10(sqrt(sum(rms(y).^2)/size(y)(2))), -22, "RMS");
do_test_neq(20*log10(rms(y(:,1))), 20*log10(rms(y(:,2))), "stereo balance", 1);
M tests/octave/run_test.m => tests/octave/run_test.m +32 -6
@@ 29,10 29,13 @@ if nargin == 2
end
## Initialization and helper functions
+global TMP_FILENAME;
+global EXPORT_TEST_SIGNALS;
UID=num2str(getuid());
PIPE_TO_PATH=strcat("/tmp/audacity_script_pipe.to.", UID);
PIPE_FROM_PATH=strcat("/tmp/audacity_script_pipe.from.", UID);
TMP_FILENAME=strcat(pwd(), "/tmp.wav");
+EXPORT_TEST_SIGNALS = false;
printf("Open scripting pipes, this may freeze if Audacity does not run...\n");
@@ 74,6 77,27 @@ function select_tracks(num, count)
aud_do(sprintf("SelectTracks: Track=%d TrackCount=%d Mode=Set\n", num, count));
end
+function x_in = import_from_aud(channels)
+ global TMP_FILENAME;
+ aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=", ...
+ num2str(channels), "\n"));
+ system("sync");
+ x_in = audioread(TMP_FILENAME);
+end
+
+function x_out = export_to_aud(x, fs, name = "")
+ global TMP_FILENAME;
+ global EXPORT_TEST_SIGNALS;
+ audiowrite(TMP_FILENAME, x, fs);
+ if EXPORT_TEST_SIGNALS && length(name) != 0
+ audiowrite(cstrcat(pwd(), "/", name), x, fs);
+ end
+ # Read it back to avoid quantization-noise in tests
+ x_out = audioread(TMP_FILENAME);
+ aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n"));
+ select_tracks(0, 100);
+end
+
## Float equal comparison helper
function [ret] = float_eq(x, y, eps=0.001)
ret = abs(x - y) < eps;
@@ 99,41 123,43 @@ function plot_failure(x, y)
plot(x, 'r')
hold on
plot(y, 'b')
- plot(log10(abs(x-y)), 'g')
+ delta = abs(x-y);
+ max(delta)
+ plot(log10(delta), 'g')
hold off
legend("Audacity", "Octave", "log-delta", "location", "southeast")
input("Press enter to continue", "s")
end
-function do_test_equ(x, y, msg, eps=0.001, skip = false)
+function do_test_equ(x, y, msg = "", eps = 0.001, skip = false)
cmp = all(all(float_eq(x, y, eps)));
if do_test(cmp, msg, skip) == 0
plot_failure(x, y);
end
end
-function do_test_neq(x, y, msg, eps=0.001, skip = false)
+function do_test_neq(x, y, msg = "", eps = 0.001, skip = false)
cmp = all(all(!float_eq(x, y, eps)));
if do_test(cmp, msg, skip) == 0
plot_failure(x, y);
end
end
-function do_test_gte(x, y, msg, skip = false)
+function do_test_gte(x, y, msg = "", skip = false)
cmp = all(all(x >= y));
if do_test(cmp, msg, skip) == 0
plot_failure(x, y);
end
end
-function do_test_lte(x, y, msg, skip = false)
+function do_test_lte(x, y, msg = "", skip = false)
cmp = all(all(x <= y));
if do_test(cmp, msg, skip) == 0
plot_failure(x, y);
end
end
-function result = do_test(result, msg, skip = false)
+function result = do_test(result, msg = "", skip = false)
global TESTS_RUN;
global TESTS_FAILED;
global TESTS_SKIPPED;