From 9fcb83882a9c36494d4240e21b52d9d4927d4656 Mon Sep 17 00:00:00 2001 From: Max Maisel Date: Tue, 24 Jul 2018 17:08:47 +0200 Subject: [PATCH 1/6] Refactor Biquad in preparation of EBU R128 loudness normalization. --- src/effects/Biquad.cpp | 45 +++++++++++++++--------------- src/effects/Biquad.h | 55 +++++++++++++++++++++---------------- src/effects/ScienFilter.cpp | 9 ++---- src/effects/ScienFilter.h | 2 +- 4 files changed, 56 insertions(+), 55 deletions(-) diff --git a/src/effects/Biquad.cpp b/src/effects/Biquad.cpp index 4a79567a1..a956cd37b 100644 --- a/src/effects/Biquad.cpp +++ b/src/effects/Biquad.cpp @@ -2,31 +2,30 @@ #define square(a) ((a)*(a)) -void Biquad_Process (BiquadStruct* pBQ, int iNumSamples) +Biquad::Biquad() +{ + pfIn = 0; + pfOut = 0; + fNumerCoeffs[B0] = 1; + fNumerCoeffs[B1] = 0; + fNumerCoeffs[B2] = 0; + fDenomCoeffs[A1] = 0; + fDenomCoeffs[A2] = 0; + Reset(); +} + +void Biquad::Reset() +{ + fPrevIn = 0; + fPrevPrevIn = 0; + fPrevOut = 0; + fPrevPrevOut = 0; +} + +void Biquad::Process(int iNumSamples) { - float* pfIn = pBQ->pfIn; - float* pfOut = pBQ->pfOut; - float fPrevIn = pBQ->fPrevIn; - float fPrevPrevIn = pBQ->fPrevPrevIn; - float fPrevOut = pBQ->fPrevOut; - float fPrevPrevOut = pBQ->fPrevPrevOut; for (int i = 0; i < iNumSamples; i++) - { - float fIn = *pfIn++; - *pfOut = fIn * pBQ->fNumerCoeffs [0] + - fPrevIn * pBQ->fNumerCoeffs [1] + - fPrevPrevIn * pBQ->fNumerCoeffs [2] - - fPrevOut * pBQ->fDenomCoeffs [0] - - fPrevPrevOut * pBQ->fDenomCoeffs [1]; - fPrevPrevIn = fPrevIn; - fPrevIn = fIn; - fPrevPrevOut = fPrevOut; - fPrevOut = *pfOut++; - } - pBQ->fPrevIn = fPrevIn; - pBQ->fPrevPrevIn = fPrevPrevIn; - pBQ->fPrevOut = fPrevOut; - pBQ->fPrevPrevOut = fPrevPrevOut; + *pfOut++ = ProcessOne(*pfIn++); } void ComplexDiv (float fNumerR, float fNumerI, float fDenomR, float fDenomI, float* pfQuotientR, float* pfQuotientI) diff --git a/src/effects/Biquad.h b/src/effects/Biquad.h index 0f6ebf235..01aaaa44a 100644 --- a/src/effects/Biquad.h +++ b/src/effects/Biquad.h @@ -1,37 +1,44 @@ #ifndef __BIQUAD_H__ #define __BIQUAD_H__ -#if 0 -//initialisations not supported in MSVC 2013. -//Gives error C2905 -// Do not make conditional on compiler. -typedef struct { - float* pfIn {}; - float* pfOut {}; - float fNumerCoeffs [3] { 1.0f, 0.0f, 0.0f }; // B0 B1 B2 - float fDenomCoeffs [2] { 0.0f, 0.0f }; // A1 A2 - float fPrevIn {}; - float fPrevPrevIn {}; - float fPrevOut {}; - float fPrevPrevOut {}; -} BiquadStruct; -#else -// WARNING: This structure may need initialisation. -typedef struct { +struct Biquad +{ + Biquad(); + void Reset(); + void Process(int iNumSamples); + + enum + { + /// Numerator coefficient indices + B0=0, B1, B2, + /// Denominator coefficient indices + A1=0, A2 + }; + + inline float ProcessOne(float fIn) + { + float fOut = fIn * fNumerCoeffs[B0] + + fPrevIn * fNumerCoeffs[B1] + + fPrevPrevIn * fNumerCoeffs[B2] - + fPrevOut * fDenomCoeffs[A1] - + fPrevPrevOut * fDenomCoeffs[A2]; + fPrevPrevIn = fPrevIn; + fPrevIn = fIn; + fPrevPrevOut = fPrevOut; + fPrevOut = fOut; + return fOut; + } + float* pfIn; float* pfOut; - float fNumerCoeffs [3]; // B0 B1 B2 - float fDenomCoeffs [2]; // A1 A2 + float fNumerCoeffs[3]; // B0 B1 B2 + float fDenomCoeffs[2]; // A1 A2, A0 == 1.0 float fPrevIn; float fPrevPrevIn; float fPrevOut; float fPrevPrevOut; -} BiquadStruct; -#endif +}; - - -void Biquad_Process (BiquadStruct* pBQ, int iNumSamples); void ComplexDiv (float fNumerR, float fNumerI, float fDenomR, float fDenomI, float* pfQuotientR, float* pfQuotientI); bool BilinTransform (float fSX, float fSY, float* pfZX, float* pfZY); float Calc2D_DistSqr (float fX1, float fY1, float fX2, float fY2); diff --git a/src/effects/ScienFilter.cpp b/src/effects/ScienFilter.cpp index bb05ffda5..109fff037 100644 --- a/src/effects/ScienFilter.cpp +++ b/src/effects/ScienFilter.cpp @@ -218,12 +218,7 @@ unsigned EffectScienFilter::GetAudioOutCount() bool EffectScienFilter::ProcessInitialize(sampleCount WXUNUSED(totalLen), ChannelNames WXUNUSED(chanMap)) { for (int iPair = 0; iPair < (mOrder + 1) / 2; iPair++) - { - mpBiquad[iPair].fPrevIn = 0; - mpBiquad[iPair].fPrevPrevIn = 0; - mpBiquad[iPair].fPrevOut = 0; - mpBiquad[iPair].fPrevPrevOut = 0; - } + mpBiquad[iPair].Reset(); return true; } @@ -235,7 +230,7 @@ size_t EffectScienFilter::ProcessBlock(float **inBlock, float **outBlock, size_t { mpBiquad[iPair].pfIn = ibuf; mpBiquad[iPair].pfOut = outBlock[0]; - Biquad_Process(&mpBiquad[iPair], blockLen); + mpBiquad[iPair].Process(blockLen); ibuf = outBlock[0]; } diff --git a/src/effects/ScienFilter.h b/src/effects/ScienFilter.h index 034f4d60a..cadbca6e0 100644 --- a/src/effects/ScienFilter.h +++ b/src/effects/ScienFilter.h @@ -102,7 +102,7 @@ private: int mFilterSubtype; // lowpass, highpass int mOrder; int mOrderIndex; - ArrayOf mpBiquad; + ArrayOf mpBiquad; double mdBMax; double mdBMin; From 473e561f5b4b7cceee6eece3f2bf614076dd31f0 Mon Sep 17 00:00:00 2001 From: Max Maisel Date: Tue, 24 Jul 2018 17:09:52 +0200 Subject: [PATCH 2/6] Add missing headers in Biquad.h/cpp. --- src/effects/Biquad.cpp | 11 +++++++++++ src/effects/Biquad.h | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/effects/Biquad.cpp b/src/effects/Biquad.cpp index a956cd37b..bab5c6915 100644 --- a/src/effects/Biquad.cpp +++ b/src/effects/Biquad.cpp @@ -1,3 +1,14 @@ +/********************************************************************** + +Audacity: A Digital Audio Editor + +EffectScienFilter.h + +Norm C +Max Maisel + +***********************************************************************/ + #include "Biquad.h" #define square(a) ((a)*(a)) diff --git a/src/effects/Biquad.h b/src/effects/Biquad.h index 01aaaa44a..f25d8b9da 100644 --- a/src/effects/Biquad.h +++ b/src/effects/Biquad.h @@ -1,3 +1,14 @@ +/********************************************************************** + +Audacity: A Digital Audio Editor + +EffectScienFilter.h + +Norm C +Max Maisel + +***********************************************************************/ + #ifndef __BIQUAD_H__ #define __BIQUAD_H__ From d707f6818972e9791c1f32ab32c1dcae1983fede Mon Sep 17 00:00:00 2001 From: Max Maisel Date: Tue, 24 Jul 2018 17:10:52 +0200 Subject: [PATCH 3/6] Expose "msg" parameter in Effect::TotalProgress(). --- src/effects/Effect.cpp | 4 ++-- src/effects/Effect.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/effects/Effect.cpp b/src/effects/Effect.cpp index d5c60d413..d0af0ac52 100644 --- a/src/effects/Effect.cpp +++ b/src/effects/Effect.cpp @@ -1974,10 +1974,10 @@ void Effect::IncludeNotSelectedPreviewTracks(bool includeNotSelected) mPreviewWithNotSelected = includeNotSelected; } -bool Effect::TotalProgress(double frac) +bool Effect::TotalProgress(double frac, const wxString &msg) { auto updateResult = (mProgress ? - mProgress->Update(frac) : + mProgress->Update(frac, msg) : ProgressResult::Success); return (updateResult != ProgressResult::Success); } diff --git a/src/effects/Effect.h b/src/effects/Effect.h index ab3d61154..de3ddbd02 100644 --- a/src/effects/Effect.h +++ b/src/effects/Effect.h @@ -330,7 +330,7 @@ protected: // is okay, but don't try to undo). // Pass a fraction between 0.0 and 1.0 - bool TotalProgress(double frac); + bool TotalProgress(double frac, const wxString & = wxEmptyString); // Pass a fraction between 0.0 and 1.0, for the current track // (when doing one track at a time) From 6655f6e38a92466144ffd854bb0a46f58f69cdee Mon Sep 17 00:00:00 2001 From: Max Maisel Date: Tue, 24 Jul 2018 17:20:52 +0200 Subject: [PATCH 4/6] Fix normalize progress bar in case of stereo tracks. Before this commit, progress jumped from 0.25 to 0.5, then from 0.75 to 0.25 and finally from 0.5 to 0.75 when normalizing a dependent stereo track. --- src/effects/Normalize.cpp | 35 +++++++++++++++++++---------------- src/effects/Normalize.h | 6 +++--- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/effects/Normalize.cpp b/src/effects/Normalize.cpp index 1558964ce..d745279b0 100644 --- a/src/effects/Normalize.cpp +++ b/src/effects/Normalize.cpp @@ -175,7 +175,7 @@ bool EffectNormalize::Process() WaveTrack *track = (WaveTrack *) iter.First(); WaveTrack *prevTrack; prevTrack = track; - int curTrackNum = 0; + double progress = 0; wxString topMsg; if(mDC && mGain) topMsg = _("Removing DC offset and Normalizing...\n"); @@ -208,7 +208,7 @@ bool EffectNormalize::Process() msg = topMsg + wxString::Format( _("Analyzing first track of stereo pair: %s"), trackName ); float offset, min, max; - bGoodResult = AnalyseTrack(track, msg, curTrackNum, offset, min, max); + bGoodResult = AnalyseTrack(track, msg, progress, offset, min, max); if (!bGoodResult ) break; if(!track->GetLinked() || mStereoInd) { @@ -224,7 +224,7 @@ bool EffectNormalize::Process() msg = topMsg + wxString::Format( _("Processing stereo channels independently: %s"), trackName ); - if (!ProcessOne(track, msg, curTrackNum, offset)) + if (!ProcessOne(track, msg, progress, offset)) { bGoodResult = false; break; @@ -239,7 +239,7 @@ bool EffectNormalize::Process() msg = topMsg + wxString::Format( _("Analyzing second track of stereo pair: %s"), trackName ); float offset2, min2, max2; - bGoodResult = AnalyseTrack(track, msg, curTrackNum + 1, offset2, min2, max2); + bGoodResult = AnalyseTrack(track, msg, progress, offset2, min2, max2); if ( !bGoodResult ) break; float extent = wxMax(fabs(min), fabs(max)); @@ -252,16 +252,15 @@ bool EffectNormalize::Process() track = (WaveTrack *) iter.Prev(); // go back to the first linked one msg = topMsg + wxString::Format( _("Processing first track of stereo pair: %s"), trackName ); - if (!ProcessOne(track, msg, curTrackNum, offset)) + if (!ProcessOne(track, msg, progress, offset)) { bGoodResult = false; break; } track = (WaveTrack *) iter.Next(); // go to the second linked one - curTrackNum++; // keeps progress bar correct msg = topMsg + wxString::Format( _("Processing second track of stereo pair: %s"), trackName ); - if (!ProcessOne(track, msg, curTrackNum, offset2)) + if (!ProcessOne(track, msg, progress, offset2)) { bGoodResult = false; break; @@ -272,7 +271,6 @@ bool EffectNormalize::Process() //Iterate to the next track prevTrack = track; track = (WaveTrack *) iter.Next(); - curTrackNum++; } this->ReplaceProcessedTracks(bGoodResult); @@ -350,7 +348,7 @@ bool EffectNormalize::TransferDataFromWindow() // EffectNormalize implementation bool EffectNormalize::AnalyseTrack(const WaveTrack * track, const wxString &msg, - int curTrackNum, + double &progress, float &offset, float &min, float &max) { if(mGain) { @@ -373,7 +371,7 @@ bool EffectNormalize::AnalyseTrack(const WaveTrack * track, const wxString &msg, } if(mDC) { - auto rc = AnalyseDC(track, msg, curTrackNum, offset); + auto rc = AnalyseDC(track, msg, progress, offset); min += offset; max += offset; return rc; @@ -386,7 +384,7 @@ bool EffectNormalize::AnalyseTrack(const WaveTrack * track, const wxString &msg, //AnalyseDC() takes a track, transforms it to bunch of buffer-blocks, //and executes AnalyzeData on it... bool EffectNormalize::AnalyseDC(const WaveTrack * track, const wxString &msg, - int curTrackNum, + double &progress, float &offset) { bool rc = true; @@ -394,7 +392,10 @@ bool EffectNormalize::AnalyseDC(const WaveTrack * track, const wxString &msg, offset = 0.0; // we might just return if(!mDC) // don't do analysis if not doing dc removal + { + progress += 1.0/double(2*GetNumWaveTracks()); return(rc); + } //Transform the marker timepoints to samples auto start = track->TimeToLongSamples(mCurT0); @@ -437,8 +438,8 @@ bool EffectNormalize::AnalyseDC(const WaveTrack * track, const wxString &msg, s += block; //Update the Progress meter - if (TrackProgress(curTrackNum, - ((s - start).as_double() / len)/2.0, msg)) { + if (TotalProgress(progress + + ((s - start).as_double() / len)/double(2*GetNumWaveTracks()), msg)) { rc = false; //lda .. break, not return, so that buffer is deleted break; } @@ -448,6 +449,7 @@ bool EffectNormalize::AnalyseDC(const WaveTrack * track, const wxString &msg, else offset = 0.0; + progress += 1.0/double(2*GetNumWaveTracks()); //Return true because the effect processing succeeded ... unless cancelled return rc; } @@ -457,7 +459,7 @@ bool EffectNormalize::AnalyseDC(const WaveTrack * track, const wxString &msg, // uses mMult and offset to normalize a track. // mMult must be set before this is called bool EffectNormalize::ProcessOne( - WaveTrack * track, const wxString &msg, int curTrackNum, float offset) + WaveTrack * track, const wxString &msg, double &progress, float offset) { bool rc = true; @@ -498,12 +500,13 @@ bool EffectNormalize::ProcessOne( s += block; //Update the Progress meter - if (TrackProgress(curTrackNum, - 0.5+((s - start).as_double() / len)/2.0, msg)) { + if (TotalProgress(progress + + ((s - start).as_double() / len)/double(2*GetNumWaveTracks()), msg)) { rc = false; //lda .. break, not return, so that buffer is deleted break; } } + progress += 1.0/double(2*GetNumWaveTracks()); //Return true because the effect processing succeeded ... unless cancelled return rc; diff --git a/src/effects/Normalize.h b/src/effects/Normalize.h index d0bd8b36e..cf4a90723 100644 --- a/src/effects/Normalize.h +++ b/src/effects/Normalize.h @@ -59,12 +59,12 @@ private: // EffectNormalize implementation bool ProcessOne( - WaveTrack * t, const wxString &msg, int curTrackNum, float offset); + WaveTrack * t, const wxString &msg, double& progress, float offset); bool AnalyseTrack(const WaveTrack * track, const wxString &msg, - int curTrackNum, + double &progress, float &offset, float &min, float &max); void AnalyzeData(float *buffer, size_t len); - bool AnalyseDC(const WaveTrack * track, const wxString &msg, int curTrackNum, + bool AnalyseDC(const WaveTrack * track, const wxString &msg, double &progress, float &offset); void ProcessData(float *buffer, size_t len, float offset); From d16b4526ce2e581a81326cbe9ede523a1b15013f Mon Sep 17 00:00:00 2001 From: Max Maisel Date: Tue, 24 Jul 2018 17:29:51 +0200 Subject: [PATCH 5/6] Refactor Normalize extent calculation in preparation of EBU R128 loudness normalization. --- src/effects/Normalize.cpp | 26 +++++++++++++------------- src/effects/Normalize.h | 3 +-- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/effects/Normalize.cpp b/src/effects/Normalize.cpp index d745279b0..26603d52f 100644 --- a/src/effects/Normalize.cpp +++ b/src/effects/Normalize.cpp @@ -207,13 +207,12 @@ bool EffectNormalize::Process() else msg = topMsg + wxString::Format( _("Analyzing first track of stereo pair: %s"), trackName ); - float offset, min, max; - bGoodResult = AnalyseTrack(track, msg, progress, offset, min, max); + float offset, extent; + bGoodResult = AnalyseTrack(track, msg, progress, offset, extent); if (!bGoodResult ) break; if(!track->GetLinked() || mStereoInd) { // mono or 'stereo tracks independently' - float extent = wxMax(fabs(max), fabs(min)); if( (extent > 0) && mGain ) mMult = ratio / extent; else @@ -238,13 +237,11 @@ bool EffectNormalize::Process() track = (WaveTrack *) iter.Next(); // get the next one msg = topMsg + wxString::Format( _("Analyzing second track of stereo pair: %s"), trackName ); - float offset2, min2, max2; - bGoodResult = AnalyseTrack(track, msg, progress, offset2, min2, max2); + float offset2, extent2; + bGoodResult = AnalyseTrack(track, msg, progress, offset2, extent2); if ( !bGoodResult ) break; - float extent = wxMax(fabs(min), fabs(max)); - extent = wxMax(extent, fabs(min2)); - extent = wxMax(extent, fabs(max2)); + extent = fmax(extent, extent2); if( (extent > 0) && mGain ) mMult = ratio / extent; // we need to use this for both linked tracks else @@ -348,9 +345,11 @@ bool EffectNormalize::TransferDataFromWindow() // EffectNormalize implementation bool EffectNormalize::AnalyseTrack(const WaveTrack * track, const wxString &msg, - double &progress, - float &offset, float &min, float &max) + double &progress, float &offset, float &extent) { + bool result = true; + float min, max; + if(mGain) { // Since we need complete summary data, we need to block until the OD tasks are done for this track // TODO: should we restrict the flags to just the relevant block files (for selections) @@ -371,14 +370,15 @@ bool EffectNormalize::AnalyseTrack(const WaveTrack * track, const wxString &msg, } if(mDC) { - auto rc = AnalyseDC(track, msg, progress, offset); + result = AnalyseDC(track, msg, progress, offset); min += offset; max += offset; - return rc; } else { offset = 0.0; - return true; } + + extent = fmax(fabs(min), fabs(max)); + return result; } //AnalyseDC() takes a track, transforms it to bunch of buffer-blocks, diff --git a/src/effects/Normalize.h b/src/effects/Normalize.h index cf4a90723..bd250e067 100644 --- a/src/effects/Normalize.h +++ b/src/effects/Normalize.h @@ -61,8 +61,7 @@ private: bool ProcessOne( WaveTrack * t, const wxString &msg, double& progress, float offset); bool AnalyseTrack(const WaveTrack * track, const wxString &msg, - double &progress, - float &offset, float &min, float &max); + double &progress, float &offset, float &extent); void AnalyzeData(float *buffer, size_t len); bool AnalyseDC(const WaveTrack * track, const wxString &msg, double &progress, float &offset); From ed41183536013fa572bcec784d4a162570778879 Mon Sep 17 00:00:00 2001 From: Max Maisel Date: Tue, 24 Jul 2018 17:35:18 +0200 Subject: [PATCH 6/6] Implement EBU R128 loudness normalization. Please note that normalizing to large LUFS values may introduce clipping of large peaks. However, the recommended target value in EBU R128 of -23 LUFS should be totally safe but only uses a small amount of the possible quantization range. For tracks with normal dynamic range, normalizing to about -10 LUFS should be possible without clipping. --- src/effects/Normalize.cpp | 226 ++++++++++++++++++++++++++++++-------- src/effects/Normalize.h | 23 +++- 2 files changed, 200 insertions(+), 49 deletions(-) diff --git a/src/effects/Normalize.cpp b/src/effects/Normalize.cpp index 26603d52f..24af23323 100644 --- a/src/effects/Normalize.cpp +++ b/src/effects/Normalize.cpp @@ -6,6 +6,7 @@ Dominic Mazzoni Vaughan Johnson (Preview) + Max Maisel (Loudness) *******************************************************************//** @@ -31,11 +32,12 @@ // Define keys, defaults, minimums, and maximums for the effect parameters // -// Name Type Key Def Min Max Scale -Param( Level, double, wxT("Level"), -1.0, -145.0, 0.0, 1 ); -Param( RemoveDC, bool, wxT("RemoveDcOffset"), true, false, true, 1 ); -Param( ApplyGain, bool, wxT("ApplyGain"), true, false, true, 1 ); -Param( StereoInd, bool, wxT("StereoIndependent"), false, false, true, 1 ); +// Name Type Key Def Min Max Scale +Param( Level, double, wxT("Level"), -23.0, -145.0, 0.0, 1 ); +Param( RemoveDC, bool, wxT("RemoveDcOffset"), true, false, true, 1 ); +Param( ApplyGain, bool, wxT("ApplyGain"), true, false, true, 1 ); +Param( StereoInd, bool, wxT("StereoIndependent"), false, false, true, 1 ); +Param( UseLoudness, bool, wxT("Use Loudness"), true, false, true, 1 ); BEGIN_EVENT_TABLE(EffectNormalize, wxEvtHandler) EVT_CHECKBOX(wxID_ANY, EffectNormalize::OnUpdateUI) @@ -48,6 +50,7 @@ EffectNormalize::EffectNormalize() mDC = DEF_RemoveDC; mGain = DEF_ApplyGain; mStereoInd = DEF_StereoInd; + mUseLoudness = DEF_UseLoudness; SetLinearEffectFlag(false); } @@ -65,7 +68,7 @@ IdentInterfaceSymbol EffectNormalize::GetSymbol() wxString EffectNormalize::GetDescription() { - return _("Sets the peak amplitude of one or more tracks"); + return _("Sets the peak amplitude or loudness of one or more tracks"); } wxString EffectNormalize::ManualPage() @@ -86,6 +89,7 @@ bool EffectNormalize::DefineParams( ShuttleParams & S ){ S.SHUTTLE_PARAM( mGain, ApplyGain ); S.SHUTTLE_PARAM( mDC, RemoveDC ); S.SHUTTLE_PARAM( mStereoInd, StereoInd ); + S.SHUTTLE_PARAM( mUseLoudness, UseLoudness ); return true; } @@ -95,6 +99,7 @@ bool EffectNormalize::GetAutomationParameters(CommandParameters & parms) parms.Write(KEY_ApplyGain, mGain); parms.Write(KEY_RemoveDC, mDC); parms.Write(KEY_StereoInd, mStereoInd); + parms.Write(KEY_UseLoudness, mUseLoudness); return true; } @@ -105,11 +110,13 @@ bool EffectNormalize::SetAutomationParameters(CommandParameters & parms) ReadAndVerifyBool(ApplyGain); ReadAndVerifyBool(RemoveDC); ReadAndVerifyBool(StereoInd); + ReadAndVerifyBool(UseLoudness); mLevel = Level; mGain = ApplyGain; mDC = RemoveDC; mStereoInd = StereoInd; + mUseLoudness = UseLoudness; return true; } @@ -145,6 +152,7 @@ bool EffectNormalize::Startup() mLevel = -mLevel; boolProxy = gPrefs->Read(base + wxT("StereoIndependent"), 0L); mStereoInd = (boolProxy == 1); + mUseLoudness = false; SaveUserPreset(GetCurrentSettingsGroup()); @@ -163,8 +171,15 @@ bool EffectNormalize::Process() float ratio; if( mGain ) - // same value used for all tracks - ratio = DB_TO_LINEAR(TrapDouble(mLevel, MIN_Level, MAX_Level)); + { + if(mUseLoudness) + // LU use 10*log10(...) instead of 20*log10(...) + // so multiply level by 2 and use standard DB_TO_LINEAR macro. + ratio = DB_TO_LINEAR(TrapDouble(mLevel*2, MIN_Level, MAX_Level)); + else + // same value used for all tracks + ratio = DB_TO_LINEAR(TrapDouble(mLevel, MIN_Level, MAX_Level)); + } else ratio = 1.0; @@ -234,14 +249,23 @@ bool EffectNormalize::Process() // we have a linked stereo track // so we need to find it's min, max and offset // as they are needed to calc the multiplier for both tracks + track = (WaveTrack *) iter.Next(); // get the next one msg = topMsg + wxString::Format( _("Analyzing second track of stereo pair: %s"), trackName ); + float offset2, extent2; bGoodResult = AnalyseTrack(track, msg, progress, offset2, extent2); if ( !bGoodResult ) break; - extent = fmax(extent, extent2); + + if (mUseLoudness) + // Loudness: use mean of both tracks. + extent = (extent + extent2) / 2; + else + // Peak: use maximum of both tracks. + extent = fmax(extent, extent2); + if( (extent > 0) && mGain ) mMult = ratio / extent; // we need to use this for both linked tracks else @@ -288,7 +312,7 @@ void EffectNormalize::PopulateOrExchange(ShuttleGui & S) mDC ? wxT("true") : wxT("false")); mDCCheckBox->SetValidator(wxGenericValidator(&mDC)); - S.StartHorizontalLay(wxALIGN_CENTER, false); + S.StartHorizontalLay(wxALIGN_LEFT, false); { mGainCheckBox = S.AddCheckBox(_("Normalize maximum amplitude to"), mGain ? wxT("true") : wxT("false")); @@ -306,7 +330,10 @@ void EffectNormalize::PopulateOrExchange(ShuttleGui & S) } S.EndHorizontalLay(); - + mUseLoudnessCheckBox = S.AddCheckBox(_("Use integrative loudness instead of maximum amplitude"), + mUseLoudness ? wxT("true") : wxT("false")); + mUseLoudnessCheckBox->SetValidator(wxGenericValidator(&mUseLoudness)); + mStereoIndCheckBox = S.AddCheckBox(_("Normalize stereo channels independently"), mStereoInd ? wxT("true") : wxT("false")); mStereoIndCheckBox->SetValidator(wxGenericValidator(&mStereoInd)); @@ -350,53 +377,75 @@ bool EffectNormalize::AnalyseTrack(const WaveTrack * track, const wxString &msg, bool result = true; float min, max; - if(mGain) { - // Since we need complete summary data, we need to block until the OD tasks are done for this track - // TODO: should we restrict the flags to just the relevant block files (for selections) - while (track->GetODFlags()) { - // update the gui - if (ProgressResult::Cancelled == mProgress->Update( - 0, _("Waiting for waveform to finish computing...")) ) - return false; - wxMilliSleep(100); + if(mGain) + { + if(mUseLoudness) + { + CalcEBUR128HPF(track->GetRate()); + CalcEBUR128HSF(track->GetRate()); + if(mDC) + { + result = AnalyseTrackData(track, msg, progress, ANALYSE_LOUDNESS_DC, offset); + } + else + { + result = AnalyseTrackData(track, msg, progress, ANALYSE_LOUDNESS, offset); + offset = 0.0; + } + + extent = sqrt(mSqSum / mCount.as_double()); } + else + { + // Since we need complete summary data, we need to block until the OD tasks are done for this track + // This is needed for track->GetMinMax + // TODO: should we restrict the flags to just the relevant block files (for selections) + while (track->GetODFlags()) { + // update the gui + if (ProgressResult::Cancelled == mProgress->Update( + 0, _("Waiting for waveform to finish computing...")) ) + return false; + wxMilliSleep(100); + } - // set mMin, mMax. No progress bar here as it's fast. - auto pair = track->GetMinMax(mCurT0, mCurT1); // may throw - min = pair.first, max = pair.second; + // set mMin, mMax. No progress bar here as it's fast. + auto pair = track->GetMinMax(mCurT0, mCurT1); // may throw + min = pair.first, max = pair.second; - } else { - min = -1.0, max = 1.0; // sensible defaults? + if(mDC) + { + min = -1.0, max = 1.0; // sensible defaults? + result = AnalyseTrackData(track, msg, progress, ANALYSE_DC, offset); + min += offset; + max += offset; + } + } } - - if(mDC) { - result = AnalyseDC(track, msg, progress, offset); + else if(mDC) + { + min = -1.0, max = 1.0; // sensible defaults? + result = AnalyseTrackData(track, msg, progress, ANALYSE_DC, offset); min += offset; max += offset; - } else { + } + else + { + min = -1.0, max = 1.0; // sensible defaults? offset = 0.0; } - extent = fmax(fabs(min), fabs(max)); + if(!mUseLoudness) + extent = fmax(fabs(min), fabs(max)); return result; } -//AnalyseDC() takes a track, transforms it to bunch of buffer-blocks, -//and executes AnalyzeData on it... -bool EffectNormalize::AnalyseDC(const WaveTrack * track, const wxString &msg, - double &progress, - float &offset) +//AnalyseTrackData() takes a track, transforms it to bunch of buffer-blocks, +//and executes selected AnalyseOperation on it... +bool EffectNormalize::AnalyseTrackData(const WaveTrack * track, const wxString &msg, + double &progress, AnalyseOperation op, float &offset) { bool rc = true; - offset = 0.0; // we might just return - - if(!mDC) // don't do analysis if not doing dc removal - { - progress += 1.0/double(2*GetNumWaveTracks()); - return(rc); - } - //Transform the marker timepoints to samples auto start = track->TimeToLongSamples(mCurT0); auto end = track->TimeToLongSamples(mCurT1); @@ -410,7 +459,8 @@ bool EffectNormalize::AnalyseDC(const WaveTrack * track, const wxString &msg, //be shorter than the length of the track being processed. Floats buffer{ track->GetMaxBlockSize() }; - mSum = 0.0; // dc offset inits + mSum = 0.0; // dc offset inits + mSqSum = 0.0; // rms init mCount = 0; sampleCount blockSamples; @@ -432,7 +482,12 @@ bool EffectNormalize::AnalyseDC(const WaveTrack * track, const wxString &msg, totalSamples += blockSamples; //Process the buffer. - AnalyzeData(buffer.get(), block); + if(op == ANALYSE_DC) + AnalyseDataDC(buffer.get(), block); + else if(op == ANALYSE_LOUDNESS) + AnalyseDataLoudness(buffer.get(), block); + else if(op == ANALYSE_LOUDNESS_DC) + AnalyseDataLoudnessDC(buffer.get(), block); //Increment s one blockfull of samples s += block; @@ -512,13 +567,46 @@ bool EffectNormalize::ProcessOne( return rc; } -void EffectNormalize::AnalyzeData(float *buffer, size_t len) +/// @see AnalyseDataLoudnessDC +void EffectNormalize::AnalyseDataDC(float *buffer, size_t len) { for(decltype(len) i = 0; i < len; i++) mSum += (double)buffer[i]; mCount += len; } +/// @see AnalyseDataLoudnessDC +void EffectNormalize::AnalyseDataLoudness(float *buffer, size_t len) +{ + float value; + for(decltype(len) i = 0; i < len; i++) + { + value = mR128HSF.ProcessOne(buffer[i]); + value = mR128HPF.ProcessOne(value); + mSqSum += ((double)value) * ((double)value); + } + mCount += len; +} + +/// Calculates sample sum (for DC) and EBU R128 weighted square sum +/// (for loudness). This function has variants which only calculate +/// sum or square sum for performance improvements if only one of those +/// values is required. +/// @see AnalyseDataLoudness +/// @see AnalyseDataDC +void EffectNormalize::AnalyseDataLoudnessDC(float *buffer, size_t len) +{ + float value; + for(decltype(len) i = 0; i < len; i++) + { + mSum += (double)buffer[i]; + value = mR128HSF.ProcessOne(buffer[i]); + value = mR128HPF.ProcessOne(value); + mSqSum += ((double)value) * ((double)value); + } + mCount += len; +} + void EffectNormalize::ProcessData(float *buffer, size_t len, float offset) { for(decltype(len) i = 0; i < len; i++) { @@ -527,6 +615,46 @@ void EffectNormalize::ProcessData(float *buffer, size_t len, float offset) } } +// after Juha, https://hydrogenaud.io/index.php/topic,76394.0.html +void EffectNormalize::CalcEBUR128HPF(float fs) +{ + double f0 = 38.13547087602444; + double Q = 0.5003270373238773; + double K = tan(M_PI * f0 / fs); + + mR128HPF.Reset(); + + mR128HPF.fNumerCoeffs[Biquad::B0] = 1.0; + mR128HPF.fNumerCoeffs[Biquad::B1] = -2.0; + mR128HPF.fNumerCoeffs[Biquad::B2] = 1.0; + + mR128HPF.fDenomCoeffs[Biquad::A1] = 2.0 * (K * K - 1.0) / (1.0 + K / Q + K * K); + mR128HPF.fDenomCoeffs[Biquad::A2] = (1.0 - K / Q + K * K) / (1.0 + K / Q + K * K); +} + +// after Juha, https://hydrogenaud.io/index.php/topic,76394.0.html +void EffectNormalize::CalcEBUR128HSF(float fs) +{ + double db = 3.999843853973347; + double f0 = 1681.974450955533; + double Q = 0.7071752369554196; + double K = tan(M_PI * f0 / fs); + + double Vh = pow(10.0, db / 20.0); + double Vb = pow(Vh, 0.4996667741545416); + + double a0 = 1.0 + K / Q + K * K; + + mR128HSF.Reset(); + + mR128HSF.fNumerCoeffs[Biquad::B0] = (Vh + Vb * K / Q + K * K) / a0; + mR128HSF.fNumerCoeffs[Biquad::B1] = 2.0 * (K * K - Vh) / a0; + mR128HSF.fNumerCoeffs[Biquad::B2] = (Vh - Vb * K / Q + K * K) / a0; + + mR128HSF.fDenomCoeffs[Biquad::A1] = 2.0 * (K * K - 1.0) / a0; + mR128HSF.fDenomCoeffs[Biquad::A2] = (1.0 - K / Q + K * K) / a0; +} + void EffectNormalize::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) { UpdateUI(); @@ -542,10 +670,16 @@ void EffectNormalize::UpdateUI() } mWarning->SetLabel(wxT("")); + if (mUseLoudness) + mLeveldB->SetLabel(_("LUFS")); + else + mLeveldB->SetLabel(_("dB")); + // Disallow level stuff if not normalizing mLevelTextCtrl->Enable(mGain); mLeveldB->Enable(mGain); mStereoIndCheckBox->Enable(mGain); + mUseLoudnessCheckBox->Enable(mGain); // Disallow OK/Preview if doing nothing EnableApply(mGain || mDC); diff --git a/src/effects/Normalize.h b/src/effects/Normalize.h index bd250e067..eb0a4afe9 100644 --- a/src/effects/Normalize.h +++ b/src/effects/Normalize.h @@ -6,6 +6,7 @@ Dominic Mazzoni Vaughan Johnson (Preview) + Max Maisel (Loudness) **********************************************************************/ @@ -19,6 +20,7 @@ #include #include "Effect.h" +#include "Biquad.h" class ShuttleGui; @@ -58,15 +60,25 @@ public: private: // EffectNormalize implementation + enum AnalyseOperation + { + ANALYSE_DC, ANALYSE_LOUDNESS, ANALYSE_LOUDNESS_DC + }; + bool ProcessOne( WaveTrack * t, const wxString &msg, double& progress, float offset); bool AnalyseTrack(const WaveTrack * track, const wxString &msg, double &progress, float &offset, float &extent); - void AnalyzeData(float *buffer, size_t len); - bool AnalyseDC(const WaveTrack * track, const wxString &msg, double &progress, - float &offset); + bool AnalyseTrackData(const WaveTrack * track, const wxString &msg, double &progress, + AnalyseOperation op, float &offset); + void AnalyseDataDC(float *buffer, size_t len); + void AnalyseDataLoudness(float *buffer, size_t len); + void AnalyseDataLoudnessDC(float *buffer, size_t len); void ProcessData(float *buffer, size_t len, float offset); + void CalcEBUR128HPF(float fs); + void CalcEBUR128HSF(float fs); + void OnUpdateUI(wxCommandEvent & evt); void UpdateUI(); @@ -75,11 +87,13 @@ private: bool mGain; bool mDC; bool mStereoInd; + bool mUseLoudness; double mCurT0; double mCurT1; float mMult; double mSum; + double mSqSum; sampleCount mCount; wxCheckBox *mGainCheckBox; @@ -87,9 +101,12 @@ private: wxTextCtrl *mLevelTextCtrl; wxStaticText *mLeveldB; wxStaticText *mWarning; + wxCheckBox *mUseLoudnessCheckBox; wxCheckBox *mStereoIndCheckBox; bool mCreating; + Biquad mR128HSF; + Biquad mR128HPF; DECLARE_EVENT_TABLE() };