From e5a6585a128d27bc6f1de88e37f21a1b6a6742a6 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Tue, 21 Aug 2018 17:32:35 +0200 Subject: [PATCH 01/33] Start work on new Compressor2 effect. Add skeleton files and add them to build systems. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/CMakeLists.txt | 2 + src/effects/Compressor2.cpp | 151 ++++++++++++++++++++++++++++++++++++ src/effects/Compressor2.h | 70 +++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 src/effects/Compressor2.cpp create mode 100644 src/effects/Compressor2.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 83e57578d..cfa06880b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -432,6 +432,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 diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp new file mode 100644 index 000000000..42558b5b3 --- /dev/null +++ b/src/effects/Compressor2.cpp @@ -0,0 +1,151 @@ +/********************************************************************** + + Audacity: A Digital Audio Editor + + Compressor2.cpp + + Max Maisel + +*******************************************************************//** + +\class EffectCompressor2 +\brief An Effect which reduces the dynamic level. + +*//*******************************************************************/ + + +#include "../Audacity.h" // for rint from configwin.h +#include "Compressor2.h" + +#include <math.h> + +#include <wx/intl.h> +#include <wx/valgen.h> + +#include "../Internat.h" +#include "../Prefs.h" +#include "../ProjectFileManager.h" +#include "../Shuttle.h" +#include "../ShuttleGui.h" +#include "../WaveTrack.h" +#include "../widgets/valnum.h" +#include "../widgets/ProgressDialog.h" + +#include "LoadEffects.h" + +BEGIN_EVENT_TABLE(EffectCompressor2, wxEvtHandler) +END_EVENT_TABLE() + +const ComponentInterfaceSymbol EffectCompressor2::Symbol +{ XO("Compressor v2") }; + +namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; } + +EffectCompressor2::EffectCompressor2() +{ + 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"Compressor2"; +} + +// EffectDefinitionInterface implementation + +EffectType EffectCompressor2::GetType() +{ + return EffectTypeProcess; +} + +// EffectClientInterface implementation +bool EffectCompressor2::DefineParams( ShuttleParams & S ) +{ + return true; +} + +bool EffectCompressor2::GetAutomationParameters(CommandParameters & parms) +{ + return true; +} + +bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms) +{ + 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)) + { + SaveUserPreset(GetCurrentSettingsGroup()); + + gPrefs->Flush(); + } + return true; +} + +bool EffectCompressor2::Process() +{ + return false; +} + +void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) +{ +} + +bool EffectCompressor2::TransferDataToWindow() +{ + if (!mUIParent->TransferDataToWindow()) + { + return false; + } + + UpdateUI(); + return true; +} + +bool EffectCompressor2::TransferDataFromWindow() +{ + if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow()) + { + return false; + } + return true; +} + +// EffectCompressor2 implementation + +void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) +{ + UpdateUI(); +} + +void EffectCompressor2::UpdateUI() +{ +} diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h new file mode 100644 index 000000000..fb97b61a0 --- /dev/null +++ b/src/effects/Compressor2.h @@ -0,0 +1,70 @@ +/********************************************************************** + + 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 ShuttleGui; + +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; + + // EffectClientInterface implementation + + bool DefineParams( ShuttleParams & S ) override; + bool GetAutomationParameters(CommandParameters & parms) override; + bool SetAutomationParameters(CommandParameters & parms) 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 + + bool UpdateProgress(); + void OnUpdateUI(wxCommandEvent & evt); + void UpdateUI(); + +private: + + DECLARE_EVENT_TABLE() +}; + +#endif From 6395c8470b6de246c86367b64d454a5d47bc39c7 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sun, 9 Feb 2020 10:34:41 +0100 Subject: [PATCH 02/33] Add all user parameters to Compressor2 effect. Don't do anything with them yet. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 126 ++++++++++++++++++++++++++++++++++++ src/effects/Compressor2.h | 13 ++++ 2 files changed, 139 insertions(+) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 42558b5b3..cdec61325 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -33,6 +33,49 @@ #include "LoadEffects.h" +enum kAlgorithms +{ + kExpFit, + kEnvPT1, + nAlgos +}; + +static const ComponentInterfaceSymbol kAlgorithmStrings[nAlgos] = +{ + { XO("Exponential-Fit") }, + { XO("Analog Model (PT1)") } +}; + +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, 10.0 ); +Param( KneeWidth, double, wxT("KneeWidth"), 10.0, 0.0, 20.0, 10.0 ); +Param( AttackTime, double, wxT("AttackTime"), 0.2, 0.00001, 30.0, 20000.0 ); +Param( ReleaseTime, double, wxT("ReleaseTime"), 1.0, 0.00001, 30.0, 20000.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( MakeupGain, double, wxT("MakeupGain"), 0.0, 0.0, 100.0, 1.0 ); +Param( DryWet, double, wxT("DryWet"), 100.0, 0.0, 100.0, 1.0 ); + BEGIN_EVENT_TABLE(EffectCompressor2, wxEvtHandler) END_EVENT_TABLE() @@ -43,6 +86,20 @@ namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; } EffectCompressor2::EffectCompressor2() { + 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; + mMakeupGainPct = DEF_MakeupGain; + mDryWetPct = DEF_DryWet; + SetLinearEffectFlag(false); } @@ -77,16 +134,71 @@ EffectType EffectCompressor2::GetType() // 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(mMakeupGainPct, MakeupGain); + S.SHUTTLE_PARAM(mDryWetPct, DryWet); + 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_MakeupGain, mMakeupGainPct); + parms.Write(KEY_DryWet, mDryWetPct); + 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(MakeupGain); + ReadAndVerifyDouble(DryWet); + + mAlgorithm = Algorithm; + mCompressBy = CompressBy; + mStereoInd = StereoInd; + + mThresholdDB = Threshold; + mRatio = Ratio; + mKneeWidthDB = KneeWidth; + mAttackTime = AttackTime; + mReleaseTime = ReleaseTime; + mLookaheadTime = LookaheadTime; + mLookbehindTime = LookbehindTime; + mMakeupGainPct = MakeupGain; + mDryWetPct = DryWet; return true; } @@ -103,6 +215,20 @@ bool EffectCompressor2::Startup() // 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; + mMakeupGainPct = DEF_MakeupGain; + mDryWetPct = DEF_DryWet; + SaveUserPreset(GetCurrentSettingsGroup()); gPrefs->Flush(); diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index fb97b61a0..8d98daef9 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -63,6 +63,19 @@ private: void UpdateUI(); private: + int mAlgorithm; + int mCompressBy; + bool mStereoInd; + + double mThresholdDB; + double mRatio; + double mKneeWidthDB; + double mAttackTime; + double mReleaseTime; + double mLookaheadTime; + double mLookbehindTime; + double mMakeupGainPct; + double mDryWetPct; DECLARE_EVENT_TABLE() }; From 2f58dc7d94ccfb790675201a794efab6a1403499 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sun, 9 Feb 2020 14:55:23 +0100 Subject: [PATCH 03/33] Add combined slider and text box widget. This widgets will be used in the new Compresor2 effect but it is designed for use in other effects as well. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/CMakeLists.txt | 2 + src/ShuttleGui.cpp | 27 ++++++ src/ShuttleGui.h | 5 + src/widgets/SliderTextCtrl.cpp | 167 +++++++++++++++++++++++++++++++++ src/widgets/SliderTextCtrl.h | 77 +++++++++++++++ 5 files changed, 278 insertions(+) create mode 100644 src/widgets/SliderTextCtrl.cpp create mode 100644 src/widgets/SliderTextCtrl.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cfa06880b..8ac44c420 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -956,6 +956,8 @@ list( APPEND SOURCES PRIVATE widgets/ReadOnlyText.h widgets/Ruler.cpp widgets/Ruler.h + widgets/SliderTextCtrl.cpp + widgets/SliderTextCtrl.h widgets/UnwritableLocationErrorDialog.cpp widgets/UnwritableLocationErrorDialog.h widgets/Warning.cpp diff --git a/src/ShuttleGui.cpp b/src/ShuttleGui.cpp index 82ab555c5..3328d80b2 100644 --- a/src/ShuttleGui.cpp +++ b/src/ShuttleGui.cpp @@ -121,6 +121,8 @@ for registering for changes. #include "widgets/wxTextCtrlWrapper.h" #include "AllThemeResources.h" +#include "widgets/SliderTextCtrl.h" + #if wxUSE_ACCESSIBILITY #include "widgets/WindowAccessible.h" #endif @@ -613,6 +615,31 @@ wxSlider * ShuttleGuiBase::AddSlider( return pSlider; } +SliderTextCtrl* ShuttleGuiBase::AddSliderTextCtrl( + const TranslatableString &Prompt, double pos, double Max, double Min, + int precision, double* value, double scale) +{ + 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, 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) { diff --git a/src/ShuttleGui.h b/src/ShuttleGui.h index d35d928cf..da01fa8d6 100644 --- a/src/ShuttleGui.h +++ b/src/ShuttleGui.h @@ -28,6 +28,7 @@ class ChoiceSetting; class wxArrayStringEx; +class SliderTextCtrl; const int nMaxNestedSizers = 20; @@ -263,6 +264,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); + // Pass the same initValue to the sequence of calls to AddRadioButton and // AddRadioButtonToGroup. // The radio button is filled if selector == initValue diff --git a/src/widgets/SliderTextCtrl.cpp b/src/widgets/SliderTextCtrl.cpp new file mode 100644 index 000000000..524d1a9f6 --- /dev/null +++ b/src/widgets/SliderTextCtrl.cpp @@ -0,0 +1,167 @@ +/********************************************************************** + + Audacity: A Digital Audio Editor + + SliderTextCtrl.cpp + + Max Maisel + +*******************************************************************//** + +\class SliderTextCtrl +\brief A slider with connected text box. + +*//*******************************************************************/ + + +#include "../Audacity.h" +#include "audacity/Types.h" +#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, + 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(); + + 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); + + if(value <= 0.0) + value = m_zero; + else + value = log10(value); + max = log10(max); + } + + 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_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_value = std::max(m_min, m_value); + m_value = std::min(m_max, m_value); + } + } + m_textbox->ChangeValue(FormatValue()); + m_textbox->SetSelection(-1, -1); + event.SetEventType(cEVT_SLIDERTEXT); + event.Skip(); +} + +void SliderTextCtrl::OnKillFocus(wxFocusEvent& _) +{ + m_textbox->ChangeValue(FormatValue()); + wxCommandEvent event(cEVT_SLIDERTEXT, GetId()); + wxPostEvent(GetParent(), event); +} + +wxString SliderTextCtrl::FormatValue() const +{ + int v = m_value; + if(m_int) + return wxString::Format(m_format, v); + else + return wxString::Format(m_format, m_value); +} + +BEGIN_EVENT_TABLE(SliderTextCtrl, wxControl) + EVT_TEXT(ID_TEXTBOX, SliderTextCtrl::OnTextChange) + EVT_SLIDER(ID_SLIDER, SliderTextCtrl::OnSlider) +END_EVENT_TABLE() diff --git a/src/widgets/SliderTextCtrl.h b/src/widgets/SliderTextCtrl.h new file mode 100644 index 000000000..f8d27efa0 --- /dev/null +++ b/src/widgets/SliderTextCtrl.h @@ -0,0 +1,77 @@ +/********************************************************************** + + 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, 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; + wxString m_format; + + DECLARE_EVENT_TABLE() +}; + +#endif From 2555b68a0444ac137881f7cfa9c125f848ed929d Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sun, 9 Feb 2020 14:55:23 +0100 Subject: [PATCH 04/33] Add Compressor2 GUI without graph. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 163 ++++++++++++++++++++++++++++++++++++ src/effects/Compressor2.h | 2 + 2 files changed, 165 insertions(+) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index cdec61325..3130ca6f2 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -30,6 +30,7 @@ #include "../WaveTrack.h" #include "../widgets/valnum.h" #include "../widgets/ProgressDialog.h" +#include "../widgets/SliderTextCtrl.h" #include "LoadEffects.h" @@ -76,7 +77,15 @@ Param( LookbehindTime, double, wxT("LookbehindTime"), 0.1, 0.0, 10. Param( MakeupGain, double, wxT("MakeupGain"), 0.0, 0.0, 100.0, 1.0 ); Param( DryWet, double, wxT("DryWet"), 100.0, 0.0, 100.0, 1.0 ); +inline int ScaleToPrecision(double scale) +{ + return ceil(log10(scale)); +} + 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 @@ -85,6 +94,7 @@ const ComponentInterfaceSymbol EffectCompressor2::Symbol namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; } EffectCompressor2::EffectCompressor2() + : mIgnoreGuiEvents(false) { mAlgorithm = DEF_Algorithm; mCompressBy = DEF_CompressBy; @@ -199,6 +209,7 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms) mLookbehindTime = LookbehindTime; mMakeupGainPct = MakeupGain; mDryWetPct = DryWet; + return true; } @@ -243,16 +254,166 @@ bool EffectCompressor2::Process() void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) { + S.StartStatic(XO("Algorithm")); + { + S.StartMultiColumn(2, wxALIGN_LEFT); + { + S.SetStretchyCol(1); + + wxChoice* ctrl = nullptr; + + ctrl = S.Validator<wxGenericValidator>(&mAlgorithm) + .AddChoice(XO("Envelope Algorithm:"), + Msgids(kAlgorithmStrings, nAlgos), + mAlgorithm); + + wxSize box_size = ctrl->GetMinSize(); + int width = S.GetParent()->GetTextExtent(wxString::Format( + "%sxxxx", kAlgorithmStrings[nAlgos-1].Translation())).GetWidth(); + box_size.SetWidth(width); + ctrl->SetMinSize(box_size); + + ctrl = S.Validator<wxGenericValidator>(&mCompressBy) + .AddChoice(XO("Compress based on:"), + Msgids(kCompressByStrings, nBy), + mCompressBy); + ctrl->SetMinSize(box_size); + + S.Validator<wxGenericValidator>(&mStereoInd) + .AddCheckBox(XO("Compress stereo channels independently"), + DEF_StereoInd); + } + S.EndMultiColumn(); + } + S.EndStatic(); + + S.StartStatic(XO("Compressor")); + { + S.StartMultiColumn(3, wxEXPAND); + { + S.SetStretchyCol(1); + int textbox_width = S.GetParent()->GetTextExtent("0.000001").GetWidth(); + SliderTextCtrl* ctrl = nullptr; + + 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("Attack Time:"), true, + wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + ctrl = S.Name(XO("Attack Time")) + .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) + .AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime, + MIN_AttackTime, ScaleToPrecision(SCL_AttackTime), + &mAttackTime, SCL_AttackTime / 1000); + ctrl->SetMinTextboxWidth(textbox_width); + S.AddVariableText(XO("s"), true, + wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + + S.AddVariableText(XO("Release Time:"), true, + wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + ctrl = S.Name(XO("Release Time")) + .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) + .AddSliderTextCtrl({}, DEF_ReleaseTime, MAX_ReleaseTime, + MIN_ReleaseTime, ScaleToPrecision(SCL_ReleaseTime), + &mReleaseTime, SCL_ReleaseTime / 1000); + 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); + ctrl = S.Name(XO("Lookahead Time")) + .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) + .AddSliderTextCtrl({}, DEF_LookaheadTime, MAX_LookaheadTime, + MIN_LookaheadTime, ScaleToPrecision(SCL_LookaheadTime), + &mLookaheadTime); + ctrl->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); + ctrl->SetMinTextboxWidth(textbox_width); + S.AddVariableText(XO("s"), true, + wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + + /* i18n-hint: Make-up, i.e. correct for any reduction, rather than fabricate it.*/ + S.AddVariableText(XO("Make-up Gain:"), true, + wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + ctrl = S.Name(XO("Make-up Gain")) + .Style(SliderTextCtrl::HORIZONTAL) + .AddSliderTextCtrl({}, DEF_MakeupGain, MAX_MakeupGain, + MIN_MakeupGain, ScaleToPrecision(SCL_MakeupGain), + &mMakeupGainPct); + ctrl->SetMinTextboxWidth(textbox_width); + S.AddVariableText(XO("%"), true, + wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + + S.AddVariableText(XO("Dry/Wet:"), true, + wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + ctrl = S.Name(XO("Dry/Wet")) + .Style(SliderTextCtrl::HORIZONTAL) + .AddSliderTextCtrl({}, DEF_DryWet, MAX_DryWet, + MIN_DryWet, ScaleToPrecision(SCL_DryWet), + &mDryWetPct); + ctrl->SetMinTextboxWidth(textbox_width); + S.AddVariableText(XO("%"), true, + wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } + S.EndMultiColumn(); + } + 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; } @@ -269,6 +430,8 @@ bool EffectCompressor2::TransferDataFromWindow() void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) { + if(!mIgnoreGuiEvents) + TransferDataFromWindow(); UpdateUI(); } diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index 8d98daef9..1c9f11f89 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -77,6 +77,8 @@ private: double mMakeupGainPct; double mDryWetPct; + bool mIgnoreGuiEvents; + DECLARE_EVENT_TABLE() }; From bcdc47bc3403e0f23ce6481e7c8298bc776fe772 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Fri, 14 Feb 2020 18:41:18 +0100 Subject: [PATCH 05/33] Add generic plot widget. This widget will be used in the new Compressor2 effect but it is designed for use in other effects as well. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/CMakeLists.txt | 2 + src/ShuttleGui.cpp | 28 +++++++++ src/ShuttleGui.h | 6 ++ src/widgets/Plot.cpp | 136 +++++++++++++++++++++++++++++++++++++++++++ src/widgets/Plot.h | 58 ++++++++++++++++++ 5 files changed, 230 insertions(+) create mode 100644 src/widgets/Plot.cpp create mode 100644 src/widgets/Plot.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8ac44c420..cbf28101f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -949,6 +949,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 diff --git a/src/ShuttleGui.cpp b/src/ShuttleGui.cpp index 3328d80b2..ead5aa0f2 100644 --- a/src/ShuttleGui.cpp +++ b/src/ShuttleGui.cpp @@ -121,6 +121,7 @@ for registering for changes. #include "widgets/wxTextCtrlWrapper.h" #include "AllThemeResources.h" +#include "widgets/Plot.h" #include "widgets/SliderTextCtrl.h" #if wxUSE_ACCESSIBILITY @@ -777,6 +778,33 @@ void ShuttleGuiBase::AddConstTextBox( UpdateSizers(); } +Plot* ShuttleGuiBase::AddPlot( const TranslatableString &Prompt, + double x_min, double x_max, double y_min, double y_max, + const TranslatableString& x_label, const TranslatableString& y_label, + int x_format, int y_format, int count) +{ + HandleOptionality( Prompt ); + AddPrompt( Prompt ); + UseUpId(); + if( mShuttleMode != eIsCreating ) + return wxDynamicCast(wxWindow::FindWindowById(miId, mpDlg), Plot); + Plot* pPlot; + mpWind = pPlot = safenew Plot(GetParent(), miId, + x_min, x_max, y_min, y_max, x_label, y_label, + x_format, y_format, count, + wxDefaultPosition, wxDefaultSize, + GetStyle( SliderTextCtrl::HORIZONTAL ) + ); +#if wxUSE_ACCESSIBILITY + // so that name can be set on a standard control + mpWind->SetAccessible(safenew WindowAccessible(mpWind)); +#endif + mpWind->SetName(wxStripMenuCodes(Prompt.Translation())); + miProp=1; + UpdateSizers(); + return pPlot; +} + wxListBox * ShuttleGuiBase::AddListBox(const wxArrayStringEx &choices) { UseUpId(); diff --git a/src/ShuttleGui.h b/src/ShuttleGui.h index da01fa8d6..04a97b28e 100644 --- a/src/ShuttleGui.h +++ b/src/ShuttleGui.h @@ -28,6 +28,7 @@ class ChoiceSetting; class wxArrayStringEx; +class Plot; class SliderTextCtrl; @@ -348,6 +349,11 @@ public: void AddConstTextBox( const TranslatableString &Caption, const TranslatableString & Value ); + Plot* AddPlot( const TranslatableString &Prompt, + double x_min, double x_max, double y_min, double y_max, + const TranslatableString& x_label, const TranslatableString& y_label, + int x_format = 1, int y_format = 1, int count = 1 ); + //-- Start and end functions. These are used for sizer, or other window containers // and create the appropriate widget. void StartHorizontalLay(int PositionFlags=wxALIGN_CENTRE, int iProp=1); diff --git a/src/widgets/Plot.cpp b/src/widgets/Plot.cpp new file mode 100644 index 000000000..fb1c1bea0 --- /dev/null +++ b/src/widgets/Plot.cpp @@ -0,0 +1,136 @@ +/********************************************************************** + + Audacity: A Digital Audio Editor + + Plot.cpp + + Max Maisel + +*******************************************************************//** + +\class Plot +\brief A customizable generic plot widget. + +*//*******************************************************************/ + + +#include "../Audacity.h" +#include "audacity/Types.h" +#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() diff --git a/src/widgets/Plot.h b/src/widgets/Plot.h new file mode 100644 index 000000000..86253f956 --- /dev/null +++ b/src/widgets/Plot.h @@ -0,0 +1,58 @@ +/********************************************************************** + + Audacity: A Digital Audio Editor + + Plot.h + + Max Maisel + + This class is a generic plot. + +**********************************************************************/ + +#ifndef __AUDACITY_PLOT__ +#define __AUDACITY_PLOT__ + +#include "wxPanelWrapper.h" // to inherit + +#include "MemoryX.h" + +class Ruler; + +struct PlotData +{ + std::unique_ptr<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 From ac277c61d7ec16e6c3b0a34a42f2988db81e06fe Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sat, 15 Feb 2020 14:52:55 +0100 Subject: [PATCH 06/33] Add gain and step response plot to Compressor2 effect GUI. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 47 +++++++++++++++++++++++++++++++++++++ src/effects/Compressor2.h | 3 +++ 2 files changed, 50 insertions(+) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 3130ca6f2..345029cb5 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -22,6 +22,7 @@ #include <wx/intl.h> #include <wx/valgen.h> +#include "../AColor.h" #include "../Internat.h" #include "../Prefs.h" #include "../ProjectFileManager.h" @@ -29,7 +30,9 @@ #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" @@ -254,6 +257,40 @@ bool EffectCompressor2::Process() void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) { + S.SetBorder(10); + + S.StartHorizontalLay(wxEXPAND, true); + { + PlotData* plot; + + mGainPlot = S.MinSize( { 200, 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)); + + mResponsePlot = S.MinSize( { 200, 200 } ) + .AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""), + Ruler::IntFormat, Ruler::RealFormat, 2); + + plot = mResponsePlot->GetPlotData(0); + plot->pen = std::unique_ptr<wxPen>( + safenew wxPen(AColor::WideEnvelopePen)); + plot->xdata = {0, 2, 2, 3, 3, 5}; + plot->ydata = {0, 0, 1, 1, 0, 0}; + + 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); + } + S.EndHorizontalLay(); + + S.SetBorder(5); + S.StartStatic(XO("Algorithm")); { S.StartMultiColumn(2, wxALIGN_LEFT); @@ -437,4 +474,14 @@ void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) void EffectCompressor2::UpdateUI() { + PlotData* plot; + plot = mGainPlot->GetPlotData(0); + plot->xdata = {-60, -40, 0}; + plot->ydata = {-60, -40, -20}; + mGainPlot->Refresh(false); + + plot = mResponsePlot->GetPlotData(1); + plot->xdata = {0, 2, 2, 3, 3, 5}; + plot->ydata = {0, 0.5, 1, 1, 0.5, 0}; + mResponsePlot->Refresh(false); } diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index 1c9f11f89..eb8f0906f 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -20,6 +20,7 @@ #include "Effect.h" +class Plot; class ShuttleGui; class EffectCompressor2 final : public Effect @@ -77,6 +78,8 @@ private: double mMakeupGainPct; double mDryWetPct; + Plot* mGainPlot; + Plot* mResponsePlot; bool mIgnoreGuiEvents; DECLARE_EVENT_TABLE() From 7326edbefe661593c86bcce789167eed7c138680 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sat, 15 Feb 2020 18:42:06 +0100 Subject: [PATCH 07/33] Implement compressor gain calculation and preview. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 70 ++++++++++++++++++++++++++++++++++--- src/effects/Compressor2.h | 8 ++++- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 345029cb5..1e61ec65b 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -18,6 +18,7 @@ #include "Compressor2.h" #include <math.h> +#include <numeric> #include <wx/intl.h> #include <wx/valgen.h> @@ -270,6 +271,9 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) 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); mResponsePlot = S.MinSize( { 200, 200 } ) .AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""), @@ -465,6 +469,46 @@ bool EffectCompressor2::TransferDataFromWindow() // EffectCompressor2 implementation +void EffectCompressor2::InitGainCalculation() +{ + mMakeupGainDB = mMakeupGainPct / 100.0 * + -(mThresholdDB * (1.0 - 1.0 / mRatio)); + mMakeupGain = DB_TO_LINEAR(mMakeupGainDB); +} + +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 mMakeupGain; + } + else if(kneeCond >= mKneeWidthDB) + { + // Above threshold: apply compression and make-up gain + return DB_TO_LINEAR(mThresholdDB + + (envDB - mThresholdDB) / mRatio + mMakeupGainDB - 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) + mMakeupGainDB); + } +} + + void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) { if(!mIgnoreGuiEvents) @@ -474,14 +518,30 @@ void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) void EffectCompressor2::UpdateUI() { - PlotData* plot; - plot = mGainPlot->GetPlotData(0); - plot->xdata = {-60, -40, 0}; - plot->ydata = {-60, -40, -20}; - mGainPlot->Refresh(false); + UpdateCompressorPlot(); + // TODO: update plots + PlotData* plot; plot = mResponsePlot->GetPlotData(1); plot->xdata = {0, 2, 2, 3, 3, 5}; plot->ydata = {0, 0.5, 1, 1, 0.5, 0}; mResponsePlot->Refresh(false); } + +void EffectCompressor2::UpdateCompressorPlot() +{ + PlotData* plot; + plot = mGainPlot->GetPlotData(0); + wxASSERT(plot->xdata.size() == plot->ydata.size()); + + InitGainCalculation(); + 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]))); + +// XXX: accessibility but fails with TranslatableString required +// mGainPlot->SetName(wxString::Format( +// "Compressor gain reduction: %.1f dB", plot->ydata[xsize-1])); + mGainPlot->Refresh(false); +} diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index eb8f0906f..fc82aac4d 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -58,12 +58,14 @@ public: private: // EffectCompressor2 implementation + void InitGainCalculation(); + double CompressorGain(double env); bool UpdateProgress(); void OnUpdateUI(wxCommandEvent & evt); void UpdateUI(); + void UpdateCompressorPlot(); -private: int mAlgorithm; int mCompressBy; bool mStereoInd; @@ -78,6 +80,10 @@ private: double mMakeupGainPct; double mDryWetPct; + // cached intermediate values + double mMakeupGain; + double mMakeupGainDB; + Plot* mGainPlot; Plot* mResponsePlot; bool mIgnoreGuiEvents; From 61e88b39ab39a5b8d8d7386a662ede4fa34aa2ee Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Fri, 21 Feb 2020 11:23:38 +0100 Subject: [PATCH 08/33] Implement sliding RMS and sliding max sample preprocessors. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 154 ++++++++++++++++++++++++++++++++++-- src/effects/Compressor2.h | 50 ++++++++++++ 2 files changed, 196 insertions(+), 8 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 1e61ec65b..f7f5d03ed 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -97,6 +97,105 @@ const ComponentInterfaceSymbol EffectCompressor2::Symbol 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); +} + +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(value); +} + +float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR) +{ + return DoProcessSample((fabs(valueL) + fabs(valueR)) / 2.0); +} + +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]); +} + EffectCompressor2::EffectCompressor2() : mIgnoreGuiEvents(false) { @@ -282,7 +381,8 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) plot = mResponsePlot->GetPlotData(0); plot->pen = std::unique_ptr<wxPen>( safenew wxPen(AColor::WideEnvelopePen)); - plot->xdata = {0, 2, 2, 3, 3, 5}; + plot->xdata = {0, RESPONSE_PLOT_STEP_START, RESPONSE_PLOT_STEP_START, + RESPONSE_PLOT_STEP_STOP, RESPONSE_PLOT_STEP_STOP, 5}; plot->ydata = {0, 0, 1, 1, 0, 0}; plot = mResponsePlot->GetPlotData(1); @@ -290,6 +390,10 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) 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.EndHorizontalLay(); @@ -519,13 +623,7 @@ void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) void EffectCompressor2::UpdateUI() { UpdateCompressorPlot(); - - // TODO: update plots - PlotData* plot; - plot = mResponsePlot->GetPlotData(1); - plot->xdata = {0, 2, 2, 3, 3, 5}; - plot->ydata = {0, 0.5, 1, 1, 0.5, 0}; - mResponsePlot->Refresh(false); + UpdateResponsePlot(); } void EffectCompressor2::UpdateCompressorPlot() @@ -545,3 +643,43 @@ void EffectCompressor2::UpdateCompressorPlot() // "Compressor gain reduction: %.1f dB", plot->ydata[xsize-1])); mGainPlot->Refresh(false); } + +void EffectCompressor2::UpdateResponsePlot() +{ + PlotData* plot; + plot = mResponsePlot->GetPlotData(1); + wxASSERT(plot->xdata.size() == plot->ydata.size()); + + std::unique_ptr<SamplePreprocessor> preproc; + float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME; + + size_t window_size = + std::max(1, int(round((mLookaheadTime + mLookbehindTime) * plot_rate))); + size_t lookahead_size = + std::max(0, int(round(mLookaheadTime * plot_rate))); + + if(mCompressBy == kRMS) + preproc = std::unique_ptr<SamplePreprocessor>( + safenew SlidingRmsPreprocessor(window_size, 1.0)); + else + preproc = std::unique_ptr<SamplePreprocessor>( + safenew SlidingMaxPreprocessor(window_size)); + + ssize_t step_start = RESPONSE_PLOT_STEP_START * plot_rate - lookahead_size; + ssize_t step_stop = RESPONSE_PLOT_STEP_STOP * plot_rate - lookahead_size; + + float value; + ssize_t xsize = plot->xdata.size(); + for(ssize_t i = -lookahead_size; i < xsize; ++i) + { + if(i < step_start || i > step_stop) + value = preproc->ProcessSample(0); + else + value = preproc->ProcessSample(1); + + if(i >= 0) + plot->ydata[i] = value; + } + + mResponsePlot->Refresh(false); +} diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index fc82aac4d..2661b8eda 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -23,6 +23,50 @@ class Plot; class ShuttleGui; +class SamplePreprocessor +{ + public: + virtual float ProcessSample(float value) = 0; + virtual float ProcessSample(float valueL, float valueR) = 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); + + 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); + + private: + std::vector<float> mWindow; + std::vector<float> mMaxes; + size_t mPos; + + inline float DoProcessSample(float value); +}; + class EffectCompressor2 final : public Effect { public: @@ -65,6 +109,7 @@ private: void OnUpdateUI(wxCommandEvent & evt); void UpdateUI(); void UpdateCompressorPlot(); + void UpdateResponsePlot(); int mAlgorithm; int mCompressBy; @@ -84,6 +129,11 @@ private: double mMakeupGain; double mMakeupGainDB; + 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; + Plot* mGainPlot; Plot* mResponsePlot; bool mIgnoreGuiEvents; From 94e21d8f1c56029f2864f069ee5480eebb37dd4f Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sat, 22 Feb 2020 10:15:16 +0100 Subject: [PATCH 09/33] Implement ExpFit and Pt1 envelope detectors including preview. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 173 ++++++++++++++++++++++++++++++++++-- src/effects/Compressor2.h | 44 +++++++++ 2 files changed, 209 insertions(+), 8 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index f7f5d03ed..163f1c228 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -196,6 +196,157 @@ float SlidingMaxPreprocessor::DoProcessSample(float value) return std::max(mMaxes[currentHead], mMaxes[nextHead]); } +EnvelopeDetector::EnvelopeDetector(size_t buffer_size) + : mPos(0), + mLookaheadBuffer(buffer_size, 0), + mProcessingBuffer(buffer_size, 0), + mProcessedBuffer(buffer_size, 0) +{ +} + +float EnvelopeDetector::ProcessSample(float value) +{ + float retval = mProcessedBuffer[mPos]; + mLookaheadBuffer[mPos++] = value; + if(mPos == mProcessingBuffer.size()) + { + Follow(); + mPos = 0; + mProcessedBuffer.swap(mProcessingBuffer); + mLookaheadBuffer.swap(mProcessingBuffer); + } + return retval; +} + +size_t EnvelopeDetector::GetBlockSize() const +{ + wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size()); + wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size()); + return mLookaheadBuffer.size(); +} + +ExpFitEnvelopeDetector::ExpFitEnvelopeDetector( + float rate, float attackTime, float releaseTime) + : EnvelopeDetector(TAU_FACTOR * (attackTime + 1.0) * rate) +{ + mAttackFactor = exp(-1.0 / (rate * attackTime)); + mReleaseFactor = exp(-1.0 / (rate * releaseTime)); +} + +void ExpFitEnvelopeDetector::Follow() +{ + /* + "Follow"ing algorithm by Roger B. Dannenberg, taken from + Nyquist. His description follows. -DMM + + Description: this is a sophisticated envelope follower. + The input is an envelope, e.g. something produced with + the AVG function. The purpose of this function is to + generate a smooth envelope that is generally not less + than the input signal. In other words, we want to "ride" + the peaks of the signal with a smooth function. The + algorithm is as follows: keep a current output value + (called the "value"). The value is allowed to increase + by at most rise_factor and decrease by at most fall_factor. + Therefore, the next value should be between + value * rise_factor and value * fall_factor. If the input + is in this range, then the next value is simply the input. + If the input is less than value * fall_factor, then the + next value is just value * fall_factor, which will be greater + than the input signal. If the input is greater than value * + rise_factor, then we compute a rising envelope that meets + the input value by working backwards in time, changing the + previous values to input / rise_factor, input / rise_factor^2, + input / rise_factor^3, etc. until this NEW envelope intersects + the previously computed values. There is only a limited buffer + in which we can work backwards, so if the NEW envelope does not + intersect the old one, then make yet another pass, this time + from the oldest buffered value forward, increasing on each + sample by rise_factor to produce a maximal envelope. This will + still be less than the input. + + The value has a lower limit of floor to make sure value has a + reasonable positive value from which to begin an attack. + */ + wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size()); + wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size()); + + // First apply a peak detect with the requested release rate. + size_t buffer_size = mProcessingBuffer.size(); + double env = mProcessedBuffer[buffer_size-1]; + for(size_t i = 0; i < buffer_size; ++i) + { + env *= mReleaseFactor; + if(mProcessingBuffer[i] > env) + env = mProcessingBuffer[i]; + mProcessingBuffer[i] = env; + } + // Preprocess lookahead buffer as well. + for(size_t i = 0; i < buffer_size; ++i) + { + env *= mReleaseFactor; + if(mLookaheadBuffer[i] > env) + env = mLookaheadBuffer[i]; + mLookaheadBuffer[i] = env; + } + + // Next do the same process in reverse direction to get the + // requested attack rate and preprocess lookahead buffer. + for(ssize_t i = buffer_size - 1; i >= 0; --i) + { + env *= mAttackFactor; + if(mLookaheadBuffer[i] < env) + mLookaheadBuffer[i] = env; + else + env = mLookaheadBuffer[i]; + } + for(ssize_t i = buffer_size - 1; i >= 0; --i) + { + if(mProcessingBuffer[i] < env * mAttackFactor) + { + env *= mAttackFactor; + mProcessingBuffer[i] = env; + } + else if(mProcessingBuffer[i] > env) + // Intersected the previous envelope buffer, so we are finished + return; + else + ; // Do nothing if we are on a plateau from peak look-around + } +} + +Pt1EnvelopeDetector::Pt1EnvelopeDetector( + float rate, float attackTime, float releaseTime, bool correctGain) + : EnvelopeDetector(TAU_FACTOR * (attackTime + 1.0) * rate) +{ + // Approximate peak amplitude correction factor. + if(correctGain) + mGainCorrection = 1.0 + exp(attackTime / 30.0); + else + mGainCorrection = 1.0; + + mAttackFactor = 1.0 / (attackTime * rate); + mReleaseFactor = 1.0 / (releaseTime * rate); +} + +void Pt1EnvelopeDetector::Follow() +{ + wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size()); + wxASSERT(mProcessedBuffer.size() == mLookaheadBuffer.size()); + + // Simulate analog compressor with PT1 characteristic. + size_t buffer_size = mProcessingBuffer.size(); + float level = mProcessedBuffer[buffer_size-1] / mGainCorrection; + for(size_t i = 0; i < buffer_size; ++i) + { + if(mProcessingBuffer[i] >= level) + level += mAttackFactor * (mProcessingBuffer[i] - level); + else + level += mReleaseFactor * (mProcessingBuffer[i] - level); + mProcessingBuffer[i] = level * mGainCorrection; + } +} + EffectCompressor2::EffectCompressor2() : mIgnoreGuiEvents(false) { @@ -612,7 +763,6 @@ double EffectCompressor2::CompressorGain(double env) } } - void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) { if(!mIgnoreGuiEvents) @@ -651,6 +801,7 @@ void EffectCompressor2::UpdateResponsePlot() wxASSERT(plot->xdata.size() == plot->ydata.size()); std::unique_ptr<SamplePreprocessor> preproc; + std::unique_ptr<EnvelopeDetector> envelope; float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME; size_t window_size = @@ -665,21 +816,27 @@ void EffectCompressor2::UpdateResponsePlot() preproc = std::unique_ptr<SamplePreprocessor>( safenew SlidingMaxPreprocessor(window_size)); + if(mAlgorithm == kExpFit) + envelope = std::unique_ptr<EnvelopeDetector>( + safenew ExpFitEnvelopeDetector(plot_rate, mAttackTime, mReleaseTime)); + else + envelope = std::unique_ptr<EnvelopeDetector>( + safenew Pt1EnvelopeDetector(plot_rate, mAttackTime, mReleaseTime, false)); + ssize_t step_start = RESPONSE_PLOT_STEP_START * plot_rate - lookahead_size; ssize_t step_stop = RESPONSE_PLOT_STEP_STOP * plot_rate - lookahead_size; - float value; ssize_t xsize = plot->xdata.size(); - for(ssize_t i = -lookahead_size; i < xsize; ++i) + ssize_t block_size = envelope->GetBlockSize(); + for(ssize_t i = -lookahead_size; i < 2*block_size; ++i) { if(i < step_start || i > step_stop) - value = preproc->ProcessSample(0); + envelope->ProcessSample(preproc->ProcessSample(0)); else - value = preproc->ProcessSample(1); - - if(i >= 0) - plot->ydata[i] = value; + envelope->ProcessSample(preproc->ProcessSample(1)); } + for(ssize_t i = 0; i < xsize; ++i) + plot->ydata[i] = envelope->ProcessSample(preproc->ProcessSample(0)); mResponsePlot->Refresh(false); } diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index 2661b8eda..36ac69c96 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -67,6 +67,50 @@ class SlidingMaxPreprocessor : public SamplePreprocessor inline float DoProcessSample(float value); }; +class EnvelopeDetector +{ + public: + EnvelopeDetector(size_t buffer_size); + + float ProcessSample(float value); + size_t GetBlockSize() const; + protected: + static const int TAU_FACTOR = 5; + + size_t mPos; + std::vector<float> mLookaheadBuffer; + std::vector<float> mProcessingBuffer; + std::vector<float> mProcessedBuffer; + + virtual void Follow() = 0; +}; + +class ExpFitEnvelopeDetector : public EnvelopeDetector +{ + public: + ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime); + + private: + double mAttackFactor; + double mReleaseFactor; + + virtual void Follow(); +}; + +class Pt1EnvelopeDetector : public EnvelopeDetector +{ + public: + Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime, + bool correctGain = true); + + private: + double mGainCorrection; + double mAttackFactor; + double mReleaseFactor; + + virtual void Follow(); +}; + class EffectCompressor2 final : public Effect { public: From 6941c66de8220e729fcc6fd0af78cf97c035027a Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Wed, 27 May 2020 16:39:19 +0200 Subject: [PATCH 10/33] Implement compressor2 offline processing. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 397 ++++++++++++++++++++++++++++++++++-- src/effects/Compressor2.h | 63 +++++- 2 files changed, 434 insertions(+), 26 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 163f1c228..8bf9d3476 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -166,7 +166,7 @@ SlidingMaxPreprocessor::SlidingMaxPreprocessor(size_t windowSize) float SlidingMaxPreprocessor::ProcessSample(float value) { - return DoProcessSample(value); + return DoProcessSample(fabs(value)); } float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR) @@ -226,8 +226,8 @@ size_t EnvelopeDetector::GetBlockSize() const } ExpFitEnvelopeDetector::ExpFitEnvelopeDetector( - float rate, float attackTime, float releaseTime) - : EnvelopeDetector(TAU_FACTOR * (attackTime + 1.0) * rate) + float rate, float attackTime, float releaseTime, size_t bufferSize) + : EnvelopeDetector(bufferSize) { mAttackFactor = exp(-1.0 / (rate * attackTime)); mReleaseFactor = exp(-1.0 / (rate * releaseTime)); @@ -316,8 +316,9 @@ void ExpFitEnvelopeDetector::Follow() } Pt1EnvelopeDetector::Pt1EnvelopeDetector( - float rate, float attackTime, float releaseTime, bool correctGain) - : EnvelopeDetector(TAU_FACTOR * (attackTime + 1.0) * rate) + float rate, float attackTime, float releaseTime, size_t bufferSize, + bool correctGain) + : EnvelopeDetector(bufferSize) { // Approximate peak amplitude correction factor. if(correctGain) @@ -347,6 +348,49 @@ void Pt1EnvelopeDetector::Follow() } } +void PipelineBuffer::pad_to(size_t len, float value, bool stereo) +{ + if(size < len) + { + size = len; + std::fill(mBlockBuffer[0].get() + trackSize, + mBlockBuffer[0].get() + size, value); + if(stereo) + std::fill(mBlockBuffer[1].get() + trackSize, + mBlockBuffer[1].get() + size, value); + } +} + +void PipelineBuffer::swap(PipelineBuffer& other) +{ + std::swap(trackPos, other.trackPos); + std::swap(trackSize, other.trackSize); + std::swap(size, other.size); + std::swap(mBlockBuffer[0], other.mBlockBuffer[0]); + std::swap(mBlockBuffer[1], other.mBlockBuffer[1]); +} + +void PipelineBuffer::init(size_t capacity, bool stereo) +{ + trackPos = 0; + trackSize = 0; + size = 0; + mCapacity = capacity; + mBlockBuffer[0].reinit(capacity); + std::fill(mBlockBuffer[0].get(), mBlockBuffer[0].get() + capacity, 0); + if(stereo) + { + mBlockBuffer[1].reinit(capacity); + std::fill(mBlockBuffer[1].get(), mBlockBuffer[1].get() + capacity, 0); + } +} + +void PipelineBuffer::free() +{ + mBlockBuffer[0].reset(); + mBlockBuffer[1].reset(); +} + EffectCompressor2::EffectCompressor2() : mIgnoreGuiEvents(false) { @@ -503,7 +547,52 @@ bool EffectCompressor2::Startup() bool EffectCompressor2::Process() { - return false; + // Iterate over each track + this->CopyInputTracks(); // Set up mOutputTracks. + bool bGoodResult = true; + + AllocPipeline(); + mProgressVal = 0; + + for(auto track : mOutputTracks->Selected<WaveTrack>() + + (mStereoInd ? &Track::Any : &Track::IsLeader)) + { + // Get start and end times from track + // PRL: No accounting for multiple channels ? + double trackStart = track->GetStartTime(); + double trackEnd = track->GetEndTime(); + + // Set the current bounds to whichever left marker is + // greater and whichever right marker is less: + mCurT0 = mT0 < trackStart? trackStart: mT0; + mCurT1 = mT1 > trackEnd? trackEnd: mT1; + + // Get the track rate + mSampleRate = track->GetRate(); + + auto range = mStereoInd + ? TrackList::SingletonRange(track) + : TrackList::Channels(track); + + mProcStereo = range.size() > 1; + + InitGainCalculation(); + mPreproc = InitPreprocessor(mSampleRate); + mEnvelope = InitEnvelope(mSampleRate, mPipeline[0].capacity()); + + if(!ProcessOne(range)) + { + // Processing failed -> abort + bGoodResult = false; + break; + } + } + + this->ReplaceProcessedTracks(bGoodResult); + mPreproc.reset(nullptr); + mEnvelope.reset(nullptr); + FreePipeline(); + return bGoodResult; } void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) @@ -726,6 +815,7 @@ bool EffectCompressor2::TransferDataFromWindow() void EffectCompressor2::InitGainCalculation() { + mDryWet = mDryWetPct / 100.0; mMakeupGainDB = mMakeupGainPct / 100.0 * -(mThresholdDB * (1.0 - 1.0 / mRatio)); mMakeupGain = DB_TO_LINEAR(mMakeupGainDB); @@ -763,6 +853,282 @@ double EffectCompressor2::CompressorGain(double env) } } +std::unique_ptr<SamplePreprocessor> EffectCompressor2::InitPreprocessor( + double rate, bool preview) +{ + size_t window_size = + std::max(1, int(round((mLookaheadTime + mLookbehindTime) * rate))); + + if(mCompressBy == kAmplitude) + return std::unique_ptr<SamplePreprocessor>(safenew + SlidingMaxPreprocessor(window_size)); + else + return std::unique_ptr<SamplePreprocessor>(safenew + SlidingRmsPreprocessor(window_size, preview ? 1.0 : 2.0)); +} + +std::unique_ptr<EnvelopeDetector> EffectCompressor2::InitEnvelope( + double rate, size_t blockSize, bool preview) +{ + if(mAlgorithm == kExpFit) + return std::unique_ptr<EnvelopeDetector>(safenew + ExpFitEnvelopeDetector(rate, mAttackTime, mReleaseTime, blockSize)); + else + return std::unique_ptr<EnvelopeDetector>(safenew + Pt1EnvelopeDetector(rate, mAttackTime, mReleaseTime, blockSize, + !preview && mCompressBy != kAmplitude)); +} + +size_t EffectCompressor2::CalcBufferSize(size_t sampleRate) +{ + size_t capacity; + + mLookaheadLength = + std::max(0, int(round(mLookaheadTime * sampleRate))); + capacity = mLookaheadLength + + size_t(float(TAU_FACTOR) * (1.0 + mAttackTime) * sampleRate); + if(capacity < MIN_BUFFER_CAPACITY) + capacity = MIN_BUFFER_CAPACITY; + return capacity; +} + +/// Get required buffer size for the largest whole track and allocate buffers. +/// This reduces the amount of allocations required. +void EffectCompressor2::AllocPipeline() +{ + bool stereoTrackFound = false; + double maxSampleRate = 0; + size_t capacity; + + mProcStereo = false; + + for(auto track : mOutputTracks->Selected<WaveTrack>() + &Track::Any) + { + maxSampleRate = std::max(maxSampleRate, track->GetRate()); + + // There is a stereo track + if(track->IsLeader()) + stereoTrackFound = true; + } + + // Initiate a processing quad-buffer. This buffer will (most likely) + // be shorter than the length of the track being processed. + stereoTrackFound = stereoTrackFound && !mStereoInd; + capacity = CalcBufferSize(maxSampleRate); + for(size_t i = 0; i < PIPELINE_DEPTH; ++i) + mPipeline[i].init(capacity, stereoTrackFound); +} + +void EffectCompressor2::FreePipeline() +{ + for(size_t i = 0; i < PIPELINE_DEPTH; ++i) + mPipeline[i].free(); +} + +void EffectCompressor2::SwapPipeline() +{ + ++mProgressVal; + for(size_t i = 0; i < PIPELINE_DEPTH-1; ++i) + mPipeline[i].swap(mPipeline[i+1]); + std::cerr << "\n"; +} + +/// ProcessOne() takes a track, transforms it to bunch of buffer-blocks, +/// and executes ProcessData, on it... +bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range) +{ + WaveTrack* track = *range.begin(); + + // Transform the marker timepoints to samples + const auto start = track->TimeToLongSamples(mCurT0); + const auto end = track->TimeToLongSamples(mCurT1); + + // Get the length of the buffer (as double). len is + // used simply to calculate a progress meter, so it is easier + // to make it a double now than it is to do it later + mTrackLen = (end - start).as_double(); + + // Abort if the right marker is not to the right of the left marker + if(mCurT1 <= mCurT0) + return false; + + // Go through the track one buffer at a time. s counts which + // sample the current buffer starts at. + auto pos = start; + + mProgressVal = 0; + while(pos < end) + { + StorePipeline(range); + SwapPipeline(); + + const size_t remainingLen = (end - pos).as_size_t(); + + // Get a block of samples (smaller than the size of the buffer) + // Adjust the block size if it is the final block in the track + const auto blockLen = limitSampleBufferSize( + remainingLen, mPipeline[PIPELINE_DEPTH-1].capacity()); + + mPipeline[PIPELINE_DEPTH-1].trackPos = pos; + if(!LoadPipeline(range, blockLen)) + return false; + + if(mPipeline[0].size == 0) + FillPipeline(); + else + ProcessPipeline(); + + // Increment s one blockfull of samples + pos += blockLen; + } + + // Handle short selections + while(mPipeline[1].size == 0) + { + SwapPipeline(); + FillPipeline(); + } + + while(PipelineHasData()) + { + StorePipeline(range); + SwapPipeline(); + DrainPipeline(); + } + StorePipeline(range); + + // Return true because the effect processing succeeded ... unless cancelled + return true; +} + +bool EffectCompressor2::LoadPipeline( + TrackIterRange<WaveTrack> range, size_t len) +{ + sampleCount read_size = -1; + sampleCount last_read_size = -1; + // Get the samples from the track and put them in the buffer + int idx = 0; + for(auto channel : range) + { + channel->Get((samplePtr) mPipeline[PIPELINE_DEPTH-1][idx], + floatSample, mPipeline[PIPELINE_DEPTH-1].trackPos, len, + fillZero, true, &read_size); + // WaveTrack::Get returns the amount of read samples excluding zero + // filled samples from clip gaps. But in case of stereo tracks with + // assymetric gaps it still returns the same number for both channels. + // + // Fail if we read different sample count from stereo pair tracks. + // Ignore this check during first iteration (last_read_size == -1). + if(read_size != last_read_size && last_read_size.as_long_long() != -1) + return false; + mPipeline[PIPELINE_DEPTH-1].trackSize = read_size.as_size_t(); + mPipeline[PIPELINE_DEPTH-1].size = read_size.as_size_t(); + ++idx; + } + + wxASSERT(mPipeline[PIPELINE_DEPTH-2].trackSize == 0 || + mPipeline[PIPELINE_DEPTH-2].trackSize >= + mPipeline[PIPELINE_DEPTH-1].trackSize); + return true; +} + +void EffectCompressor2::FillPipeline() +{ + // TODO: correct end conditions + mPipeline[PIPELINE_DEPTH-1].pad_to(mEnvelope->GetBlockSize(), 0, mProcStereo); + + size_t length = mPipeline[PIPELINE_DEPTH-1].size; + for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp) + { + // TODO: correct initial conditions + if(rp < length) + EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp); + else + EnvelopeSample(mPipeline[PIPELINE_DEPTH-1], rp % length); + } +} + +void EffectCompressor2::ProcessPipeline() +{ + float env; + size_t length = mPipeline[0].size; + + for(size_t i = 0; i < PIPELINE_DEPTH-2; ++i) + { wxASSERT(mPipeline[0].size == mPipeline[i+1].size); } + + for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp) + { + if(rp < length) + env = EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp); + else if((rp % length) < mPipeline[PIPELINE_DEPTH-1].size) + env = EnvelopeSample(mPipeline[PIPELINE_DEPTH-1], rp % length); + else + // TODO: correct end condition + env = mEnvelope->ProcessSample(mPreproc->ProcessSample(0.0)); + CompressSample(env, wp); + } +} + +inline float EffectCompressor2::EnvelopeSample(PipelineBuffer& pbuf, size_t rp) +{ + float preprocessed; + if(mProcStereo) + preprocessed = mPreproc->ProcessSample(pbuf[0][rp], pbuf[1][rp]); + else + preprocessed = mPreproc->ProcessSample(pbuf[0][rp]); + return mEnvelope->ProcessSample(preprocessed); +} + +inline void EffectCompressor2::CompressSample(float env, size_t wp) +{ + float gain = (1.0 - mDryWet) + CompressorGain(env) * mDryWet; + mPipeline[0][0][wp] = mPipeline[0][0][wp] * gain; + if(mProcStereo) + mPipeline[0][1][wp] = mPipeline[0][1][wp] * gain; +} + +bool EffectCompressor2::PipelineHasData() +{ + for(size_t i = 0; i < PIPELINE_DEPTH; ++i) + { + if(mPipeline[i].size != 0) + return true; + } + return false; +} + +void EffectCompressor2::DrainPipeline() +{ + float env; + size_t length = mPipeline[0].size; + size_t length2 = mPipeline[PIPELINE_DEPTH-2].size; + for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp) + { + if(rp < length2 && mPipeline[PIPELINE_DEPTH-2].size != 0) + { + env = EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp); + } + else + // TODO: correct end condition + env = mEnvelope->ProcessSample(mPreproc->ProcessSample(0.0)); + CompressSample(env, wp); + } +} + +void EffectCompressor2::StorePipeline(TrackIterRange<WaveTrack> range) +{ + int idx = 0; + for(auto channel : range) + { + // Copy the newly-changed samples back onto the track. + channel->Set((samplePtr) mPipeline[0][idx], + floatSample, mPipeline[0].trackPos, mPipeline[0].trackSize); + ++idx; + } + mPipeline[0].trackSize = 0; + mPipeline[0].size = 0; +} + void EffectCompressor2::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) { if(!mIgnoreGuiEvents) @@ -804,30 +1170,17 @@ void EffectCompressor2::UpdateResponsePlot() std::unique_ptr<EnvelopeDetector> envelope; float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME; - size_t window_size = - std::max(1, int(round((mLookaheadTime + mLookbehindTime) * plot_rate))); size_t lookahead_size = std::max(0, int(round(mLookaheadTime * plot_rate))); + ssize_t block_size = float(TAU_FACTOR) * (mAttackTime + 1.0) * plot_rate; - if(mCompressBy == kRMS) - preproc = std::unique_ptr<SamplePreprocessor>( - safenew SlidingRmsPreprocessor(window_size, 1.0)); - else - preproc = std::unique_ptr<SamplePreprocessor>( - safenew SlidingMaxPreprocessor(window_size)); - - if(mAlgorithm == kExpFit) - envelope = std::unique_ptr<EnvelopeDetector>( - safenew ExpFitEnvelopeDetector(plot_rate, mAttackTime, mReleaseTime)); - else - envelope = std::unique_ptr<EnvelopeDetector>( - safenew Pt1EnvelopeDetector(plot_rate, mAttackTime, mReleaseTime, false)); + preproc = InitPreprocessor(plot_rate, true); + envelope = InitEnvelope(plot_rate, block_size, true); ssize_t step_start = RESPONSE_PLOT_STEP_START * plot_rate - lookahead_size; ssize_t step_stop = RESPONSE_PLOT_STEP_STOP * plot_rate - lookahead_size; ssize_t xsize = plot->xdata.size(); - ssize_t block_size = envelope->GetBlockSize(); for(ssize_t i = -lookahead_size; i < 2*block_size; ++i) { if(i < step_start || i > step_stop) diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index 36ac69c96..57c69b766 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -75,8 +75,6 @@ class EnvelopeDetector float ProcessSample(float value); size_t GetBlockSize() const; protected: - static const int TAU_FACTOR = 5; - size_t mPos; std::vector<float> mLookaheadBuffer; std::vector<float> mProcessingBuffer; @@ -88,7 +86,8 @@ class EnvelopeDetector class ExpFitEnvelopeDetector : public EnvelopeDetector { public: - ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime); + ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime, + size_t buffer_size = 0); private: double mAttackFactor; @@ -101,7 +100,7 @@ class Pt1EnvelopeDetector : public EnvelopeDetector { public: Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime, - bool correctGain = true); + size_t buffer_size = 0, bool correctGain = true); private: double mGainCorrection; @@ -111,6 +110,27 @@ class Pt1EnvelopeDetector : public EnvelopeDetector virtual void Follow(); }; +struct PipelineBuffer +{ + public: + sampleCount trackPos; + size_t trackSize; + size_t size; + + inline float* operator[](size_t idx) + { return mBlockBuffer[idx].get(); } + + void pad_to(size_t len, float value, bool stereo); + void swap(PipelineBuffer& other); + void init(size_t size, bool stereo); + inline size_t capacity() const { return mCapacity; } + void free(); + + private: + size_t mCapacity; + Floats mBlockBuffer[2]; +}; + class EffectCompressor2 final : public Effect { public: @@ -148,6 +168,24 @@ private: // EffectCompressor2 implementation void InitGainCalculation(); double CompressorGain(double env); + std::unique_ptr<SamplePreprocessor> InitPreprocessor( + double rate, bool preview = false); + std::unique_ptr<EnvelopeDetector> InitEnvelope( + double rate, size_t blockSize = 0, bool preview = false); + size_t CalcBufferSize(size_t sampleRate); + + void AllocPipeline(); + void FreePipeline(); + void SwapPipeline(); + bool ProcessOne(TrackIterRange<WaveTrack> range); + bool LoadPipeline(TrackIterRange<WaveTrack> range, size_t len); + void FillPipeline(); + void ProcessPipeline(); + inline float EnvelopeSample(PipelineBuffer& pbuf, size_t rp); + inline void CompressSample(float env, size_t wp); + bool PipelineHasData(); + void DrainPipeline(); + void StorePipeline(TrackIterRange<WaveTrack> range); bool UpdateProgress(); void OnUpdateUI(wxCommandEvent & evt); @@ -155,6 +193,21 @@ private: void UpdateCompressorPlot(); void UpdateResponsePlot(); + static const int TAU_FACTOR = 5; + static const size_t MIN_BUFFER_CAPACITY = 1048576; // 1MB + + static const size_t PIPELINE_DEPTH = 4; + PipelineBuffer mPipeline[PIPELINE_DEPTH]; + + double mCurT0; + double mCurT1; + double mProgressVal; + double mTrackLen; + bool mProcStereo; + + std::unique_ptr<SamplePreprocessor> mPreproc; + std::unique_ptr<EnvelopeDetector> mEnvelope; + int mAlgorithm; int mCompressBy; bool mStereoInd; @@ -170,8 +223,10 @@ private: double mDryWetPct; // cached intermediate values + double mDryWet; double mMakeupGain; double mMakeupGainDB; + size_t mLookaheadLength; static const size_t RESPONSE_PLOT_SAMPLES = 200; static const size_t RESPONSE_PLOT_TIME = 5; From 1d7b143c915e55227b5ce85c7ea29360c3505e66 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Wed, 27 May 2020 16:39:54 +0200 Subject: [PATCH 11/33] Debugging helper code for Compressor2 effect. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- scripts/debug/compressor2_buffers.m | 25 +++++ src/effects/Compressor2.cpp | 160 ++++++++++++++++++++++++++++ src/effects/Compressor2.h | 1 + 3 files changed, 186 insertions(+) create mode 100644 scripts/debug/compressor2_buffers.m diff --git a/scripts/debug/compressor2_buffers.m b/scripts/debug/compressor2_buffers.m new file mode 100644 index 000000000..f20c85b12 --- /dev/null +++ b/scripts/debug/compressor2_buffers.m @@ -0,0 +1,25 @@ +%% Debug Compressor v2 pipeline buffers +buffer_ids = [1,2,3,4,5]; +prefix = '/tmp'; + +figure(1); +for k = 1:length(buffer_ids) + subplot(length(buffer_ids), 1, k) + bfile = fopen(sprintf('%s/envbuf.%d.bin', prefix, buffer_ids(k))); + env = fread(bfile, 'float').'; + bfile = fopen(sprintf('%s/blockbuf.%d.bin', prefix, buffer_ids(k))); + block_raw = fread(bfile, 'float').'; + + sizes = reshape(block_raw(1:12), 3, 4); + capacity = (1:4).*sizes(3,:); + track_size = horzcat(0, capacity(1:3)) + sizes(1,:); + block = block_raw(13:end); + + plot(block, 'b', 'linewidth', 3); + hold on; + plot(circshift(env, length(env)/3), 'r'); + stem(capacity, ones(1, length(capacity)), 'g'); + stem(track_size, 1.5.*ones(1, length(capacity)), 'b'); + ylim([-2 2]); + hold off; +end \ No newline at end of file diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 8bf9d3476..5c70ede44 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -38,6 +38,15 @@ #include "LoadEffects.h" +//#define DEBUG_COMPRESSOR2_DUMP_BUFFERS +//#define DEBUG_COMPRESSOR2_ENV +//#define DEBUG_COMPRESSOR2_TRACE + +#ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS +#include <fstream> +int buf_num; +#endif + enum kAlgorithms { kExpFit, @@ -225,6 +234,19 @@ size_t EnvelopeDetector::GetBlockSize() const 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) @@ -927,10 +949,58 @@ void EffectCompressor2::FreePipeline() 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 + ++mProgressVal; 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, @@ -956,9 +1026,22 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range) // 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 + 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(); @@ -985,6 +1068,15 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range) // 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(); } @@ -995,6 +1087,9 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range) SwapPipeline(); DrainPipeline(); } +#ifdef DEBUG_COMPRESSOR2_TRACE + std::cerr << "StoreLastBlock\n" << std::flush; +#endif StorePipeline(range); // Return true because the effect processing succeeded ... unless cancelled @@ -1006,6 +1101,11 @@ bool EffectCompressor2::LoadPipeline( { 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) @@ -1034,6 +1134,16 @@ bool EffectCompressor2::LoadPipeline( 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); @@ -1050,12 +1160,26 @@ void EffectCompressor2::FillPipeline() 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) @@ -1082,7 +1206,14 @@ inline float EffectCompressor2::EnvelopeSample(PipelineBuffer& pbuf, size_t rp) inline void EffectCompressor2::CompressSample(float env, size_t wp) { float gain = (1.0 - mDryWet) + CompressorGain(env) * mDryWet; +#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; } @@ -1099,13 +1230,37 @@ bool EffectCompressor2::PipelineHasData() 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 @@ -1117,6 +1272,11 @@ void EffectCompressor2::DrainPipeline() 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) { diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index 57c69b766..f1b26e13d 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -74,6 +74,7 @@ class EnvelopeDetector float ProcessSample(float value); size_t GetBlockSize() const; + const float* GetBuffer(int idx) const; protected: size_t mPos; std::vector<float> mLookaheadBuffer; From 277f64c4ca041fa01dec04412ed80c34f1fcf495 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sun, 8 Mar 2020 10:00:59 +0100 Subject: [PATCH 12/33] Imprement progress dialog. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 5c70ede44..c389f0080 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -995,7 +995,6 @@ void EffectCompressor2::SwapPipeline() std::cerr << "\n" << std::flush; #endif - ++mProgressVal; for(size_t i = 0; i < PIPELINE_DEPTH-1; ++i) mPipeline[i].swap(mPipeline[i+1]); #ifdef DEBUG_COMPRESSOR2_TRACE @@ -1063,6 +1062,9 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range) // Increment s one blockfull of samples pos += blockLen; + + if(!UpdateProgress()) + return false; } // Handle short selections @@ -1079,6 +1081,8 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range) #endif SwapPipeline(); FillPipeline(); + if(!UpdateProgress()) + return false; } while(PipelineHasData()) @@ -1086,6 +1090,8 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range) StorePipeline(range); SwapPipeline(); DrainPipeline(); + if(!UpdateProgress()) + return false; } #ifdef DEBUG_COMPRESSOR2_TRACE std::cerr << "StoreLastBlock\n" << std::flush; @@ -1289,6 +1295,14 @@ void EffectCompressor2::StorePipeline(TrackIterRange<WaveTrack> range) 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) From ae4189a5332b6da93c45a75edc713e7a9e5446f9 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Thu, 28 May 2020 09:33:04 +0200 Subject: [PATCH 13/33] Implement initial condition optimization. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 87 ++++++++++++++++++++++++++++++++----- src/effects/Compressor2.h | 12 +++++ 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index c389f0080..f4916d364 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -125,6 +125,14 @@ float SlidingRmsPreprocessor::ProcessSample(float valueL, float valueR) return DoProcessSample((valueL * valueL + valueR * valueR) / 2.0); } +void SlidingRmsPreprocessor::Reset() +{ + mSum = 0; + mPos = 0; + mInsertCount = 0; + std::fill(mWindow.begin(), mWindow.end(), 0); +} + float SlidingRmsPreprocessor::DoProcessSample(float value) { if(mInsertCount > REFRESH_WINDOW_EVERY) @@ -183,6 +191,13 @@ float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR) return DoProcessSample((fabs(valueL) + fabs(valueR)) / 2.0); } +void SlidingMaxPreprocessor::Reset() +{ + mPos = 0; + std::fill(mWindow.begin(), mWindow.end(), 0); + std::fill(mMaxes.begin(), mMaxes.end(), 0); +} + float SlidingMaxPreprocessor::DoProcessSample(float value) { size_t oldHead = (mPos-1) % mWindow.size(); @@ -207,6 +222,8 @@ float SlidingMaxPreprocessor::DoProcessSample(float value) EnvelopeDetector::EnvelopeDetector(size_t buffer_size) : mPos(0), + mInitialCondition(0), + mInitialBlockSize(0), mLookaheadBuffer(buffer_size, 0), mProcessingBuffer(buffer_size, 0), mProcessedBuffer(buffer_size, 0) @@ -227,6 +244,10 @@ float EnvelopeDetector::ProcessSample(float value) return retval; } +void EnvelopeDetector::CalcInitialCondition(float value) +{ +} + size_t EnvelopeDetector::GetBlockSize() const { wxASSERT(mProcessedBuffer.size() == mProcessingBuffer.size()); @@ -350,6 +371,28 @@ Pt1EnvelopeDetector::Pt1EnvelopeDetector( mAttackFactor = 1.0 / (attackTime * rate); mReleaseFactor = 1.0 / (releaseTime * rate); + mInitialBlockSize = std::min(size_t(rate * sqrt(attackTime)), bufferSize); +} + +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() @@ -399,12 +442,16 @@ void PipelineBuffer::init(size_t capacity, bool stereo) size = 0; mCapacity = capacity; mBlockBuffer[0].reinit(capacity); - std::fill(mBlockBuffer[0].get(), mBlockBuffer[0].get() + capacity, 0); if(stereo) - { mBlockBuffer[1].reinit(capacity); - std::fill(mBlockBuffer[1].get(), mBlockBuffer[1].get() + capacity, 0); - } + 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() @@ -1032,6 +1079,7 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range) std::cerr << "LookaheadLen: " << mLookaheadLength << "\n" << std::flush; #endif + bool first = true; mProgressVal = 0; #ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS buf_num = 0; @@ -1055,6 +1103,21 @@ bool EffectCompressor2::ProcessOne(TrackIterRange<WaveTrack> range) 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 @@ -1156,7 +1219,6 @@ void EffectCompressor2::FillPipeline() size_t length = mPipeline[PIPELINE_DEPTH-1].size; for(size_t rp = mLookaheadLength, wp = 0; wp < length; ++rp, ++wp) { - // TODO: correct initial conditions if(rp < length) EnvelopeSample(mPipeline[PIPELINE_DEPTH-2], rp); else @@ -1199,14 +1261,17 @@ void EffectCompressor2::ProcessPipeline() } } +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) { - float preprocessed; - if(mProcStereo) - preprocessed = mPreproc->ProcessSample(pbuf[0][rp], pbuf[1][rp]); - else - preprocessed = mPreproc->ProcessSample(pbuf[0][rp]); - return mEnvelope->ProcessSample(preprocessed); + return mEnvelope->ProcessSample(PreprocSample(pbuf, rp)); } inline void EffectCompressor2::CompressSample(float env, size_t wp) diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index f1b26e13d..9e223075d 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -28,6 +28,7 @@ class SamplePreprocessor public: virtual float ProcessSample(float value) = 0; virtual float ProcessSample(float valueL, float valueR) = 0; + virtual void Reset() = 0; }; class SlidingRmsPreprocessor : public SamplePreprocessor @@ -37,6 +38,7 @@ class SlidingRmsPreprocessor : public SamplePreprocessor virtual float ProcessSample(float value); virtual float ProcessSample(float valueL, float valueR); + virtual void Reset(); static const size_t REFRESH_WINDOW_EVERY = 1048576; // 1 MB @@ -58,6 +60,7 @@ class SlidingMaxPreprocessor : public SamplePreprocessor virtual float ProcessSample(float value); virtual float ProcessSample(float valueL, float valueR); + virtual void Reset(); private: std::vector<float> mWindow; @@ -75,8 +78,14 @@ class EnvelopeDetector 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; } protected: size_t mPos; + float mInitialCondition; + size_t mInitialBlockSize; std::vector<float> mLookaheadBuffer; std::vector<float> mProcessingBuffer; std::vector<float> mProcessedBuffer; @@ -102,6 +111,7 @@ class Pt1EnvelopeDetector : public EnvelopeDetector public: Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime, size_t buffer_size = 0, bool correctGain = true); + virtual void CalcInitialCondition(float value); private: double mGainCorrection; @@ -124,6 +134,7 @@ struct PipelineBuffer 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(); @@ -182,6 +193,7 @@ private: 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(); From 1d728bec89a4eda74ff2537a56368fb29df730f9 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Fri, 12 Jun 2020 13:47:02 +0200 Subject: [PATCH 14/33] Improve octave test framework and refactor loudness test. Add helper functions "import_from_aud" and "export_to_aud" to de-deplicate common code in tests. The export functions reads back the exported signal to remove the impact of quantization errors during wav export from test results. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- tests/octave/.gitignore | 1 + tests/octave/loudness_test.m | 95 +++++++++--------------------------- tests/octave/run_test.m | 38 ++++++++++++--- 3 files changed, 57 insertions(+), 77 deletions(-) create mode 100644 tests/octave/.gitignore diff --git a/tests/octave/.gitignore b/tests/octave/.gitignore new file mode 100644 index 000000000..d8dd7532a --- /dev/null +++ b/tests/octave/.gitignore @@ -0,0 +1 @@ +*.wav diff --git a/tests/octave/loudness_test.m b/tests/octave/loudness_test.m index 43a9ea74d..ac53616c4 100644 --- a/tests/octave/loudness_test.m +++ b/tests/octave/loudness_test.m @@ -125,20 +125,12 @@ end ## Test Loudness LUFS mode: block to short and all silent CURRENT_TEST = "Loudness LUFS mode, short silent block"; fs= 44100; -x = zeros(ceil(fs*0.35), 2); -audiowrite(TMP_FILENAME, x, fs); -if EXPORT_TEST_SIGNALS - audiowrite(cstrcat(pwd(), "/Loudness-LUFS-silence-test.wav"), x, fs); -end +x1 = zeros(ceil(fs*0.35), 2); remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs, "Loudness-LUFS-silence-test.wav"); aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=1 NormalizeTo=0 StereoIndependent=0\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(2); do_test_equ(y, x, "identity"); ## Test Loudness LUFS mode: stereo dependent @@ -146,76 +138,50 @@ CURRENT_TEST = "Loudness LUFS mode, keep DC and stereo balance"; randn("seed", 1); # Include some silence in the test signal to test loudness gating # and vary the overall loudness over time. -x = [0.1*randn(15*fs, 2).', zeros(5*fs, 2).', 0.1*randn(15*fs, 2).'].'; -x(:,1) = x(:,1) .* sin(2*pi/fs/35*(1:1:35*fs)).' .* 1.2; -x(:,2) = x(:,2) .* sin(2*pi/fs/35*(1:1:35*fs)).'; -audiowrite(TMP_FILENAME, x, fs); -if EXPORT_TEST_SIGNALS - audiowrite(cstrcat(pwd(), "/Loudness-LUFS-stereo-test.wav"), x, fs); -end +x1 = [0.1*randn(15*fs, 2).', zeros(5*fs, 2).', 0.1*randn(15*fs, 2).'].'; +x1(:,1) = x1(:,1) .* sin(2*pi/fs/35*(1:1:35*fs)).' .* 1.2; +x1(:,2) = x1(:,2) .* sin(2*pi/fs/35*(1:1:35*fs)).'; remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs, "Loudness-LUFS-stereo-test.wav"); aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=1 NormalizeTo=0 StereoIndependent=0\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(2); do_test_equ(calc_LUFS(y, fs), -23, "loudness", LUFS_epsilon); do_test_neq(calc_LUFS(y(:,1), fs), calc_LUFS(y(:,2), fs), "stereo balance", 1); ## Test Loudness LUFS mode, stereo independent CURRENT_TEST = "Loudness LUFS mode, stereo independence"; -audiowrite(TMP_FILENAME, x, fs); remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs); aud_do("LoudnessNormalization: LUFSLevel=-23 DualMono=0 NormalizeTo=0 StereoIndependent=1\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(2); # Independently processed stereo channels have half the target loudness. do_test_equ(calc_LUFS(y(:,1), fs), -26, "channel 1 loudness", LUFS_epsilon); do_test_equ(calc_LUFS(y(:,2), fs), -26, "channel 2 loudness", LUFS_epsilon); ## Test Loudness LUFS mode: mono as mono CURRENT_TEST = "Test Loudness LUFS mode: mono as mono"; -x = x(:,1); -audiowrite(TMP_FILENAME, x, fs); -if EXPORT_TEST_SIGNALS - audiowrite(cstrcat(pwd(), "/Loudness-LUFS-mono-test.wav"), x, fs); -end +x1 = x1(:,1); remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs, "Loudness-LUFS-mono-test.wav"); aud_do("LoudnessNormalization: LUFSLevel=-26 DualMono=0 NormalizeTo=0 StereoIndependent=1\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=1\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(1); do_test_equ(calc_LUFS(y, fs), -26, "loudness", LUFS_epsilon); ## Test Loudness LUFS mode: mono as dual-mono CURRENT_TEST = "Test Loudness LUFS mode: mono as dual-mono"; -audiowrite(TMP_FILENAME, x, fs); - remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs); aud_do("LoudnessNormalization: LUFSLevel=-26 DualMono=1 NormalizeTo=0 StereoIndependent=0\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=1\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(1); # This shall be 3 LU quieter as it is compared to strict spec. do_test_equ(calc_LUFS(y, fs), -29, "loudness", LUFS_epsilon); ## Test Loudness LUFS mode: multi-rate project CURRENT_TEST = "Test Loudness LUFS mode: multi-rate project"; -audiowrite(TMP_FILENAME, x, fs); +audiowrite(TMP_FILENAME, x1, fs); +x = audioread(TMP_FILENAME); remove_all_tracks(); aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); @@ -228,6 +194,7 @@ audiowrite(TMP_FILENAME, x1, fs1); if EXPORT_TEST_SIGNALS audiowrite(cstrcat(pwd(), "/Loudness-LUFS-stereo-test-8kHz.wav"), x1, fs1); end +x1 = audioread(TMP_FILENAME); aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); select_tracks(0, 100); @@ -255,36 +222,22 @@ do_test_neq(calc_LUFS(y1(:,1), fs), calc_LUFS(y1(:,2), fs), "stereo balance trac CURRENT_TEST = "Loudness RMS mode, stereo independent"; randn("seed", 1); fs= 44100; -x = 0.1*randn(30*fs, 2); -x(:,1) = x(:,1) * 0.6; -audiowrite(TMP_FILENAME, x, fs); -if EXPORT_TEST_SIGNALS - audiowrite(cstrcat(pwd(), "/Loudness-RMS-test.wav"), x, fs); -end +x1 = 0.1*randn(30*fs, 2); +x1(:,1) = x1(:,1) * 0.6; remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs, "Loudness-RMS-test.wav"); aud_do("LoudnessNormalization: RMSLevel=-20 DualMono=0 NormalizeTo=1 StereoIndependent=1\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(2); do_test_equ(20*log10(sqrt(sum(y(:,1).*y(:,1)/length(y)))), -20, "channel 1 RMS"); do_test_equ(20*log10(sqrt(sum(y(:,2).*y(:,2)/length(y)))), -20, "channel 2 RMS"); ## Test Loudness RMS mode: stereo dependent CURRENT_TEST = "Loudness RMS mode, stereo dependent"; -audiowrite(TMP_FILENAME, x, fs); - remove_all_tracks(); -aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); -select_tracks(0, 100); +x = export_to_aud(x1, fs); aud_do("LoudnessNormalization: RMSLevel=-22 DualMono=1 NormalizeTo=1 StereoIndependent=0\n"); -aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=2\n")); -system("sync"); - -y = audioread(TMP_FILENAME); +y = import_from_aud(2); # Stereo RMS must be calculated in quadratic domain. do_test_equ(20*log10(sqrt(sum(rms(y).^2)/size(y)(2))), -22, "RMS"); do_test_neq(20*log10(rms(y(:,1))), 20*log10(rms(y(:,2))), "stereo balance", 1); diff --git a/tests/octave/run_test.m b/tests/octave/run_test.m index d14810334..3496ddd7f 100755 --- a/tests/octave/run_test.m +++ b/tests/octave/run_test.m @@ -29,10 +29,13 @@ if nargin == 2 end ## Initialization and helper functions +global TMP_FILENAME; +global EXPORT_TEST_SIGNALS; UID=num2str(getuid()); PIPE_TO_PATH=strcat("/tmp/audacity_script_pipe.to.", UID); PIPE_FROM_PATH=strcat("/tmp/audacity_script_pipe.from.", UID); TMP_FILENAME=strcat(pwd(), "/tmp.wav"); +EXPORT_TEST_SIGNALS = false; printf("Open scripting pipes, this may freeze if Audacity does not run...\n"); @@ -74,6 +77,27 @@ function select_tracks(num, count) aud_do(sprintf("SelectTracks: Track=%d TrackCount=%d Mode=Set\n", num, count)); end +function x_in = import_from_aud(channels) + global TMP_FILENAME; + aud_do(cstrcat("Export2: Filename=\"", TMP_FILENAME, "\" NumChannels=", ... + num2str(channels), "\n")); + system("sync"); + x_in = audioread(TMP_FILENAME); +end + +function x_out = export_to_aud(x, fs, name = "") + global TMP_FILENAME; + global EXPORT_TEST_SIGNALS; + audiowrite(TMP_FILENAME, x, fs); + if EXPORT_TEST_SIGNALS && length(name) != 0 + audiowrite(cstrcat(pwd(), "/", name), x, fs); + end + # Read it back to avoid quantization-noise in tests + x_out = audioread(TMP_FILENAME); + aud_do(cstrcat("Import2: Filename=\"", TMP_FILENAME, "\"\n")); + select_tracks(0, 100); +end + ## Float equal comparison helper function [ret] = float_eq(x, y, eps=0.001) ret = abs(x - y) < eps; @@ -99,41 +123,43 @@ function plot_failure(x, y) plot(x, 'r') hold on plot(y, 'b') - plot(log10(abs(x-y)), 'g') + delta = abs(x-y); + max(delta) + plot(log10(delta), 'g') hold off legend("Audacity", "Octave", "log-delta", "location", "southeast") input("Press enter to continue", "s") end -function do_test_equ(x, y, msg, eps=0.001, skip = false) +function do_test_equ(x, y, msg = "", eps = 0.001, skip = false) cmp = all(all(float_eq(x, y, eps))); if do_test(cmp, msg, skip) == 0 plot_failure(x, y); end end -function do_test_neq(x, y, msg, eps=0.001, skip = false) +function do_test_neq(x, y, msg = "", eps = 0.001, skip = false) cmp = all(all(!float_eq(x, y, eps))); if do_test(cmp, msg, skip) == 0 plot_failure(x, y); end end -function do_test_gte(x, y, msg, skip = false) +function do_test_gte(x, y, msg = "", skip = false) cmp = all(all(x >= y)); if do_test(cmp, msg, skip) == 0 plot_failure(x, y); end end -function do_test_lte(x, y, msg, skip = false) +function do_test_lte(x, y, msg = "", skip = false) cmp = all(all(x <= y)); if do_test(cmp, msg, skip) == 0 plot_failure(x, y); end end -function result = do_test(result, msg, skip = false) +function result = do_test(result, msg = "", skip = false) global TESTS_RUN; global TESTS_FAILED; global TESTS_SKIPPED; From 2a6c2aaf806b01a4df27a4ea86fcf476176964cc Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sun, 31 May 2020 10:49:08 +0200 Subject: [PATCH 15/33] Add Compressor 2 octave tests. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- tests/octave/compressor2_test.m | 306 ++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 tests/octave/compressor2_test.m diff --git a/tests/octave/compressor2_test.m b/tests/octave/compressor2_test.m new file mode 100644 index 000000000..b24c83a82 --- /dev/null +++ b/tests/octave/compressor2_test.m @@ -0,0 +1,306 @@ +## 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, makeup) + makeupG_DB = -thresh_DB * (1-1/ratio) * makeup / 100; + 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) = makeupG_DB; + gain_DB(aboveKnee) = thresh_DB + ... + (env_DB(aboveKnee) - thresh_DB) / ratio + ... + makeupG_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) + makeupG_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("CompressorV2: 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("CompressorV2: 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("CompressorV2: 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("CompressorV2: 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("CompressorV2: 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("CompressorV2: Threshold=-6 Algorithm=1 CompressBy=0 Ratio=2.0 AttackTime=1.0 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0 MakeupGain=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("CompressorV2: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.1 KneeWidth=5 MakeupGain=50\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, 50).*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("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.2 KneeWidth=3 MakeupGain=80\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, 80) ... + .*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("CompressorV2: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\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, 50).*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("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.00001 ReleaseTime=0.00001 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("CompressorV2: 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("CompressorV2: 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("CompressorV2: 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("CompressorV2: 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("CompressorV2: Threshold=-17 Algorithm=1 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\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, 50).*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("CompressorV2: Threshold=-20 Algorithm=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.1 KneeWidth=3 CompressBy=1 MakeupGain=60\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, 60) ... + .*settled(x, fs, 2.5)); From aa619de49c16eca0a3cd34ea954b4c621599e563 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Tue, 15 Sep 2020 15:23:42 +0200 Subject: [PATCH 16/33] Add realtime support to Compressor2 effect. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 176 ++++++++++++++++++++++++++++++++---- src/effects/Compressor2.h | 36 +++++++- 2 files changed, 190 insertions(+), 22 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index f4916d364..9a2bf5bdf 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -133,6 +133,12 @@ void SlidingRmsPreprocessor::Reset() 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) @@ -198,6 +204,13 @@ void SlidingMaxPreprocessor::Reset() std::fill(mMaxes.begin(), mMaxes.end(), 0); } +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(); @@ -272,8 +285,14 @@ ExpFitEnvelopeDetector::ExpFitEnvelopeDetector( float rate, float attackTime, float releaseTime, size_t bufferSize) : EnvelopeDetector(bufferSize) { - mAttackFactor = exp(-1.0 / (rate * attackTime)); - mReleaseFactor = exp(-1.0 / (rate * releaseTime)); + SetParams(rate, attackTime, releaseTime); +} + +void ExpFitEnvelopeDetector::SetParams( + float sampleRate, float attackTime, float releaseTime) +{ + mAttackFactor = exp(-1.0 / (sampleRate * attackTime)); + mReleaseFactor = exp(-1.0 / (sampleRate * releaseTime)); } void ExpFitEnvelopeDetector::Follow() @@ -361,17 +380,24 @@ void ExpFitEnvelopeDetector::Follow() Pt1EnvelopeDetector::Pt1EnvelopeDetector( float rate, float attackTime, float releaseTime, size_t bufferSize, bool correctGain) - : EnvelopeDetector(bufferSize) + : EnvelopeDetector(bufferSize), + mCorrectGain(correctGain) +{ + SetParams(rate, attackTime, releaseTime); +} + +void Pt1EnvelopeDetector::SetParams( + float sampleRate, float attackTime, float releaseTime) { // Approximate peak amplitude correction factor. - if(correctGain) + if(mCorrectGain) mGainCorrection = 1.0 + exp(attackTime / 30.0); else mGainCorrection = 1.0; - mAttackFactor = 1.0 / (attackTime * rate); - mReleaseFactor = 1.0 / (releaseTime * rate); - mInitialBlockSize = std::min(size_t(rate * sqrt(attackTime)), bufferSize); + 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) @@ -461,7 +487,11 @@ void PipelineBuffer::free() } EffectCompressor2::EffectCompressor2() - : mIgnoreGuiEvents(false) + : mIgnoreGuiEvents(false), + mAlgorithmCtrl(0), + mPreprocCtrl(0), + mAttackTimeCtrl(0), + mLookaheadTimeCtrl(0) { mAlgorithm = DEF_Algorithm; mCompressBy = DEF_CompressBy; @@ -508,6 +538,84 @@ EffectType EffectCompressor2::GetType() return EffectTypeProcess; } +bool EffectCompressor2::SupportsRealtime() +{ +#if defined(EXPERIMENTAL_REALTIME_AUDACITY_EFFECTS) + return true; +#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); + + 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); + 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 ) { @@ -714,24 +822,22 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) { S.SetStretchyCol(1); - wxChoice* ctrl = nullptr; - - ctrl = S.Validator<wxGenericValidator>(&mAlgorithm) + mAlgorithmCtrl = S.Validator<wxGenericValidator>(&mAlgorithm) .AddChoice(XO("Envelope Algorithm:"), Msgids(kAlgorithmStrings, nAlgos), mAlgorithm); - wxSize box_size = ctrl->GetMinSize(); + wxSize box_size = mAlgorithmCtrl->GetMinSize(); int width = S.GetParent()->GetTextExtent(wxString::Format( "%sxxxx", kAlgorithmStrings[nAlgos-1].Translation())).GetWidth(); box_size.SetWidth(width); - ctrl->SetMinSize(box_size); + mAlgorithmCtrl->SetMinSize(box_size); - ctrl = S.Validator<wxGenericValidator>(&mCompressBy) + mPreprocCtrl = S.Validator<wxGenericValidator>(&mCompressBy) .AddChoice(XO("Compress based on:"), Msgids(kCompressByStrings, nBy), mCompressBy); - ctrl->SetMinSize(box_size); + mPreprocCtrl->SetMinSize(box_size); S.Validator<wxGenericValidator>(&mStereoInd) .AddCheckBox(XO("Compress stereo channels independently"), @@ -784,12 +890,12 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) S.AddVariableText(XO("Attack Time:"), true, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); - ctrl = S.Name(XO("Attack Time")) + mAttackTimeCtrl = S.Name(XO("Attack Time")) .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) .AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime, MIN_AttackTime, ScaleToPrecision(SCL_AttackTime), &mAttackTime, SCL_AttackTime / 1000); - ctrl->SetMinTextboxWidth(textbox_width); + mAttackTimeCtrl->SetMinTextboxWidth(textbox_width); S.AddVariableText(XO("s"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); @@ -806,12 +912,12 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) S.AddVariableText(XO("Lookahead Time:"), true, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); - ctrl = S.Name(XO("Lookahead Time")) + mLookaheadTimeCtrl = S.Name(XO("Lookahead Time")) .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) .AddSliderTextCtrl({}, DEF_LookaheadTime, MAX_LookaheadTime, MIN_LookaheadTime, ScaleToPrecision(SCL_LookaheadTime), &mLookaheadTime); - ctrl->SetMinTextboxWidth(textbox_width); + mLookaheadTimeCtrl->SetMinTextboxWidth(textbox_width); S.AddVariableText(XO("s"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); @@ -988,6 +1094,24 @@ void EffectCompressor2::AllocPipeline() mPipeline[i].init(capacity, stereoTrackFound); } +void EffectCompressor2::AllocRealtimePipeline() +{ + mLookaheadLength = + std::max(0, int(round(mLookaheadTime * 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) @@ -1379,6 +1503,8 @@ void EffectCompressor2::UpdateUI() { UpdateCompressorPlot(); UpdateResponsePlot(); + if(mEnvelope.get() != nullptr) + UpdateRealtimeParams(); } void EffectCompressor2::UpdateCompressorPlot() @@ -1432,3 +1558,15 @@ void EffectCompressor2::UpdateResponsePlot() mResponsePlot->Refresh(false); } + +void EffectCompressor2::UpdateRealtimeParams() +{ + std::lock_guard<std::mutex> guard(mRealtimeMutex); + // TODO: extract it + size_t window_size = + std::max(1, int(round((mLookaheadTime + mLookbehindTime) * mSampleRate))); + mLookaheadLength = // TODO: depup this everywhere + std::max(0, int(round(mLookaheadTime * mSampleRate))); + mPreproc->SetWindowSize(window_size); + mEnvelope->SetParams(mSampleRate, mAttackTime, mReleaseTime); +} diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index 9e223075d..5506e5899 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -22,6 +22,7 @@ class Plot; class ShuttleGui; +class SliderTextCtrl; class SamplePreprocessor { @@ -29,6 +30,7 @@ class SamplePreprocessor virtual float ProcessSample(float value) = 0; virtual float ProcessSample(float valueL, float valueR) = 0; virtual void Reset() = 0; + virtual void SetWindowSize(size_t windowSize) = 0; }; class SlidingRmsPreprocessor : public SamplePreprocessor @@ -39,6 +41,7 @@ class SlidingRmsPreprocessor : public SamplePreprocessor virtual float ProcessSample(float value); virtual float ProcessSample(float valueL, float valueR); virtual void Reset(); + virtual void SetWindowSize(size_t windowSize); static const size_t REFRESH_WINDOW_EVERY = 1048576; // 1 MB @@ -61,6 +64,7 @@ class SlidingMaxPreprocessor : public SamplePreprocessor virtual float ProcessSample(float value); virtual float ProcessSample(float valueL, float valueR); virtual void Reset(); + virtual void SetWindowSize(size_t windowSize); private: std::vector<float> mWindow; @@ -82,6 +86,10 @@ class EnvelopeDetector virtual void CalcInitialCondition(float value); inline float InitialCondition() const { return mInitialCondition; } inline size_t InitialConditionSize() const { return mInitialBlockSize; } + + virtual void SetParams(float sampleRate, float attackTime, + float releaseTime) = 0; + protected: size_t mPos; float mInitialCondition; @@ -97,7 +105,10 @@ class ExpFitEnvelopeDetector : public EnvelopeDetector { public: ExpFitEnvelopeDetector(float rate, float attackTime, float releaseTime, - size_t buffer_size = 0); + size_t buffer_size); + + virtual void SetParams(float sampleRate, float attackTime, + float releaseTime); private: double mAttackFactor; @@ -110,10 +121,14 @@ class Pt1EnvelopeDetector : public EnvelopeDetector { public: Pt1EnvelopeDetector(float rate, float attackTime, float releaseTime, - size_t buffer_size = 0, bool correctGain = true); + size_t buffer_size, bool correctGain = true); virtual void CalcInitialCondition(float value); + virtual void SetParams(float sampleRate, float attackTime, + float releaseTime); + private: + bool mCorrectGain; double mGainCorrection; double mAttackFactor; double mReleaseFactor; @@ -160,9 +175,17 @@ public: // 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; @@ -187,6 +210,7 @@ private: size_t CalcBufferSize(size_t sampleRate); void AllocPipeline(); + void AllocRealtimePipeline(); void FreePipeline(); void SwapPipeline(); bool ProcessOne(TrackIterRange<WaveTrack> range); @@ -205,6 +229,7 @@ private: void UpdateUI(); void UpdateCompressorPlot(); void UpdateResponsePlot(); + void UpdateRealtimeParams(); static const int TAU_FACTOR = 5; static const size_t MIN_BUFFER_CAPACITY = 1048576; // 1MB @@ -218,6 +243,7 @@ private: double mTrackLen; bool mProcStereo; + std::mutex mRealtimeMutex; std::unique_ptr<SamplePreprocessor> mPreproc; std::unique_ptr<EnvelopeDetector> mEnvelope; @@ -246,9 +272,13 @@ private: static const size_t RESPONSE_PLOT_STEP_START = 2; static const size_t RESPONSE_PLOT_STEP_STOP = 3; + bool mIgnoreGuiEvents; Plot* mGainPlot; Plot* mResponsePlot; - bool mIgnoreGuiEvents; + wxChoice* mAlgorithmCtrl; + wxChoice* mPreprocCtrl; + SliderTextCtrl* mAttackTimeCtrl; + SliderTextCtrl* mLookaheadTimeCtrl; DECLARE_EVENT_TABLE() }; From dfffeb76dc2de79d3f5adb734eb5eced8df3d096 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Wed, 27 May 2020 21:13:55 +0200 Subject: [PATCH 17/33] Add Compressor2 realtime debugging utilities. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- scripts/debug/compressor2_trace.m | 63 ++++++++++++++++++++++++++ src/effects/Compressor2.cpp | 73 ++++++++++++++++++++++++++++++- src/effects/Compressor2.h | 5 +++ 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 scripts/debug/compressor2_trace.m diff --git a/scripts/debug/compressor2_trace.m b/scripts/debug/compressor2_trace.m new file mode 100644 index 000000000..fa8284dcf --- /dev/null +++ b/scripts/debug/compressor2_trace.m @@ -0,0 +1,63 @@ +## plot realtime trace data from Compressor2 effect + +stereo = true; +bfile = fopen("/tmp/audio.out"); + +if stereo + width = 15; +else + width = 13; +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.makeup_gain_pct = raw_data(:,8); +data.dry_wet_pct = raw_data(:,9); + +if stereo + data.in = horzcat(raw_data(:,10), raw_data(:,11)); + data.env = raw_data(:,12); + data.gain = raw_data(:,13); + data.out = horzcat(raw_data(:,14), raw_data(:,15)); +else + data.in = raw_data(:,10); + data.env = raw_data(:,11); + data.gain = raw_data(:,12); + data.out = raw_data(:,13); +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.makeup_gain_pct, 'r'); +plot(data.dry_wet_pct, '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", ... + "makeup", "dry/wet", "env*100", "gain*50"); +else + legend("in*100", "out*100", "threshold", "ratio", ... + "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ... + "makeup", "dry/wet", "env*100", "gain*50"); +end diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 9a2bf5bdf..52ccc5ee7 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -41,10 +41,12 @@ //#define DEBUG_COMPRESSOR2_DUMP_BUFFERS //#define DEBUG_COMPRESSOR2_ENV //#define DEBUG_COMPRESSOR2_TRACE +//#define DEBUG_COMPRESSOR2_TRACE2 -#ifdef DEBUG_COMPRESSOR2_DUMP_BUFFERS +#if defined(DEBUG_COMPRESSOR2_DUMP_BUFFERS) or defined(DEBUG_COMPRESSOR2_TRACE2) #include <fstream> int buf_num; +std::fstream debugfile; #endif enum kAlgorithms @@ -243,6 +245,15 @@ EnvelopeDetector::EnvelopeDetector(size_t buffer_size) { } +float EnvelopeDetector::AttackFactor() +{ + return 0; +} +float EnvelopeDetector::DecayFactor() +{ + return 0; +} + float EnvelopeDetector::ProcessSample(float value) { float retval = mProcessedBuffer[mPos]; @@ -386,6 +397,15 @@ Pt1EnvelopeDetector::Pt1EnvelopeDetector( SetParams(rate, attackTime, releaseTime); } +float Pt1EnvelopeDetector::AttackFactor() +{ + return mAttackFactor; +} +float Pt1EnvelopeDetector::DecayFactor() +{ + return mReleaseFactor; +} + void Pt1EnvelopeDetector::SetParams( float sampleRate, float attackTime, float releaseTime) { @@ -577,6 +597,12 @@ bool EffectCompressor2::RealtimeAddProcessor( 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; } @@ -590,6 +616,9 @@ bool EffectCompressor2::RealtimeFinalize() mLookaheadTimeCtrl->Enable(true); if(mAlgorithm == kExpFit) mAttackTimeCtrl->Enable(true); +#ifdef DEBUG_COMPRESSOR2_TRACE2 + debugfile.close(); +#endif return true; } @@ -731,6 +760,11 @@ bool EffectCompressor2::Process() 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)) { @@ -769,6 +803,9 @@ bool EffectCompressor2::Process() mPreproc.reset(nullptr); mEnvelope.reset(nullptr); FreePipeline(); +#ifdef DEBUG_COMPRESSOR2_TRACE2 + debugfile.close(); +#endif return bGoodResult; } @@ -1401,6 +1438,34 @@ inline float EffectCompressor2::EnvelopeSample(PipelineBuffer& pbuf, size_t rp) inline void EffectCompressor2::CompressSample(float env, size_t wp) { float gain = (1.0 - mDryWet) + CompressorGain(env) * mDryWet; + +#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 MakeupGainPct = mMakeupGainPct; + float DryWetPct = mDryWetPct; + + 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*)&MakeupGainPct, sizeof(float)); + debugfile.write((char*)&DryWetPct, 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; @@ -1411,6 +1476,12 @@ inline void EffectCompressor2::CompressSample(float env, size_t wp) #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() diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index 5506e5899..097c9bb30 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -90,6 +90,9 @@ class EnvelopeDetector virtual void SetParams(float sampleRate, float attackTime, float releaseTime) = 0; + virtual float AttackFactor(); + virtual float DecayFactor(); + protected: size_t mPos; float mInitialCondition; @@ -126,6 +129,8 @@ class Pt1EnvelopeDetector : public EnvelopeDetector virtual void SetParams(float sampleRate, float attackTime, float releaseTime); + virtual float AttackFactor(); + virtual float DecayFactor(); private: bool mCorrectGain; From 9606aa73122ad84b719ce5e745dd438741d2655a Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Mon, 14 Sep 2020 13:04:16 +0200 Subject: [PATCH 18/33] Deduplicate Compressor2 effect code. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 34 +++++++++++++++++----------------- src/effects/Compressor2.h | 5 ++++- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 52ccc5ee7..0680afd73 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -1068,9 +1068,7 @@ double EffectCompressor2::CompressorGain(double env) std::unique_ptr<SamplePreprocessor> EffectCompressor2::InitPreprocessor( double rate, bool preview) { - size_t window_size = - std::max(1, int(round((mLookaheadTime + mLookbehindTime) * rate))); - + size_t window_size = CalcWindowLength(rate); if(mCompressBy == kAmplitude) return std::unique_ptr<SamplePreprocessor>(safenew SlidingMaxPreprocessor(window_size)); @@ -1091,12 +1089,10 @@ std::unique_ptr<EnvelopeDetector> EffectCompressor2::InitEnvelope( !preview && mCompressBy != kAmplitude)); } -size_t EffectCompressor2::CalcBufferSize(size_t sampleRate) +size_t EffectCompressor2::CalcBufferSize(double sampleRate) { size_t capacity; - - mLookaheadLength = - std::max(0, int(round(mLookaheadTime * sampleRate))); + mLookaheadLength = CalcLookaheadLength(sampleRate); capacity = mLookaheadLength + size_t(float(TAU_FACTOR) * (1.0 + mAttackTime) * sampleRate); if(capacity < MIN_BUFFER_CAPACITY) @@ -1104,6 +1100,16 @@ size_t EffectCompressor2::CalcBufferSize(size_t sampleRate) 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() @@ -1133,9 +1139,7 @@ void EffectCompressor2::AllocPipeline() void EffectCompressor2::AllocRealtimePipeline() { - mLookaheadLength = - std::max(0, int(round(mLookaheadTime * mSampleRate))); - + mLookaheadLength = CalcLookaheadLength(mSampleRate); size_t blockSize = std::max(mLookaheadLength, size_t(512)); if(mAlgorithm == kExpFit) { @@ -1606,8 +1610,7 @@ void EffectCompressor2::UpdateResponsePlot() std::unique_ptr<EnvelopeDetector> envelope; float plot_rate = RESPONSE_PLOT_SAMPLES / RESPONSE_PLOT_TIME; - size_t lookahead_size = - std::max(0, int(round(mLookaheadTime * plot_rate))); + size_t lookahead_size = CalcLookaheadLength(plot_rate); ssize_t block_size = float(TAU_FACTOR) * (mAttackTime + 1.0) * plot_rate; preproc = InitPreprocessor(plot_rate, true); @@ -1633,11 +1636,8 @@ void EffectCompressor2::UpdateResponsePlot() void EffectCompressor2::UpdateRealtimeParams() { std::lock_guard<std::mutex> guard(mRealtimeMutex); - // TODO: extract it - size_t window_size = - std::max(1, int(round((mLookaheadTime + mLookbehindTime) * mSampleRate))); - mLookaheadLength = // TODO: depup this everywhere - std::max(0, int(round(mLookaheadTime * mSampleRate))); + size_t window_size = CalcWindowLength(mSampleRate); + mLookaheadLength = CalcLookaheadLength(mSampleRate); mPreproc->SetWindowSize(window_size); mEnvelope->SetParams(mSampleRate, mAttackTime, mReleaseTime); } diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index 097c9bb30..10cdd8921 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -212,7 +212,10 @@ private: double rate, bool preview = false); std::unique_ptr<EnvelopeDetector> InitEnvelope( double rate, size_t blockSize = 0, bool preview = false); - size_t CalcBufferSize(size_t sampleRate); + size_t CalcBufferSize(double sampleRate); + + inline size_t CalcLookaheadLength(double rate); + inline size_t CalcWindowLength(double rate); void AllocPipeline(); void AllocRealtimePipeline(); From 1b76326d0111de255c98dde7e164fc00405e6dd3 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Wed, 16 Sep 2020 09:01:21 +0200 Subject: [PATCH 19/33] Use TranslatableString::Format for Compressor v2 plot accessibility. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 0680afd73..8701a7748 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -831,6 +831,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) mResponsePlot = S.MinSize( { 200, 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>( @@ -1594,9 +1595,8 @@ void EffectCompressor2::UpdateCompressorPlot() plot->ydata[i] = plot->xdata[i] + LINEAR_TO_DB(CompressorGain(DB_TO_LINEAR(plot->xdata[i]))); -// XXX: accessibility but fails with TranslatableString required -// mGainPlot->SetName(wxString::Format( -// "Compressor gain reduction: %.1f dB", plot->ydata[xsize-1])); + mGainPlot->SetName(XO("Compressor gain reduction: %.1f dB"). + Format(plot->ydata[xsize-1])); mGainPlot->Refresh(false); } From e725c4825cbbc29cd63884f0292f04d08bbc8238 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Fri, 25 Sep 2020 10:55:33 +0200 Subject: [PATCH 20/33] Rename Compressor v2 to "Dynamic Compressor". Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 4 ++-- tests/octave/compressor2_test.m | 32 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 8701a7748..4ea2d295b 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -104,7 +104,7 @@ BEGIN_EVENT_TABLE(EffectCompressor2, wxEvtHandler) END_EVENT_TABLE() const ComponentInterfaceSymbol EffectCompressor2::Symbol -{ XO("Compressor v2") }; +{ XO("Dynamic Compressor") }; namespace{ BuiltinEffectsModule::Registration< EffectCompressor2 > reg; } @@ -548,7 +548,7 @@ TranslatableString EffectCompressor2::GetDescription() ManualPageID EffectCompressor2::ManualPage() { - return L"Compressor2"; + return L"Dynamic_Compressor"; } // EffectDefinitionInterface implementation diff --git a/tests/octave/compressor2_test.m b/tests/octave/compressor2_test.m index b24c83a82..90e714eb1 100644 --- a/tests/octave/compressor2_test.m +++ b/tests/octave/compressor2_test.m @@ -108,7 +108,7 @@ 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("CompressorV2: Threshold=-20 Algorithm=0 AttackTime=0.1 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n"); +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. @@ -119,7 +119,7 @@ CURRENT_TEST = "Compressor2, mono compression PT1"; x1 = x1.*10; remove_all_tracks(); x = export_to_aud(x1, fs); -aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2.5 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0.0 LookbehindTime=0.0 KneeWidth=12\n"); +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), ... @@ -132,7 +132,7 @@ 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("CompressorV2: Threshold=-23 Algorithm=1 CompressBy=1 Ratio=2.5 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=12\n"); +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 @@ -147,7 +147,7 @@ CURRENT_TEST = "Compressor2, mono compression PT1 - faded sinewave - medium sign 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("CompressorV2: Threshold=-10 Algorithm=1 CompressBy=0 Ratio=100 AttackTime=0.01 ReleaseTime=0.01 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n"); +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), ... @@ -160,7 +160,7 @@ 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("CompressorV2: Threshold=-10 Algorithm=1 CompressBy=0 Ratio=100 AttackTime=0.01 ReleaseTime=0.01 LookaheadTime=0 LookbehindTime=0 KneeWidth=0\n"); +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), ... @@ -173,7 +173,7 @@ CURRENT_TEST = "Compressor2, mono compression PT1 - sinewave - asymetric attack 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("CompressorV2: Threshold=-6 Algorithm=1 CompressBy=0 Ratio=2.0 AttackTime=1.0 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0 MakeupGain=0\n"); +aud_do("DynamicCompressor: Threshold=-6 Algorithm=1 CompressBy=0 Ratio=2.0 AttackTime=1.0 ReleaseTime=0.3 LookaheadTime=0 LookbehindTime=0 KneeWidth=0 MakeupGain=0\n"); y = import_from_aud(1); do_test_equ(settled(y, fs, 1), ... @@ -184,7 +184,7 @@ do_test_equ(settled(y, fs, 1), ... CURRENT_TEST = "Compressor2, mono asymmetric lookaround max"; remove_all_tracks(); x = export_to_aud(x1, fs); -aud_do("CompressorV2: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.1 KneeWidth=5 MakeupGain=50\n"); +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 MakeupGain=50\n"); y = import_from_aud(1); do_test_equ(settled(y, fs, 0.6), ... @@ -195,7 +195,7 @@ do_test_equ(settled(y, fs, 0.6), ... CURRENT_TEST = "Compressor2, mono asymmetric lookaround RMS"; remove_all_tracks(); x = export_to_aud(x1, fs); -aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.2 KneeWidth=3 MakeupGain=80\n"); +aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.2 KneeWidth=3 MakeupGain=80\n"); y = import_from_aud(1); do_test_equ(settled(y, fs, 2), ... @@ -208,7 +208,7 @@ remove_all_tracks(); x = export_to_aud(x1, fs); aud_do("Select: Start=2 End=5 Mode=Set\n"); -aud_do("CompressorV2: Threshold=-17 Algorithm=1 CompressBy=0 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\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 MakeupGain=50\n"); y = import_from_aud(1); x = x(2*fs+1:5*fs); @@ -220,7 +220,7 @@ do_test_equ(settled(y, fs, 0.1), ... CURRENT_TEST = "Compressor2, mono, ultra short attack time"; remove_all_tracks(); x = export_to_aud(x1, fs); -aud_do("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.00001 ReleaseTime=0.00001 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n"); +aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.00001 ReleaseTime=0.00001 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n"); y = import_from_aud(2); # XXX: use larger epsilon due to numerical issues @@ -238,7 +238,7 @@ 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("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n"); +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), ... @@ -247,7 +247,7 @@ do_test_equ(settled(y, fs, 1), ... remove_all_tracks(); x = export_to_aud(x1, fs); -aud_do("CompressorV2: Threshold=-20 Algorithm=1 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10 StereoIndependent=1\n"); +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), ... @@ -264,7 +264,7 @@ 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("CompressorV2: Threshold=-20 Algorithm=1 CompressBy=0 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n"); +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), ... @@ -273,7 +273,7 @@ do_test_equ(settled(y, fs, 1), ... remove_all_tracks(); x = export_to_aud(x2, fs); -aud_do("CompressorV2: Threshold=-20 Algorithm=1 Ratio=2 AttackTime=0.5 ReleaseTime=0.5 LookaheadTime=0 LookbehindTime=0 KneeWidth=10 StereoIndependent=1\n"); +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), ... @@ -287,7 +287,7 @@ do_test_equ(settled(y(:,2), fs, 1), ... CURRENT_TEST = "Compressor2, stereo lookaround max"; remove_all_tracks(); x = export_to_aud(x1, fs); -aud_do("CompressorV2: Threshold=-17 Algorithm=1 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\n"); +aud_do("DynamicCompressor: Threshold=-17 Algorithm=1 Ratio=1.2 AttackTime=0.3 ReleaseTime=0.3 LookaheadTime=0.2 LookbehindTime=0.2 KneeWidth=5 MakeupGain=50\n"); y = import_from_aud(2); do_test_equ(settled(y, fs, 0.6), ... @@ -298,7 +298,7 @@ do_test_equ(settled(y, fs, 0.6), ... CURRENT_TEST = "Compressor2, stereo lookaround RMS"; remove_all_tracks(); x = export_to_aud(x1, fs); -aud_do("CompressorV2: Threshold=-20 Algorithm=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.1 KneeWidth=3 CompressBy=1 MakeupGain=60\n"); +aud_do("DynamicCompressor: Threshold=-20 Algorithm=1 Ratio=3 AttackTime=1 ReleaseTime=1 LookaheadTime=0.1 LookbehindTime=0.1 KneeWidth=3 CompressBy=1 MakeupGain=60\n"); y = import_from_aud(2); do_test_equ(settled(y, fs, 2.5), ... From 510f89567ad4a0ef57ffbc2589957b118a484866 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Fri, 25 Sep 2020 12:41:27 +0200 Subject: [PATCH 21/33] Optimize Compressor dialog layout. The old layout did not fit well on small screens. I tried using StartScroller() but this caused problems on large screens. Also fix clipped text in textboxes on some themes. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 97 +++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 4ea2d295b..8f8ea52d8 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -813,7 +813,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) { S.SetBorder(10); - S.StartHorizontalLay(wxEXPAND, true); + S.StartHorizontalLay(wxEXPAND, 1); { PlotData* plot; @@ -856,6 +856,11 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) S.StartStatic(XO("Algorithm")); { + wxSize box_size; + int width; + + S.StartHorizontalLay(wxEXPAND, 1); + S.StartVerticalLay(1); S.StartMultiColumn(2, wxALIGN_LEFT); { S.SetStretchyCol(1); @@ -865,33 +870,48 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) Msgids(kAlgorithmStrings, nAlgos), mAlgorithm); - wxSize box_size = mAlgorithmCtrl->GetMinSize(); - int width = S.GetParent()->GetTextExtent(wxString::Format( + 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.Validator<wxGenericValidator>(&mStereoInd) - .AddCheckBox(XO("Compress stereo channels independently"), - DEF_StereoInd); } 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.000001XX").GetWidth(); + SliderTextCtrl* ctrl = nullptr; + + S.StartHorizontalLay(wxEXPAND, true); + S.StartVerticalLay(1); S.StartMultiColumn(3, wxEXPAND); { S.SetStretchyCol(1); - int textbox_width = S.GetParent()->GetTextExtent("0.000001").GetWidth(); - SliderTextCtrl* ctrl = nullptr; S.AddVariableText(XO("Threshold:"), true, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); @@ -926,6 +946,40 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) S.AddVariableText(XO("dB"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + /* i18n-hint: Make-up, i.e. correct for any reduction, rather than fabricate it.*/ + S.AddVariableText(XO("Make-up Gain:"), true, + wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + ctrl = S.Name(XO("Make-up Gain")) + .Style(SliderTextCtrl::HORIZONTAL) + .AddSliderTextCtrl({}, DEF_MakeupGain, MAX_MakeupGain, + MIN_MakeupGain, ScaleToPrecision(SCL_MakeupGain), + &mMakeupGainPct); + ctrl->SetMinTextboxWidth(textbox_width); + S.AddVariableText(XO("%"), true, + wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + + S.AddVariableText(XO("Dry/Wet:"), true, + wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + ctrl = S.Name(XO("Dry/Wet")) + .Style(SliderTextCtrl::HORIZONTAL) + .AddSliderTextCtrl({}, DEF_DryWet, MAX_DryWet, + MIN_DryWet, ScaleToPrecision(SCL_DryWet), + &mDryWetPct); + ctrl->SetMinTextboxWidth(textbox_width); + S.AddVariableText(XO("%"), 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 Time:"), true, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); mAttackTimeCtrl = S.Name(XO("Attack Time")) @@ -969,31 +1023,10 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) ctrl->SetMinTextboxWidth(textbox_width); S.AddVariableText(XO("s"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - - /* i18n-hint: Make-up, i.e. correct for any reduction, rather than fabricate it.*/ - S.AddVariableText(XO("Make-up Gain:"), true, - wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); - ctrl = S.Name(XO("Make-up Gain")) - .Style(SliderTextCtrl::HORIZONTAL) - .AddSliderTextCtrl({}, DEF_MakeupGain, MAX_MakeupGain, - MIN_MakeupGain, ScaleToPrecision(SCL_MakeupGain), - &mMakeupGainPct); - ctrl->SetMinTextboxWidth(textbox_width); - S.AddVariableText(XO("%"), true, - wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - - S.AddVariableText(XO("Dry/Wet:"), true, - wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); - ctrl = S.Name(XO("Dry/Wet")) - .Style(SliderTextCtrl::HORIZONTAL) - .AddSliderTextCtrl({}, DEF_DryWet, MAX_DryWet, - MIN_DryWet, ScaleToPrecision(SCL_DryWet), - &mDryWetPct); - ctrl->SetMinTextboxWidth(textbox_width); - S.AddVariableText(XO("%"), true, - wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); } S.EndMultiColumn(); + S.EndVerticalLay(); + S.EndHorizontalLay(); } S.EndVerticalLay(); } From be1883bdd30d93e942f0b0bfffd4fc3a88010b0a Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sat, 26 Sep 2020 14:10:14 +0200 Subject: [PATCH 22/33] Fix invalid Compressor plots if out of range values are entered in textboxes. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 8f8ea52d8..c9b0c1e7f 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -97,6 +97,11 @@ 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) @@ -1622,6 +1627,15 @@ void EffectCompressor2::UpdateCompressorPlot() 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(mMakeupGainPct, MIN_MakeupGain, MAX_MakeupGain)) + return; + InitGainCalculation(); size_t xsize = plot->xdata.size(); for(size_t i = 0; i < xsize; ++i) @@ -1639,6 +1653,15 @@ void EffectCompressor2::UpdateResponsePlot() 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; From 4b4fbafb0e06f9a34bc1477b6b77f91c429abc12 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Tue, 2 Feb 2021 19:21:32 +0100 Subject: [PATCH 23/33] Remove "PT1" from dynamic compressor effect dialog. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index c9b0c1e7f..4546bd9ea 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -59,7 +59,7 @@ enum kAlgorithms static const ComponentInterfaceSymbol kAlgorithmStrings[nAlgos] = { { XO("Exponential-Fit") }, - { XO("Analog Model (PT1)") } + { XO("Analog Model") } }; enum kCompressBy From 7a23e6a52fcd7846f18a0aebfb9b2970ddd54f1c Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Tue, 2 Feb 2021 19:30:32 +0100 Subject: [PATCH 24/33] Remove Dry/Wet mix from dynamic compressor effect. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- scripts/debug/compressor2_trace.m | 24 +++++++++++------------- src/effects/Compressor2.cpp | 23 +---------------------- src/effects/Compressor2.h | 2 -- 3 files changed, 12 insertions(+), 37 deletions(-) diff --git a/scripts/debug/compressor2_trace.m b/scripts/debug/compressor2_trace.m index fa8284dcf..0307a0a9c 100644 --- a/scripts/debug/compressor2_trace.m +++ b/scripts/debug/compressor2_trace.m @@ -4,9 +4,9 @@ stereo = true; bfile = fopen("/tmp/audio.out"); if stereo - width = 15; + width = 14; else - width = 13; + width = 12; end raw_data = reshape(fread(bfile, 'float'), width, []).'; @@ -20,18 +20,17 @@ data.release_time = raw_data(:,5); data.lookahead_time = raw_data(:,6); data.lookbehind_time = raw_data(:,7); data.makeup_gain_pct = raw_data(:,8); -data.dry_wet_pct = raw_data(:,9); if stereo - data.in = horzcat(raw_data(:,10), raw_data(:,11)); - data.env = raw_data(:,12); - data.gain = raw_data(:,13); - data.out = horzcat(raw_data(:,14), raw_data(:,15)); -else - data.in = raw_data(:,10); + data.in = horzcat(raw_data(:,9), raw_data(:,10)); data.env = raw_data(:,11); data.gain = raw_data(:,12); - data.out = raw_data(:,13); + 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); @@ -46,7 +45,6 @@ plot(data.release_time.*10, 'c', "linewidth", 2); plot(data.lookahead_time, 'm'); plot(data.lookbehind_time, 'm'); plot(data.makeup_gain_pct, 'r'); -plot(data.dry_wet_pct, 'r'); plot(data.env.*100, 'k', "linewidth", 2); plot(data.gain.*50, 'k', "linestyle", '--'); hold off; @@ -55,9 +53,9 @@ grid; if stereo legend("in*100", "in*100", "out*100", "out*100", "threshold", "ratio", ... "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ... - "makeup", "dry/wet", "env*100", "gain*50"); + "makeup", "env*100", "gain*50"); else legend("in*100", "out*100", "threshold", "ratio", ... "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ... - "makeup", "dry/wet", "env*100", "gain*50"); + "makeup", "env*100", "gain*50"); end diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 4546bd9ea..148137861 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -90,7 +90,6 @@ Param( ReleaseTime, double, wxT("ReleaseTime"), 1.0, 0.00001, 30. 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( MakeupGain, double, wxT("MakeupGain"), 0.0, 0.0, 100.0, 1.0 ); -Param( DryWet, double, wxT("DryWet"), 100.0, 0.0, 100.0, 1.0 ); inline int ScaleToPrecision(double scale) { @@ -530,7 +529,6 @@ EffectCompressor2::EffectCompressor2() mLookaheadTime = DEF_LookaheadTime; mLookbehindTime = DEF_LookbehindTime; mMakeupGainPct = DEF_MakeupGain; - mDryWetPct = DEF_DryWet; SetLinearEffectFlag(false); } @@ -665,7 +663,6 @@ bool EffectCompressor2::DefineParams( ShuttleParams & S ) S.SHUTTLE_PARAM(mLookaheadTime, LookaheadTime); S.SHUTTLE_PARAM(mLookbehindTime, LookbehindTime); S.SHUTTLE_PARAM(mMakeupGainPct, MakeupGain); - S.SHUTTLE_PARAM(mDryWetPct, DryWet); return true; } @@ -684,7 +681,6 @@ bool EffectCompressor2::GetAutomationParameters(CommandParameters & parms) parms.Write(KEY_LookaheadTime, mLookaheadTime); parms.Write(KEY_LookbehindTime, mLookbehindTime); parms.Write(KEY_MakeupGain, mMakeupGainPct); - parms.Write(KEY_DryWet, mDryWetPct); return true; } @@ -703,7 +699,6 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms) ReadAndVerifyDouble(LookaheadTime); ReadAndVerifyDouble(LookbehindTime); ReadAndVerifyDouble(MakeupGain); - ReadAndVerifyDouble(DryWet); mAlgorithm = Algorithm; mCompressBy = CompressBy; @@ -717,7 +712,6 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms) mLookaheadTime = LookaheadTime; mLookbehindTime = LookbehindTime; mMakeupGainPct = MakeupGain; - mDryWetPct = DryWet; return true; } @@ -747,7 +741,6 @@ bool EffectCompressor2::Startup() mLookaheadTime = DEF_LookaheadTime; mLookbehindTime = DEF_LookbehindTime; mMakeupGainPct = DEF_MakeupGain; - mDryWetPct = DEF_DryWet; SaveUserPreset(GetCurrentSettingsGroup()); @@ -962,17 +955,6 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) ctrl->SetMinTextboxWidth(textbox_width); S.AddVariableText(XO("%"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - - S.AddVariableText(XO("Dry/Wet:"), true, - wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); - ctrl = S.Name(XO("Dry/Wet")) - .Style(SliderTextCtrl::HORIZONTAL) - .AddSliderTextCtrl({}, DEF_DryWet, MAX_DryWet, - MIN_DryWet, ScaleToPrecision(SCL_DryWet), - &mDryWetPct); - ctrl->SetMinTextboxWidth(textbox_width); - S.AddVariableText(XO("%"), true, - wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); } S.EndMultiColumn(); S.EndVerticalLay(); @@ -1066,7 +1048,6 @@ bool EffectCompressor2::TransferDataFromWindow() void EffectCompressor2::InitGainCalculation() { - mDryWet = mDryWetPct / 100.0; mMakeupGainDB = mMakeupGainPct / 100.0 * -(mThresholdDB * (1.0 - 1.0 / mRatio)); mMakeupGain = DB_TO_LINEAR(mMakeupGainDB); @@ -1480,7 +1461,7 @@ inline float EffectCompressor2::EnvelopeSample(PipelineBuffer& pbuf, size_t rp) inline void EffectCompressor2::CompressSample(float env, size_t wp) { - float gain = (1.0 - mDryWet) + CompressorGain(env) * mDryWet; + float gain = CompressorGain(env); #ifdef DEBUG_COMPRESSOR2_TRACE2 float ThresholdDB = mThresholdDB; @@ -1491,7 +1472,6 @@ inline void EffectCompressor2::CompressSample(float env, size_t wp) float LookaheadTime = mLookaheadTime; float LookbehindTime = mLookbehindTime; float MakeupGainPct = mMakeupGainPct; - float DryWetPct = mDryWetPct; debugfile.write((char*)&ThresholdDB, sizeof(float)); debugfile.write((char*)&Ratio, sizeof(float)); @@ -1501,7 +1481,6 @@ inline void EffectCompressor2::CompressSample(float env, size_t wp) debugfile.write((char*)&LookaheadTime, sizeof(float)); debugfile.write((char*)&LookbehindTime, sizeof(float)); debugfile.write((char*)&MakeupGainPct, sizeof(float)); - debugfile.write((char*)&DryWetPct, sizeof(float)); debugfile.write((char*)&mPipeline[0][0][wp], sizeof(float)); if(mProcStereo) debugfile.write((char*)&mPipeline[0][1][wp], sizeof(float)); diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index 10cdd8921..a813a508e 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -267,10 +267,8 @@ private: double mLookaheadTime; double mLookbehindTime; double mMakeupGainPct; - double mDryWetPct; // cached intermediate values - double mDryWet; double mMakeupGain; double mMakeupGainDB; size_t mLookaheadLength; From 90d7c1d2265a33e88113c03b62b3a823870e044c Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sun, 7 Feb 2021 18:31:34 +0100 Subject: [PATCH 25/33] Add labels to Compressor2 plots and tweak parameter labels. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 148137861..214dd2346 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -815,6 +815,9 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) { PlotData* plot; + S.StartVerticalLay(); + S.AddVariableText(XO("Envelope dependent gain"), 0, + wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); mGainPlot = S.MinSize( { 200, 200 } ) .AddPlot({}, -60, 0, -60, 0, XO("dB"), XO("dB"), Ruler::LinearDBFormat, Ruler::LinearDBFormat); @@ -826,10 +829,15 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) plot->ydata.resize(61); std::iota(plot->xdata.begin(), plot->xdata.end(), -60); + S.EndVerticalLay(); + S.StartVerticalLay(); + + S.AddVariableText(XO("Envelope detector step response"), 0, + wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); mResponsePlot = S.MinSize( { 200, 200 } ) .AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""), Ruler::IntFormat, Ruler::RealFormat, 2); - mResponsePlot->SetName(XO("Compressor step response plot")); + mResponsePlot->SetName(XO("Envelope detector step response plot")); plot = mResponsePlot->GetPlotData(0); plot->pen = std::unique_ptr<wxPen>( @@ -847,6 +855,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) 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(); @@ -967,9 +976,9 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) { S.SetStretchyCol(1); - S.AddVariableText(XO("Attack Time:"), true, + S.AddVariableText(XO("Attack:"), true, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); - mAttackTimeCtrl = S.Name(XO("Attack Time")) + mAttackTimeCtrl = S.Name(XO("Attack")) .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) .AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime, MIN_AttackTime, ScaleToPrecision(SCL_AttackTime), @@ -978,9 +987,9 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) S.AddVariableText(XO("s"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - S.AddVariableText(XO("Release Time:"), true, + S.AddVariableText(XO("Release:"), true, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); - ctrl = S.Name(XO("Release Time")) + ctrl = S.Name(XO("Release")) .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) .AddSliderTextCtrl({}, DEF_ReleaseTime, MAX_ReleaseTime, MIN_ReleaseTime, ScaleToPrecision(SCL_ReleaseTime), From 3d4e504bc38955f9cc3c1cba39989811b109c5fa Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Tue, 9 Feb 2021 19:38:02 +0100 Subject: [PATCH 26/33] Draw full compressor response plot instead of just the envelope detector step response. Also increase minimum plot width for non realtime enabled case. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 53 ++++++++++++++++++++++++++++--------- src/effects/Compressor2.h | 9 ++++--- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 214dd2346..f458bd0bf 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -131,9 +131,9 @@ float SlidingRmsPreprocessor::ProcessSample(float valueL, float valueR) return DoProcessSample((valueL * valueL + valueR * valueR) / 2.0); } -void SlidingRmsPreprocessor::Reset() +void SlidingRmsPreprocessor::Reset(float level) { - mSum = 0; + mSum = (level / mGain) * (level / mGain) * float(mWindow.size()); mPos = 0; mInsertCount = 0; std::fill(mWindow.begin(), mWindow.end(), 0); @@ -203,11 +203,11 @@ float SlidingMaxPreprocessor::ProcessSample(float valueL, float valueR) return DoProcessSample((fabs(valueL) + fabs(valueR)) / 2.0); } -void SlidingMaxPreprocessor::Reset() +void SlidingMaxPreprocessor::Reset(float value) { mPos = 0; - std::fill(mWindow.begin(), mWindow.end(), 0); - std::fill(mMaxes.begin(), mMaxes.end(), 0); + std::fill(mWindow.begin(), mWindow.end(), value); + std::fill(mMaxes.begin(), mMaxes.end(), value); } void SlidingMaxPreprocessor::SetWindowSize(size_t windowSize) @@ -303,6 +303,13 @@ ExpFitEnvelopeDetector::ExpFitEnvelopeDetector( 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) { @@ -410,6 +417,14 @@ 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) { @@ -818,7 +833,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) S.StartVerticalLay(); S.AddVariableText(XO("Envelope dependent gain"), 0, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); - mGainPlot = S.MinSize( { 200, 200 } ) + mGainPlot = S.MinSize( { 400, 200 } ) .AddPlot({}, -60, 0, -60, 0, XO("dB"), XO("dB"), Ruler::LinearDBFormat, Ruler::LinearDBFormat); @@ -832,19 +847,19 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) S.EndVerticalLay(); S.StartVerticalLay(); - S.AddVariableText(XO("Envelope detector step response"), 0, + S.AddVariableText(XO("Compressor step response"), 0, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); - mResponsePlot = S.MinSize( { 200, 200 } ) + mResponsePlot = S.MinSize( { 400, 200 } ) .AddPlot({}, 0, 5, -0.2, 1.2, XO("s"), XO(""), Ruler::IntFormat, Ruler::RealFormat, 2); - mResponsePlot->SetName(XO("Envelope detector step response plot")); + 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, 0, 1, 1, 0, 0}; + plot->ydata = {0.1, 0.1, 1, 1, 0.1, 0.1}; plot = mResponsePlot->GetPlotData(1); plot->pen = std::unique_ptr<wxPen>( @@ -1655,11 +1670,16 @@ void EffectCompressor2::UpdateResponsePlot() 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; + InitGainCalculation(); 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; @@ -1667,12 +1687,21 @@ void EffectCompressor2::UpdateResponsePlot() for(ssize_t i = -lookahead_size; i < 2*block_size; ++i) { if(i < step_start || i > step_stop) - envelope->ProcessSample(preproc->ProcessSample(0)); + envelope->ProcessSample(preproc->ProcessSample(0.1)); else envelope->ProcessSample(preproc->ProcessSample(1)); } + for(ssize_t i = 0; i < xsize; ++i) - plot->ydata[i] = envelope->ProcessSample(preproc->ProcessSample(0)); + { + 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); } diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index a813a508e..de09581e8 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -29,7 +29,7 @@ class SamplePreprocessor public: virtual float ProcessSample(float value) = 0; virtual float ProcessSample(float valueL, float valueR) = 0; - virtual void Reset() = 0; + virtual void Reset(float value = 0) = 0; virtual void SetWindowSize(size_t windowSize) = 0; }; @@ -40,7 +40,7 @@ class SlidingRmsPreprocessor : public SamplePreprocessor virtual float ProcessSample(float value); virtual float ProcessSample(float valueL, float valueR); - virtual void Reset(); + virtual void Reset(float value = 0); virtual void SetWindowSize(size_t windowSize); static const size_t REFRESH_WINDOW_EVERY = 1048576; // 1 MB @@ -63,7 +63,7 @@ class SlidingMaxPreprocessor : public SamplePreprocessor virtual float ProcessSample(float value); virtual float ProcessSample(float valueL, float valueR); - virtual void Reset(); + virtual void Reset(float value = 0); virtual void SetWindowSize(size_t windowSize); private: @@ -87,6 +87,7 @@ class EnvelopeDetector 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; @@ -110,6 +111,7 @@ class ExpFitEnvelopeDetector : public EnvelopeDetector 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); @@ -127,6 +129,7 @@ class Pt1EnvelopeDetector : public EnvelopeDetector 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(); From fd4276e3f2540491ad368eae509f212e14360ffd Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Fri, 12 Feb 2021 15:10:14 +0100 Subject: [PATCH 27/33] Clamp attack/release times at 1/sample_rate. Otherwise, numerical instability could occur. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index f458bd0bf..5fa0aa4e3 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -313,6 +313,8 @@ void ExpFitEnvelopeDetector::Reset(float 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)); } @@ -428,6 +430,9 @@ void Pt1EnvelopeDetector::Reset(float 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); From dec7e72abce6a7d0cd75df8bd37a3a38b58d92cd Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Fri, 12 Feb 2021 14:25:37 +0100 Subject: [PATCH 28/33] Add "offset" to SliderTextCtrl widget This parameter adds an offset to the value before calculating the logarithm to tweak the scaling of the slider. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/ShuttleGui.cpp | 4 ++-- src/ShuttleGui.h | 2 +- src/widgets/SliderTextCtrl.cpp | 14 ++++++++------ src/widgets/SliderTextCtrl.h | 4 +++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/ShuttleGui.cpp b/src/ShuttleGui.cpp index ead5aa0f2..48f5e78b2 100644 --- a/src/ShuttleGui.cpp +++ b/src/ShuttleGui.cpp @@ -618,7 +618,7 @@ wxSlider * ShuttleGuiBase::AddSlider( SliderTextCtrl* ShuttleGuiBase::AddSliderTextCtrl( const TranslatableString &Prompt, double pos, double Max, double Min, - int precision, double* value, double scale) + int precision, double* value, double scale, double offset) { HandleOptionality( Prompt ); AddPrompt( Prompt ); @@ -627,7 +627,7 @@ SliderTextCtrl* ShuttleGuiBase::AddSliderTextCtrl( return wxDynamicCast(wxWindow::FindWindowById( miId, mpDlg), SliderTextCtrl); SliderTextCtrl * pSlider; mpWind = pSlider = safenew SliderTextCtrl(GetParent(), miId, - pos, Min, Max, precision, scale, wxDefaultPosition, wxDefaultSize, + pos, Min, Max, precision, scale, offset, wxDefaultPosition, wxDefaultSize, GetStyle( SliderTextCtrl::HORIZONTAL ), value ); diff --git a/src/ShuttleGui.h b/src/ShuttleGui.h index 04a97b28e..5bd72d9a8 100644 --- a/src/ShuttleGui.h +++ b/src/ShuttleGui.h @@ -267,7 +267,7 @@ public: SliderTextCtrl* AddSliderTextCtrl( const TranslatableString &Prompt, double pos, double Max, double Min = 0, - int precision = 2, double* value = NULL, double scale = 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. diff --git a/src/widgets/SliderTextCtrl.cpp b/src/widgets/SliderTextCtrl.cpp index 524d1a9f6..837152d89 100644 --- a/src/widgets/SliderTextCtrl.cpp +++ b/src/widgets/SliderTextCtrl.cpp @@ -29,7 +29,8 @@ wxDEFINE_EVENT(cEVT_SLIDERTEXT, wxCommandEvent); SliderTextCtrl::SliderTextCtrl(wxWindow *parent, wxWindowID winid, double value, double min, double max, int precision, double scale, - const wxPoint& pos, const wxSize& size, long style, double* varValue) + 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; @@ -38,6 +39,7 @@ SliderTextCtrl::SliderTextCtrl(wxWindow *parent, wxWindowID winid, m_min = min; m_max = max; m_zero = -std::numeric_limits<double>::infinity(); + m_offset = offset; if(m_int) { @@ -62,13 +64,13 @@ SliderTextCtrl::SliderTextCtrl(wxWindow *parent, wxWindowID winid, min = m_zero; } else - min = log10(min); + min = log10(min + m_offset); if(value <= 0.0) value = m_zero; else - value = log10(value); - max = log10(max); + value = log10(value + m_offset); + max = log10(max + m_offset); } m_sizer = safenew wxBoxSizer( @@ -118,7 +120,7 @@ void SliderTextCtrl::OnTextChange(wxCommandEvent& event) if(m_value == 0.0) value = m_zero; else - value = log10(m_value); + value = log10(m_value + m_offset); } m_slider->SetValue(round(value * m_scale)); event.SetEventType(cEVT_SLIDERTEXT); @@ -134,7 +136,7 @@ void SliderTextCtrl::OnSlider(wxCommandEvent& event) m_value = 0.0; else { - m_value = pow(10.0, m_value); + 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); } diff --git a/src/widgets/SliderTextCtrl.h b/src/widgets/SliderTextCtrl.h index f8d27efa0..b2c0d15da 100644 --- a/src/widgets/SliderTextCtrl.h +++ b/src/widgets/SliderTextCtrl.h @@ -37,7 +37,8 @@ class SliderTextCtrl : public wxPanelWrapper SliderTextCtrl(wxWindow *parent, wxWindowID winid, double value, double min, double max, int precision = 2, - double scale = 0, const wxPoint& pos = wxDefaultPosition, + double scale = 0, double offset = 0, + const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = HORIZONTAL, double* varValue = NULL); @@ -69,6 +70,7 @@ class SliderTextCtrl : public wxPanelWrapper double m_min; double m_max; double m_zero; + double m_offset; wxString m_format; DECLARE_EVENT_TABLE() From 5c14ec43d3ceb72526500427baba2187aec83321 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Thu, 11 Feb 2021 18:30:05 +0100 Subject: [PATCH 29/33] Tweak parameter ranges and lookaround keyboard tick scaling. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 16 ++++++++-------- tests/octave/compressor2_test.m | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 5fa0aa4e3..5af30a1ae 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -83,10 +83,10 @@ Param( CompressBy, int, wxT("CompressBy"), kAmplitude, 0, nBy-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, 10.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.00001, 30.0, 20000.0 ); -Param( ReleaseTime, double, wxT("ReleaseTime"), 1.0, 0.00001, 30.0, 20000.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( MakeupGain, double, wxT("MakeupGain"), 0.0, 0.0, 100.0, 1.0 ); @@ -931,7 +931,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) S.StartStatic(XO("Compressor")); { - int textbox_width = S.GetParent()->GetTextExtent("10.000001XX").GetWidth(); + int textbox_width = S.GetParent()->GetTextExtent("10.00001XX").GetWidth(); SliderTextCtrl* ctrl = nullptr; S.StartHorizontalLay(wxEXPAND, true); @@ -1002,7 +1002,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) .AddSliderTextCtrl({}, DEF_AttackTime, MAX_AttackTime, MIN_AttackTime, ScaleToPrecision(SCL_AttackTime), - &mAttackTime, SCL_AttackTime / 1000); + &mAttackTime, SCL_AttackTime / 100, 0.033); mAttackTimeCtrl->SetMinTextboxWidth(textbox_width); S.AddVariableText(XO("s"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); @@ -1013,7 +1013,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) .AddSliderTextCtrl({}, DEF_ReleaseTime, MAX_ReleaseTime, MIN_ReleaseTime, ScaleToPrecision(SCL_ReleaseTime), - &mReleaseTime, SCL_ReleaseTime / 1000); + &mReleaseTime, SCL_ReleaseTime / 100, 0.033); ctrl->SetMinTextboxWidth(textbox_width); S.AddVariableText(XO("s"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); @@ -1024,7 +1024,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) .AddSliderTextCtrl({}, DEF_LookaheadTime, MAX_LookaheadTime, MIN_LookaheadTime, ScaleToPrecision(SCL_LookaheadTime), - &mLookaheadTime); + &mLookaheadTime, SCL_LookaheadTime / 10); mLookaheadTimeCtrl->SetMinTextboxWidth(textbox_width); S.AddVariableText(XO("s"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); @@ -1035,7 +1035,7 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) .Style(SliderTextCtrl::HORIZONTAL | SliderTextCtrl::LOG) .AddSliderTextCtrl({}, DEF_LookbehindTime, MAX_LookbehindTime, MIN_LookbehindTime, ScaleToPrecision(SCL_LookbehindTime), - &mLookbehindTime); + &mLookbehindTime, SCL_LookbehindTime / 10); ctrl->SetMinTextboxWidth(textbox_width); S.AddVariableText(XO("s"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); diff --git a/tests/octave/compressor2_test.m b/tests/octave/compressor2_test.m index 90e714eb1..81c5783eb 100644 --- a/tests/octave/compressor2_test.m +++ b/tests/octave/compressor2_test.m @@ -220,7 +220,7 @@ do_test_equ(settled(y, fs, 0.1), ... 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.00001 ReleaseTime=0.00001 LookaheadTime=0 LookbehindTime=0 KneeWidth=10\n"); +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 From c534e07424e934488f8870368943ef3c379ff763 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Mon, 15 Feb 2021 19:08:59 +0100 Subject: [PATCH 30/33] Replace Compressor2 dynamic makeup gain with fixed output gain. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- scripts/debug/compressor2_trace.m | 8 ++--- src/effects/Compressor2.cpp | 49 ++++++++++++------------------- src/effects/Compressor2.h | 5 +--- tests/octave/compressor2_test.m | 31 ++++++++++--------- 4 files changed, 39 insertions(+), 54 deletions(-) diff --git a/scripts/debug/compressor2_trace.m b/scripts/debug/compressor2_trace.m index 0307a0a9c..f7b2721d8 100644 --- a/scripts/debug/compressor2_trace.m +++ b/scripts/debug/compressor2_trace.m @@ -19,7 +19,7 @@ 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.makeup_gain_pct = raw_data(:,8); +data.output_gain_DB = raw_data(:,8); if stereo data.in = horzcat(raw_data(:,9), raw_data(:,10)); @@ -44,7 +44,7 @@ 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.makeup_gain_pct, 'r'); +plot(data.output_gain_DB, 'r'); plot(data.env.*100, 'k', "linewidth", 2); plot(data.gain.*50, 'k', "linestyle", '--'); hold off; @@ -53,9 +53,9 @@ grid; if stereo legend("in*100", "in*100", "out*100", "out*100", "threshold", "ratio", ... "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ... - "makeup", "env*100", "gain*50"); + "out_gain", "env*100", "gain*50"); else legend("in*100", "out*100", "threshold", "ratio", ... "kneewidth", "attack*10", "release*10", "lookahead", "lookbehind", ... - "makeup", "env*100", "gain*50"); + "out_gain", "env*100", "gain*50"); end diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 5af30a1ae..98a2a65ae 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -89,7 +89,7 @@ Param( AttackTime, double, wxT("AttackTime"), 0.2, 0.0001, 30. 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( MakeupGain, double, wxT("MakeupGain"), 0.0, 0.0, 100.0, 1.0 ); +Param( OutputGain, double, wxT("OutputGain"), 0.0, 0.0, 50.0, 10.0 ); inline int ScaleToPrecision(double scale) { @@ -548,7 +548,7 @@ EffectCompressor2::EffectCompressor2() mReleaseTime = DEF_ReleaseTime; // seconds mLookaheadTime = DEF_LookaheadTime; mLookbehindTime = DEF_LookbehindTime; - mMakeupGainPct = DEF_MakeupGain; + mOutputGainDB = DEF_OutputGain; SetLinearEffectFlag(false); } @@ -682,7 +682,7 @@ bool EffectCompressor2::DefineParams( ShuttleParams & S ) S.SHUTTLE_PARAM(mReleaseTime, ReleaseTime); S.SHUTTLE_PARAM(mLookaheadTime, LookaheadTime); S.SHUTTLE_PARAM(mLookbehindTime, LookbehindTime); - S.SHUTTLE_PARAM(mMakeupGainPct, MakeupGain); + S.SHUTTLE_PARAM(mOutputGainDB, OutputGain); return true; } @@ -700,7 +700,7 @@ bool EffectCompressor2::GetAutomationParameters(CommandParameters & parms) parms.Write(KEY_ReleaseTime, mReleaseTime); parms.Write(KEY_LookaheadTime, mLookaheadTime); parms.Write(KEY_LookbehindTime, mLookbehindTime); - parms.Write(KEY_MakeupGain, mMakeupGainPct); + parms.Write(KEY_OutputGain, mOutputGainDB); return true; } @@ -718,7 +718,7 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms) ReadAndVerifyDouble(ReleaseTime); ReadAndVerifyDouble(LookaheadTime); ReadAndVerifyDouble(LookbehindTime); - ReadAndVerifyDouble(MakeupGain); + ReadAndVerifyDouble(OutputGain); mAlgorithm = Algorithm; mCompressBy = CompressBy; @@ -731,7 +731,7 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms) mReleaseTime = ReleaseTime; mLookaheadTime = LookaheadTime; mLookbehindTime = LookbehindTime; - mMakeupGainPct = MakeupGain; + mOutputGainDB = OutputGain; return true; } @@ -760,7 +760,7 @@ bool EffectCompressor2::Startup() mReleaseTime = DEF_ReleaseTime; // seconds mLookaheadTime = DEF_LookaheadTime; mLookbehindTime = DEF_LookbehindTime; - mMakeupGainPct = DEF_MakeupGain; + mOutputGainDB = DEF_OutputGain; SaveUserPreset(GetCurrentSettingsGroup()); @@ -805,7 +805,6 @@ bool EffectCompressor2::Process() mProcStereo = range.size() > 1; - InitGainCalculation(); mPreproc = InitPreprocessor(mSampleRate); mEnvelope = InitEnvelope(mSampleRate, mPipeline[0].capacity()); @@ -973,16 +972,15 @@ void EffectCompressor2::PopulateOrExchange(ShuttleGui & S) S.AddVariableText(XO("dB"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - /* i18n-hint: Make-up, i.e. correct for any reduction, rather than fabricate it.*/ - S.AddVariableText(XO("Make-up Gain:"), true, + S.AddVariableText(XO("Output Gain:"), true, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); - ctrl = S.Name(XO("Make-up Gain")) + ctrl = S.Name(XO("Output Gain")) .Style(SliderTextCtrl::HORIZONTAL) - .AddSliderTextCtrl({}, DEF_MakeupGain, MAX_MakeupGain, - MIN_MakeupGain, ScaleToPrecision(SCL_MakeupGain), - &mMakeupGainPct); + .AddSliderTextCtrl({}, DEF_OutputGain, MAX_OutputGain, + MIN_OutputGain, ScaleToPrecision(SCL_OutputGain), + &mOutputGainDB); ctrl->SetMinTextboxWidth(textbox_width); - S.AddVariableText(XO("%"), true, + S.AddVariableText(XO("dB"), true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); } S.EndMultiColumn(); @@ -1075,13 +1073,6 @@ bool EffectCompressor2::TransferDataFromWindow() // EffectCompressor2 implementation -void EffectCompressor2::InitGainCalculation() -{ - mMakeupGainDB = mMakeupGainPct / 100.0 * - -(mThresholdDB * (1.0 - 1.0 / mRatio)); - mMakeupGain = DB_TO_LINEAR(mMakeupGainDB); -} - double EffectCompressor2::CompressorGain(double env) { double kneeCond; @@ -1096,13 +1087,13 @@ double EffectCompressor2::CompressorGain(double env) if(kneeCond < -mKneeWidthDB) { // Below threshold: only apply make-up gain - return mMakeupGain; + 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 + mMakeupGainDB - envDB); + (envDB - mThresholdDB) / mRatio + mOutputGainDB - envDB); } else { @@ -1110,7 +1101,7 @@ double EffectCompressor2::CompressorGain(double env) return DB_TO_LINEAR( (1.0 / mRatio - 1.0) * pow(envDB - mThresholdDB + mKneeWidthDB / 2.0, 2) - / (2.0 * mKneeWidthDB) + mMakeupGainDB); + / (2.0 * mKneeWidthDB) + mOutputGainDB); } } @@ -1500,7 +1491,7 @@ inline void EffectCompressor2::CompressSample(float env, size_t wp) float ReleaseTime = mReleaseTime; float LookaheadTime = mLookaheadTime; float LookbehindTime = mLookbehindTime; - float MakeupGainPct = mMakeupGainPct; + float OutputGainDB = mOutputGainDB; debugfile.write((char*)&ThresholdDB, sizeof(float)); debugfile.write((char*)&Ratio, sizeof(float)); @@ -1509,7 +1500,7 @@ inline void EffectCompressor2::CompressSample(float env, size_t wp) debugfile.write((char*)&ReleaseTime, sizeof(float)); debugfile.write((char*)&LookaheadTime, sizeof(float)); debugfile.write((char*)&LookbehindTime, sizeof(float)); - debugfile.write((char*)&MakeupGainPct, 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)); @@ -1641,10 +1632,9 @@ void EffectCompressor2::UpdateCompressorPlot() return; if(!IsInRange(mKneeWidthDB, MIN_KneeWidth, MAX_KneeWidth)) return; - if(!IsInRange(mMakeupGainPct, MIN_MakeupGain, MAX_MakeupGain)) + if(!IsInRange(mOutputGainDB, MIN_OutputGain, MAX_OutputGain)) return; - InitGainCalculation(); size_t xsize = plot->xdata.size(); for(size_t i = 0; i < xsize; ++i) plot->ydata[i] = plot->xdata[i] + @@ -1678,7 +1668,6 @@ void EffectCompressor2::UpdateResponsePlot() lookahead_size -= (lookahead_size > 0); ssize_t block_size = float(TAU_FACTOR) * (mAttackTime + 1.0) * plot_rate; - InitGainCalculation(); preproc = InitPreprocessor(plot_rate, true); envelope = InitEnvelope(plot_rate, block_size, true); diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index de09581e8..f1be14686 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -209,7 +209,6 @@ public: private: // EffectCompressor2 implementation - void InitGainCalculation(); double CompressorGain(double env); std::unique_ptr<SamplePreprocessor> InitPreprocessor( double rate, bool preview = false); @@ -269,11 +268,9 @@ private: double mReleaseTime; double mLookaheadTime; double mLookbehindTime; - double mMakeupGainPct; + double mOutputGainDB; // cached intermediate values - double mMakeupGain; - double mMakeupGainDB; size_t mLookaheadLength; static const size_t RESPONSE_PLOT_SAMPLES = 200; diff --git a/tests/octave/compressor2_test.m b/tests/octave/compressor2_test.m index 81c5783eb..46404c188 100644 --- a/tests/octave/compressor2_test.m +++ b/tests/octave/compressor2_test.m @@ -48,8 +48,7 @@ function y = env_PT1_asym(x, fs, t_a, t_r, gain = 0) end ## Compressor gain helper function -function gain = comp_gain(env, thresh_DB, ratio, kneeW_DB, makeup) - makeupG_DB = -thresh_DB * (1-1/ratio) * makeup / 100; +function gain = comp_gain(env, thresh_DB, ratio, kneeW_DB, outG_DB) env_DB = 20*log10(env); kneeCond_DB = 2*(env_DB-thresh_DB); @@ -59,15 +58,15 @@ function gain = comp_gain(env, thresh_DB, ratio, kneeW_DB, makeup) withinKnee = (kneeCond_DB >= -kneeW_DB) & (kneeCond_DB < kneeW_DB); gain_DB = zeros(size(env)); - gain_DB(belowKnee) = makeupG_DB; + gain_DB(belowKnee) = outG_DB; gain_DB(aboveKnee) = thresh_DB + ... (env_DB(aboveKnee) - thresh_DB) / ratio + ... - makeupG_DB - env_DB(aboveKnee); + 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) + makeupG_DB; + (2*kneeW_DB) + outG_DB; gain = 10.^(gain_DB/20); end @@ -173,7 +172,7 @@ CURRENT_TEST = "Compressor2, mono compression PT1 - sinewave - asymetric attack 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 MakeupGain=0\n"); +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), ... @@ -184,22 +183,22 @@ do_test_equ(settled(y, fs, 1), ... 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 MakeupGain=50\n"); +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, 50).*settled(x, 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 MakeupGain=80\n"); +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, 80) ... + 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 @@ -208,13 +207,13 @@ 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 MakeupGain=50\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, 50).*settled(x, 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"; @@ -287,20 +286,20 @@ do_test_equ(settled(y(:,2), fs, 1), ... 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 MakeupGain=50\n"); +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, 50).*settled(x, 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 MakeupGain=60\n"); +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, 60) ... + 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)); From 06f6aa123dabe785e4f9cb17175d717217b4a9c4 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sun, 28 Feb 2021 10:51:32 +0100 Subject: [PATCH 31/33] Add factory presets to dynamic compressor effect. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 57 +++++++++++++++++++++++++++++++++++++ src/effects/Compressor2.h | 2 ++ 2 files changed, 59 insertions(+) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index 98a2a65ae..e0da5115f 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -91,6 +91,29 @@ Param( LookaheadTime, double, wxT("LookaheadTime"), 0.0, 0.0, 10. 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)); @@ -736,6 +759,40 @@ bool EffectCompressor2::SetAutomationParameters(CommandParameters & parms) 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() diff --git a/src/effects/Compressor2.h b/src/effects/Compressor2.h index f1be14686..c4c1159c9 100644 --- a/src/effects/Compressor2.h +++ b/src/effects/Compressor2.h @@ -197,6 +197,8 @@ public: 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 From 9a22a6921123e46f1aaeee74e92745880f7a9d3e Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Fri, 5 Mar 2021 16:05:15 +0100 Subject: [PATCH 32/33] Disable Dynamic Compressor realtime support for now. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index e0da5115f..bc7eb0742 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -607,7 +607,7 @@ EffectType EffectCompressor2::GetType() bool EffectCompressor2::SupportsRealtime() { #if defined(EXPERIMENTAL_REALTIME_AUDACITY_EFFECTS) - return true; + return false; #else return false; #endif From d7fea9475d0e1df18c03af300ca2aa3c64fc2393 Mon Sep 17 00:00:00 2001 From: Max Maisel <max.maisel@posteo.de> Date: Sun, 16 May 2021 15:15:55 +0200 Subject: [PATCH 33/33] Remove removed header files from new cpp files. Signed-off-by: Max Maisel <max.maisel@posteo.de> --- src/effects/Compressor2.cpp | 2 -- src/widgets/Plot.cpp | 2 -- src/widgets/SliderTextCtrl.cpp | 2 -- 3 files changed, 6 deletions(-) diff --git a/src/effects/Compressor2.cpp b/src/effects/Compressor2.cpp index bc7eb0742..745456c78 100644 --- a/src/effects/Compressor2.cpp +++ b/src/effects/Compressor2.cpp @@ -14,7 +14,6 @@ *//*******************************************************************/ -#include "../Audacity.h" // for rint from configwin.h #include "Compressor2.h" #include <math.h> @@ -24,7 +23,6 @@ #include <wx/valgen.h> #include "../AColor.h" -#include "../Internat.h" #include "../Prefs.h" #include "../ProjectFileManager.h" #include "../Shuttle.h" diff --git a/src/widgets/Plot.cpp b/src/widgets/Plot.cpp index fb1c1bea0..21a33e9c3 100644 --- a/src/widgets/Plot.cpp +++ b/src/widgets/Plot.cpp @@ -14,8 +14,6 @@ *//*******************************************************************/ -#include "../Audacity.h" -#include "audacity/Types.h" #include "Plot.h" #include "Ruler.h" #include "../AColor.h" diff --git a/src/widgets/SliderTextCtrl.cpp b/src/widgets/SliderTextCtrl.cpp index 837152d89..faa1af8ae 100644 --- a/src/widgets/SliderTextCtrl.cpp +++ b/src/widgets/SliderTextCtrl.cpp @@ -14,8 +14,6 @@ *//*******************************************************************/ -#include "../Audacity.h" -#include "audacity/Types.h" #include "SliderTextCtrl.h" #include <wx/defs.h>