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

Merge pull request #186 from mmmaisel/realtime-compressor2

Merge Max Maisel's branch that adds their Dynamic Compressor
effect to Tenacity. The commit has been already polished extensively,
as it was originally intended to be merged in Audacity.

Signed-off-by: Panagiotis Vasilopoulos <hello@alwayslivid.com>
Reference-to: https://github.com/tenacityteam/tenacity/pull/186
This commit is contained in:
Panagiotis Vasilopoulos 2021-10-12 08:14:20 +00:00 committed by GitHub
commit f06ac9bd96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 3017 additions and 77 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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);

1765
src/effects/Compressor2.cpp Normal file

File diff suppressed because it is too large Load Diff

294
src/effects/Compressor2.h Normal file
View File

@ -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

134
src/widgets/Plot.cpp Normal file
View File

@ -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()

58
src/widgets/Plot.h Normal file
View File

@ -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

View File

@ -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()

View File

@ -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

1
tests/octave/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.wav

View File

@ -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));

View File

@ -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);

View File

@ -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;