/********************************************************************** Audacity: A Digital Audio Editor Loudness.cpp Max Maisel *******************************************************************//** \class EffectLoudness \brief An Effect to bring the loudness level up to a chosen level. *//*******************************************************************/ #include "Loudness.h" #include #include #include #include #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" enum kNormalizeTargets { kLoudness, kRMS, nAlgos }; static const EnumValueSymbol kNormalizeTargetStrings[nAlgos] = { { XO("perceived loudness") }, { XO("RMS") } }; // Define keys, defaults, minimums, and maximums for the effect parameters // // Name Type Key Def Min Max Scale Param( StereoInd, bool, wxT("StereoIndependent"), false, false, true, 1 ); Param( LUFSLevel, double, wxT("LUFSLevel"), -23.0, -145.0, 0.0, 1 ); Param( RMSLevel, double, wxT("RMSLevel"), -20.0, -145.0, 0.0, 1 ); Param( DualMono, bool, wxT("DualMono"), true, false, true, 1 ); Param( NormalizeTo, int, wxT("NormalizeTo"), kLoudness , 0 , nAlgos-1, 1 ); BEGIN_EVENT_TABLE(EffectLoudness, wxEvtHandler) EVT_CHOICE(wxID_ANY, EffectLoudness::OnChoice) EVT_CHECKBOX(wxID_ANY, EffectLoudness::OnUpdateUI) EVT_TEXT(wxID_ANY, EffectLoudness::OnUpdateUI) END_EVENT_TABLE() const ComponentInterfaceSymbol EffectLoudness::Symbol { XO("Loudness Normalization") }; namespace{ BuiltinEffectsModule::Registration< EffectLoudness > reg; } EffectLoudness::EffectLoudness() { mStereoInd = DEF_StereoInd; mLUFSLevel = DEF_LUFSLevel; mRMSLevel = DEF_RMSLevel; mDualMono = DEF_DualMono; mNormalizeTo = DEF_NormalizeTo; SetLinearEffectFlag(false); } EffectLoudness::~EffectLoudness() { } // ComponentInterface implementation ComponentInterfaceSymbol EffectLoudness::GetSymbol() { return Symbol; } TranslatableString EffectLoudness::GetDescription() { return XO("Sets the loudness of one or more tracks"); } ManualPageID EffectLoudness::ManualPage() { return L"Loudness_Normalization"; } // EffectDefinitionInterface implementation EffectType EffectLoudness::GetType() { return EffectTypeProcess; } // EffectClientInterface implementation bool EffectLoudness::DefineParams( ShuttleParams & S ) { S.SHUTTLE_PARAM( mStereoInd, StereoInd ); S.SHUTTLE_PARAM( mLUFSLevel, LUFSLevel ); S.SHUTTLE_PARAM( mRMSLevel, RMSLevel ); S.SHUTTLE_PARAM( mDualMono, DualMono ); S.SHUTTLE_PARAM( mNormalizeTo, NormalizeTo ); return true; } bool EffectLoudness::GetAutomationParameters(CommandParameters & parms) { parms.Write(KEY_StereoInd, mStereoInd); parms.Write(KEY_LUFSLevel, mLUFSLevel); parms.Write(KEY_RMSLevel, mRMSLevel); parms.Write(KEY_DualMono, mDualMono); parms.Write(KEY_NormalizeTo, mNormalizeTo); return true; } bool EffectLoudness::SetAutomationParameters(CommandParameters & parms) { ReadAndVerifyBool(StereoInd); ReadAndVerifyDouble(LUFSLevel); ReadAndVerifyDouble(RMSLevel); ReadAndVerifyBool(DualMono); ReadAndVerifyInt(NormalizeTo); mStereoInd = StereoInd; mLUFSLevel = LUFSLevel; mRMSLevel = RMSLevel; mDualMono = DualMono; mNormalizeTo = NormalizeTo; return true; } // Effect implementation bool EffectLoudness::CheckWhetherSkipEffect() { return false; } bool EffectLoudness::Startup() { wxString base = wxT("/Effects/Loudness/"); // Load the old "current" settings if (gPrefs->Exists(base)) { mStereoInd = false; mDualMono = DEF_DualMono; mNormalizeTo = kLoudness; mLUFSLevel = DEF_LUFSLevel; mRMSLevel = DEF_RMSLevel; SaveUserPreset(GetCurrentSettingsGroup()); gPrefs->Flush(); } return true; } bool EffectLoudness::Process() { if(mNormalizeTo == kLoudness) // LU use 10*log10(...) instead of 20*log10(...) // so multiply level by 2 and use standard DB_TO_LINEAR macro. mRatio = DB_TO_LINEAR(TrapDouble(mLUFSLevel*2, MIN_LUFSLevel, MAX_LUFSLevel)); else // RMS mRatio = DB_TO_LINEAR(TrapDouble(mRMSLevel, MIN_RMSLevel, MAX_RMSLevel)); // Iterate over each track this->CopyInputTracks(); // Set up mOutputTracks. bool bGoodResult = true; auto topMsg = XO("Normalizing Loudness...\n"); AllocBuffers(); mProgressVal = 0; for(auto track : mOutputTracks->Selected() + (mStereoInd ? &Track::Any : &Track::IsLeader)) { // Get start and end times from track // PRL: No accounting for multiple channels ? double trackStart = track->GetStartTime(); double trackEnd = track->GetEndTime(); // Set the current bounds to whichever left marker is // greater and whichever right marker is less: mCurT0 = mT0 < trackStart? trackStart: mT0; mCurT1 = mT1 > trackEnd? trackEnd: mT1; // Get the track rate mCurRate = track->GetRate(); wxString msg; auto trackName = track->GetName(); mSteps = 2; mProgressMsg = topMsg + XO("Analyzing: %s").Format( trackName ); auto range = mStereoInd ? TrackList::SingletonRange(track) : TrackList::Channels(track); mProcStereo = range.size() > 1; if(mNormalizeTo == kLoudness) { mLoudnessProcessor.reset(safenew EBUR128(mCurRate, range.size())); mLoudnessProcessor->Initialize(); if(!ProcessOne(range, true)) { // Processing failed -> abort bGoodResult = false; break; } } else // RMS { size_t idx = 0; for(auto channel : range) { if(!GetTrackRMS(channel, mRMS[idx])) { bGoodResult = false; return false; } ++idx; } mSteps = 1; } // Calculate normalization values the analysis results float extent; if(mNormalizeTo == kLoudness) extent = mLoudnessProcessor->IntegrativeLoudness(); else // RMS { extent = mRMS[0]; if(mProcStereo) // RMS: use average RMS, average must be calculated in quadratic domain. extent = sqrt((mRMS[0] * mRMS[0] + mRMS[1] * mRMS[1]) / 2.0); } if(extent == 0.0) { mLoudnessProcessor.reset(); FreeBuffers(); return false; } mMult = mRatio / extent; if(mNormalizeTo == kLoudness) { // Target half the LUFS value if mono (or independent processed stereo) // shall be treated as dual mono. if(range.size() == 1 && (mDualMono || track->GetChannel() != Track::MonoChannel)) mMult /= 2.0; // LUFS are related to square values so the multiplier must be the root. mMult = sqrt(mMult); } mProgressMsg = topMsg + XO("Processing: %s").Format( trackName ); if(!ProcessOne(range, false)) { // Processing failed -> abort bGoodResult = false; break; } } this->ReplaceProcessedTracks(bGoodResult); mLoudnessProcessor.reset(); FreeBuffers(); return bGoodResult; } void EffectLoudness::PopulateOrExchange(ShuttleGui & S) { S.StartVerticalLay(0); { S.StartMultiColumn(2, wxALIGN_CENTER); { S.StartVerticalLay(false); { S.StartHorizontalLay(wxALIGN_LEFT, false); { S.AddVariableText(XO("&Normalize"), false, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT); mChoice = S .Validator( &mNormalizeTo ) .AddChoice( {}, Msgids(kNormalizeTargetStrings, nAlgos), mNormalizeTo ); S .AddVariableText(XO("t&o"), false, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT); // Use a notebook so we can have two controls but show only one // They target different variables with their validators mBook = S .StartSimplebook(); { S.StartNotebookPage({}); { S.StartHorizontalLay(wxALIGN_LEFT, false); { S /* i18n-hint: LUFS is a particular method for measuring loudnesss */ .Name( XO("Loudness LUFS") ) .Validator>( 2, &mLUFSLevel, NumValidatorStyle::ONE_TRAILING_ZERO, MIN_LUFSLevel, MAX_LUFSLevel ) .AddTextBox( {}, wxT(""), 10); /* i18n-hint: LUFS is a particular method for measuring loudnesss */ S .AddVariableText(XO("LUFS"), false, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT); } S.EndHorizontalLay(); } S.EndNotebookPage(); S.StartNotebookPage({}); { S.StartHorizontalLay(wxALIGN_LEFT, false); { S .Name( XO("RMS dB") ) .Validator>( 2, &mRMSLevel, NumValidatorStyle::ONE_TRAILING_ZERO, MIN_RMSLevel, MAX_RMSLevel ) .AddTextBox( {}, wxT(""), 10); S .AddVariableText(XO("dB"), false, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT); } S.EndHorizontalLay(); } S.EndNotebookPage(); } S.EndSimplebook(); mWarning = S .AddVariableText( {}, false, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT); } S.EndHorizontalLay(); mStereoIndCheckBox = S .Validator( &mStereoInd ) .AddCheckBox(XXO("Normalize &stereo channels independently"), mStereoInd ); mDualMonoCheckBox = S .Validator( &mDualMono ) .AddCheckBox(XXO("&Treat mono as dual-mono (recommended)"), mDualMono ); } S.EndVerticalLay(); } S.EndMultiColumn(); } S.EndVerticalLay(); } bool EffectLoudness::TransferDataToWindow() { if (!mUIParent->TransferDataToWindow()) { return false; } // adjust controls which depend on mchoice wxCommandEvent dummy; OnChoice(dummy); return true; } bool EffectLoudness::TransferDataFromWindow() { if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow()) { return false; } return true; } // EffectLoudness implementation /// Get required buffer size for the largest whole track and allocate buffers. /// This reduces the amount of allocations required. void EffectLoudness::AllocBuffers() { mTrackBufferCapacity = 0; bool stereoTrackFound = false; double maxSampleRate = 0; mProcStereo = false; for(auto track : mOutputTracks->Selected() + &Track::Any) { mTrackBufferCapacity = std::max(mTrackBufferCapacity, track->GetMaxBlockSize()); maxSampleRate = std::max(maxSampleRate, track->GetRate()); // There is a stereo track if(track->IsLeader()) stereoTrackFound = true; } // Initiate a processing buffer. This buffer will (most likely) // be shorter than the length of the track being processed. mTrackBuffer[0].reinit(mTrackBufferCapacity); if(!mStereoInd && stereoTrackFound) mTrackBuffer[1].reinit(mTrackBufferCapacity); } void EffectLoudness::FreeBuffers() { mTrackBuffer[0].reset(); mTrackBuffer[1].reset(); } bool EffectLoudness::GetTrackRMS(WaveTrack* track, float& rms) { // set mRMS. No progress bar here as it's fast. float _rms = track->GetRMS(mCurT0, mCurT1); // may throw rms = _rms; return true; } /// ProcessOne() takes a track, transforms it to bunch of buffer-blocks, /// and executes ProcessData, on it... /// uses mMult to normalize a track. /// mMult must be set before this is called /// In analyse mode, it executes the selected analyse operation on it... /// mMult does not have to be set before this is called bool EffectLoudness::ProcessOne(TrackIterRange range, bool analyse) { WaveTrack* track = *range.begin(); // Transform the marker timepoints to samples auto start = track->TimeToLongSamples(mCurT0); 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 s = start; while(s < end) { // 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 auto blockLen = limitSampleBufferSize( track->GetBestBlockSize(s), mTrackBufferCapacity); const size_t remainingLen = (end - s).as_size_t(); blockLen = blockLen > remainingLen ? remainingLen : blockLen; LoadBufferBlock(range, s, blockLen); // Process the buffer. if(analyse) { if(!AnalyseBufferBlock()) return false; } else { if(!ProcessBufferBlock()) return false; StoreBufferBlock(range, s, blockLen); } // Increment s one blockfull of samples s += blockLen; } // Return true because the effect processing succeeded ... unless cancelled return true; } void EffectLoudness::LoadBufferBlock(TrackIterRange range, sampleCount pos, size_t len) { // Get the samples from the track and put them in the buffer int idx = 0; for(auto channel : range) { channel->GetFloats(mTrackBuffer[idx].get(), pos, len ); ++idx; } mTrackBufferLen = len; } /// Calculates sample sum (for DC) and EBU R128 weighted square sum /// (for loudness). bool EffectLoudness::AnalyseBufferBlock() { for(size_t i = 0; i < mTrackBufferLen; i++) { mLoudnessProcessor->ProcessSampleFromChannel(mTrackBuffer[0][i], 0); if(mProcStereo) mLoudnessProcessor->ProcessSampleFromChannel(mTrackBuffer[1][i], 1); mLoudnessProcessor->NextSample(); } if(!UpdateProgress()) return false; return true; } bool EffectLoudness::ProcessBufferBlock() { for(size_t i = 0; i < mTrackBufferLen; i++) { mTrackBuffer[0][i] = mTrackBuffer[0][i] * mMult; if(mProcStereo) mTrackBuffer[1][i] = mTrackBuffer[1][i] * mMult; } if(!UpdateProgress()) return false; return true; } void EffectLoudness::StoreBufferBlock(TrackIterRange range, sampleCount pos, size_t len) { int idx = 0; for(auto channel : range) { // Copy the newly-changed samples back onto the track. channel->Set((samplePtr) mTrackBuffer[idx].get(), floatSample, pos, len); ++idx; } } bool EffectLoudness::UpdateProgress() { mProgressVal += (double(1+mProcStereo) * double(mTrackBufferLen) / (double(GetNumWaveTracks()) * double(mSteps) * mTrackLen)); return !TotalProgress(mProgressVal, mProgressMsg); } void EffectLoudness::OnChoice(wxCommandEvent & WXUNUSED(evt)) { mChoice->GetValidator()->TransferFromWindow(); mBook->SetSelection( mNormalizeTo ); UpdateUI(); mDualMonoCheckBox->Enable(mNormalizeTo == kLoudness); } void EffectLoudness::OnUpdateUI(wxCommandEvent & WXUNUSED(evt)) { UpdateUI(); } void EffectLoudness::UpdateUI() { if (!mUIParent->TransferDataFromWindow()) { mWarning->SetLabel(_("(Maximum 0dB)")); // TODO: recalculate layout here EnableApply(false); return; } mWarning->SetLabel(wxT("")); EnableApply(true); }