audacia/src/effects/NoiseReduction.cpp
Paul Licameli bb26b2f2c4 Require a ProjectWindow as ancestor of effect dialog...
... by passing parent as reference, not pointer, and testing in the dialog
factory function.

This is important so that we know the lifetime of an effect dialog, even when
it is non-modal, is bounded by the lifetime of the associated project.
2020-01-06 11:30:47 -05:00

1918 lines
59 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
NoiseReduction.cpp
Dominic Mazzoni
detailed rewriting by
Paul Licameli
*******************************************************************//**
\class EffectNoiseReduction
\brief A two-pass effect to reduce background noise.
The first pass is done over just noise. For each windowed sample
of the sound, we take a FFT and then statistics are tabulated for
each frequency band.
During the noise reduction phase, we start by setting a gain control
for each frequency band such that if the sound has exceeded the
previously-determined threshold, the gain is set to 0 dB, otherwise
the gain is set lower (e.g. -18 dB), to suppress the noise.
Then time-smoothing is applied so that the gain for each frequency
band moves slowly, and then frequency-smoothing is applied so that a
single frequency is never suppressed or boosted in isolation.
Lookahead is employed; this effect is not designed for real-time
but if it were, there would be a significant delay.
The gain controls are applied to the complex FFT of the signal,
and then the inverse FFT is applied. A Hann window may be
applied (depending on the advanced window types setting), and then
the output signal is then pieced together using overlap/add.
*//****************************************************************//**
*/
#include "../Audacity.h"
#include "NoiseReduction.h"
#include "../Experimental.h"
#include "EffectManager.h"
#include "../ShuttleGui.h"
#include "../widgets/HelpSystem.h"
#include "../Prefs.h"
#include "../RealFFTf.h"
#include "../WaveTrack.h"
#include "../widgets/AudacityMessageBox.h"
#include "../widgets/valnum.h"
#include <algorithm>
#include <vector>
#include <math.h>
#if defined(__WXMSW__) && !defined(__CYGWIN__)
#include <float.h>
#define finite(x) _finite(x)
#endif
#include <wx/button.h>
#include <wx/choice.h>
#include <wx/dialog.h>
#include <wx/radiobut.h>
#include <wx/slider.h>
#include <wx/valtext.h>
#include <wx/textctrl.h>
#include <wx/sizer.h>
// SPECTRAL_SELECTION not to affect this effect for now, as there might be no indication that it does.
// [Discussed and agreed for v2.1 by Steve, Paul, Bill].
#undef EXPERIMENTAL_SPECTRAL_EDITING
typedef std::vector<float> FloatVector;
// Define both of these to make the radio button three-way
#define RESIDUE_CHOICE
//#define ISOLATE_CHOICE
// Define for Attack and release controls.
// #define ATTACK_AND_RELEASE
// Define to expose other advanced, experimental dialog controls
//#define ADVANCED_SETTINGS
// Define to make the old statistical methods an available choice
//#define OLD_METHOD_AVAILABLE
namespace {
enum DiscriminationMethod {
DM_MEDIAN,
DM_SECOND_GREATEST,
DM_OLD_METHOD,
DM_N_METHODS,
DM_DEFAULT_METHOD = DM_SECOND_GREATEST,
};
const struct DiscriminationMethodInfo {
const TranslatableString name;
} discriminationMethodInfo[DM_N_METHODS] = {
// Experimental only, don't need translations
{ XO("Median") },
{ XO("Second greatest") },
{ XO("Old") },
};
// magic number used only in the old statistics
// and the old discrimination
const float minSignalTime = 0.05f;
enum WindowTypes {
WT_RECTANGULAR_HANN = 0, // 2.0.6 behavior, requires 1/2 step
WT_HANN_RECTANGULAR, // requires 1/2 step
WT_HANN_HANN, // requires 1/4 step
WT_BLACKMAN_HANN, // requires 1/4 step
WT_HAMMING_RECTANGULAR, // requires 1/2 step
WT_HAMMING_HANN, // requires 1/4 step
WT_HAMMING_INV_HAMMING, // requires 1/2 step
WT_N_WINDOW_TYPES,
WT_DEFAULT_WINDOW_TYPES = WT_HANN_HANN
};
const struct WindowTypesInfo {
const TranslatableString name;
unsigned minSteps;
double inCoefficients[3];
double outCoefficients[3];
double productConstantTerm;
} windowTypesInfo [WT_N_WINDOW_TYPES] = {
// In all of these cases (but the last), the constant term of the product of windows
// is the product of the windows' two constant terms,
// plus one half the product of the first cosine coefficients.
// Experimental only, don't need translations
{ XO("none, Hann (2.0.6 behavior)"), 2, { 1, 0, 0 }, { 0.5, -0.5, 0 }, 0.5 },
{ XO("Hann, none"), 2, { 0.5, -0.5, 0 }, { 1, 0, 0 }, 0.5 },
{ XO("Hann, Hann (default)"), 4, { 0.5, -0.5, 0 }, { 0.5, -0.5, 0 }, 0.375 },
{ XO("Blackman, Hann"), 4, { 0.42, -0.5, 0.08 }, { 0.5, -0.5, 0 }, 0.335 },
{ XO("Hamming, none"), 2, { 0.54, -0.46, 0.0 }, { 1, 0, 0 }, 0.54 },
{ XO("Hamming, Hann"), 4, { 0.54, -0.46, 0.0 }, { 0.5, -0.5, 0 }, 0.385 },
{ XO("Hamming, Reciprocal Hamming"), 2, { 0.54, -0.46, 0.0 }, { 1, 0, 0 }, 1.0 }, // output window is special
};
enum {
DEFAULT_WINDOW_SIZE_CHOICE = 8, // corresponds to 2048
DEFAULT_STEPS_PER_WINDOW_CHOICE = 1 // corresponds to 4, minimum for WT_HANN_HANN
};
enum NoiseReductionChoice {
NRC_REDUCE_NOISE,
NRC_ISOLATE_NOISE,
NRC_LEAVE_RESIDUE,
};
} // namespace
//----------------------------------------------------------------------------
// EffectNoiseReduction::Statistics
//----------------------------------------------------------------------------
class EffectNoiseReduction::Statistics
{
public:
Statistics(size_t spectrumSize, double rate, int windowTypes)
: mRate(rate)
, mWindowSize((spectrumSize - 1) * 2)
, mWindowTypes(windowTypes)
, mTotalWindows(0)
, mTrackWindows(0)
, mSums(spectrumSize)
, mMeans(spectrumSize)
#ifdef OLD_METHOD_AVAILABLE
, mNoiseThreshold(spectrumSize)
#endif
{}
// Noise profile statistics follow
double mRate; // Rate of profile track(s) -- processed tracks must match
size_t mWindowSize;
int mWindowTypes;
int mTotalWindows;
int mTrackWindows;
FloatVector mSums;
FloatVector mMeans;
#ifdef OLD_METHOD_AVAILABLE
// Old statistics:
FloatVector mNoiseThreshold;
#endif
};
//----------------------------------------------------------------------------
// EffectNoiseReduction::Settings
//----------------------------------------------------------------------------
// This object is the memory of the effect between uses
// (other than noise profile statistics)
class EffectNoiseReduction::Settings
{
public:
Settings();
~Settings() {}
bool PromptUser(EffectNoiseReduction *effect,
wxWindow &parent, bool bHasProfile, bool bAllowTwiddleSettings);
bool PrefsIO(bool read);
bool Validate(EffectNoiseReduction *effect) const;
size_t WindowSize() const { return 1u << (3 + mWindowSizeChoice); }
unsigned StepsPerWindow() const { return 1u << (1 + mStepsPerWindowChoice); }
bool mDoProfile;
// Stored in preferences:
// Basic:
double mNewSensitivity; // - log10 of a probability... yeah.
double mFreqSmoothingBands; // really an integer
double mNoiseGain; // in dB, positive
double mAttackTime; // in secs
double mReleaseTime; // in secs
// Advanced:
double mOldSensitivity; // in dB, plus or minus
// Basic:
int mNoiseReductionChoice;
// Advanced:
int mWindowTypes;
int mWindowSizeChoice;
int mStepsPerWindowChoice;
int mMethod;
};
EffectNoiseReduction::Settings::Settings()
: mDoProfile(true)
{
PrefsIO(true);
}
//----------------------------------------------------------------------------
// EffectNoiseReduction::Worker
//----------------------------------------------------------------------------
// This object holds information needed only during effect calculation
class EffectNoiseReduction::Worker
{
public:
typedef EffectNoiseReduction::Settings Settings;
typedef EffectNoiseReduction::Statistics Statistics;
Worker(const Settings &settings, double sampleRate
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
, double f0, double f1
#endif
);
~Worker();
bool Process(EffectNoiseReduction &effect,
Statistics &statistics, TrackFactory &factory,
TrackList &tracks, double mT0, double mT1);
private:
bool ProcessOne(EffectNoiseReduction &effect,
Statistics &statistics,
TrackFactory &factory,
int count, WaveTrack *track,
sampleCount start, sampleCount len);
void StartNewTrack();
void ProcessSamples(Statistics &statistics,
WaveTrack *outputTrack, size_t len, float *buffer);
void FillFirstHistoryWindow();
void ApplyFreqSmoothing(FloatVector &gains);
void GatherStatistics(Statistics &statistics);
inline bool Classify(const Statistics &statistics, int band);
void ReduceNoise(const Statistics &statistics, WaveTrack *outputTrack);
void RotateHistoryWindows();
void FinishTrackStatistics(Statistics &statistics);
void FinishTrack(Statistics &statistics, WaveTrack *outputTrack);
private:
const bool mDoProfile;
const double mSampleRate;
const size_t mWindowSize;
// These have that size:
HFFT hFFT;
FloatVector mFFTBuffer;
FloatVector mInWaveBuffer;
FloatVector mOutOverlapBuffer;
// These have that size, or 0:
FloatVector mInWindow;
FloatVector mOutWindow;
const size_t mSpectrumSize;
FloatVector mFreqSmoothingScratch;
const size_t mFreqSmoothingBins;
// When spectral selection limits the affected band:
int mBinLow; // inclusive lower bound
int mBinHigh; // exclusive upper bound
const int mNoiseReductionChoice;
const unsigned mStepsPerWindow;
const size_t mStepSize;
const int mMethod;
const double mNewSensitivity;
sampleCount mInSampleCount;
sampleCount mOutStepCount;
int mInWavePos;
float mOneBlockAttack;
float mOneBlockRelease;
float mNoiseAttenFactor;
float mOldSensitivityFactor;
unsigned mNWindowsToExamine;
unsigned mCenter;
unsigned mHistoryLen;
struct Record
{
Record(size_t spectrumSize)
: mSpectrums(spectrumSize)
, mGains(spectrumSize)
, mRealFFTs(spectrumSize - 1)
, mImagFFTs(spectrumSize - 1)
{
}
FloatVector mSpectrums;
FloatVector mGains;
FloatVector mRealFFTs;
FloatVector mImagFFTs;
};
std::vector<std::unique_ptr<Record>> mQueue;
};
/****************************************************************//**
\class EffectNoiseReduction::Dialog
\brief Dialog used with EffectNoiseReduction
**//*****************************************************************/
//----------------------------------------------------------------------------
// EffectNoiseReduction::Dialog
//----------------------------------------------------------------------------
class EffectNoiseReduction::Dialog final : public EffectDialog
{
public:
// constructors and destructors
Dialog
(EffectNoiseReduction *effect,
Settings *settings,
wxWindow *parent, bool bHasProfile,
bool bAllowTwiddleSettings);
void PopulateOrExchange(ShuttleGui & S) override;
bool TransferDataToWindow() override;
bool TransferDataFromWindow() override;
const Settings &GetTempSettings() const
{ return mTempSettings; }
private:
void DisableControlsIfIsolating();
#ifdef ADVANCED_SETTINGS
void EnableDisableSensitivityControls();
#endif
// handlers
void OnGetProfile( wxCommandEvent &event );
void OnNoiseReductionChoice( wxCommandEvent &event );
#ifdef ADVANCED_SETTINGS
void OnMethodChoice(wxCommandEvent &);
#endif
void OnPreview(wxCommandEvent &event) override;
void OnReduceNoise( wxCommandEvent &event );
void OnCancel( wxCommandEvent &event );
void OnHelp( wxCommandEvent &event );
void OnText(wxCommandEvent &event);
void OnSlider(wxCommandEvent &event);
// data members
EffectNoiseReduction *m_pEffect;
EffectNoiseReduction::Settings *m_pSettings;
EffectNoiseReduction::Settings mTempSettings;
bool mbHasProfile;
bool mbAllowTwiddleSettings;
wxRadioButton *mKeepSignal;
#ifdef ISOLATE_CHOICE
wxRadioButton *mKeepNoise;
#endif
#ifdef RESIDUE_CHOICE
wxRadioButton *mResidue;
#endif
private:
DECLARE_EVENT_TABLE()
};
EffectNoiseReduction::EffectNoiseReduction()
: mSettings(std::make_unique<EffectNoiseReduction::Settings>())
{
Init();
}
EffectNoiseReduction::~EffectNoiseReduction()
{
}
// ComponentInterface implementation
ComponentInterfaceSymbol EffectNoiseReduction::GetSymbol()
{
return NOISEREDUCTION_PLUGIN_SYMBOL;
}
TranslatableString EffectNoiseReduction::GetDescription()
{
return XO("Removes background noise such as fans, tape noise, or hums");
}
// EffectDefinitionInterface implementation
EffectType EffectNoiseReduction::GetType()
{
return EffectTypeProcess;
}
bool EffectNoiseReduction::Init()
{
return true;
}
bool EffectNoiseReduction::CheckWhetherSkipEffect()
{
return false;
}
bool EffectNoiseReduction::ShowInterface(
wxWindow &parent, const EffectDialogFactory &, bool forceModal)
{
// to do: use forceModal correctly
// Doesn't use the factory but substitutes its own dialog
// We may want to twiddle the levels if we are setting
// from an automation dialog, the only case in which we can
// get here without any wavetracks.
return mSettings->PromptUser(this, parent,
bool(mStatistics), (GetNumWaveTracks() == 0));
}
bool EffectNoiseReduction::Settings::PromptUser
(EffectNoiseReduction *effect, wxWindow &parent,
bool bHasProfile, bool bAllowTwiddleSettings)
{
EffectNoiseReduction::Dialog dlog
(effect, this, &parent, bHasProfile, bAllowTwiddleSettings);
dlog.CentreOnParent();
dlog.ShowModal();
if (dlog.GetReturnCode() == 0)
return false;
*this = dlog.GetTempSettings();
mDoProfile = (dlog.GetReturnCode() == 1);
return PrefsIO(false);
}
namespace {
template <typename StructureType, typename FieldType>
struct PrefsTableEntry {
typedef FieldType (StructureType::*MemberPointer);
MemberPointer field;
const wxChar *name;
FieldType defaultValue;
};
template <typename StructureType, typename FieldType>
void readPrefs(
StructureType *structure, const wxString &prefix,
const PrefsTableEntry<StructureType, FieldType> *fields, int numFields)
{
for (int ii = 0; ii < numFields; ++ii) {
const PrefsTableEntry<StructureType, FieldType> &entry = fields[ii];
gPrefs->Read(prefix + entry.name, &(structure->*(entry.field)),
entry.defaultValue);
}
}
template <typename StructureType, typename FieldType>
void writePrefs(
StructureType *structure, const wxString &prefix,
const PrefsTableEntry<StructureType, FieldType> *fields, int numFields)
{
for (int ii = 0; ii < numFields; ++ii) {
const PrefsTableEntry<StructureType, FieldType> &entry = fields[ii];
gPrefs->Write(prefix + entry.name, structure->*(entry.field));
}
}
}
bool EffectNoiseReduction::Settings::PrefsIO(bool read)
{
static const double DEFAULT_OLD_SENSITIVITY = 0.0;
static const PrefsTableEntry<Settings, double> doubleTable[] = {
{ &Settings::mNewSensitivity, wxT("Sensitivity"), 6.0 },
{ &Settings::mNoiseGain, wxT("Gain"), 12.0 },
{ &Settings::mAttackTime, wxT("AttackTime"), 0.02 },
{ &Settings::mReleaseTime, wxT("ReleaseTime"), 0.10 },
{ &Settings::mFreqSmoothingBands, wxT("FreqSmoothing"), 3.0 },
// Advanced settings
{ &Settings::mOldSensitivity, wxT("OldSensitivity"), DEFAULT_OLD_SENSITIVITY },
};
static int doubleTableSize = sizeof(doubleTable) / sizeof(doubleTable[0]);
static const PrefsTableEntry<Settings, int> intTable[] = {
{ &Settings::mNoiseReductionChoice, wxT("ReductionChoice"), NRC_REDUCE_NOISE },
// Advanced settings
{ &Settings::mWindowTypes, wxT("WindowTypes"), WT_DEFAULT_WINDOW_TYPES },
{ &Settings::mWindowSizeChoice, wxT("WindowSize"), DEFAULT_WINDOW_SIZE_CHOICE },
{ &Settings::mStepsPerWindowChoice, wxT("StepsPerWindow"), DEFAULT_STEPS_PER_WINDOW_CHOICE },
{ &Settings::mMethod, wxT("Method"), DM_DEFAULT_METHOD },
};
static int intTableSize = sizeof(intTable) / sizeof(intTable[0]);
static const wxString prefix(wxT("/Effects/NoiseReduction/"));
if (read) {
readPrefs(this, prefix, doubleTable, doubleTableSize);
readPrefs(this, prefix, intTable, intTableSize);
// Ignore preferences for unavailable options.
#ifndef RESIDUE_CHOICE
if (mNoiseReductionChoice == NRC_LEAVE_RESIDUE)
mNoiseReductionChoice = NRC_ISOLATE_NOISE;
#endif
#ifndef ADVANCED_SETTINGS
// Initialize all hidden advanced settings to defaults.
mWindowTypes = WT_DEFAULT_WINDOW_TYPES;
mWindowSizeChoice = DEFAULT_WINDOW_SIZE_CHOICE;
mStepsPerWindowChoice = DEFAULT_STEPS_PER_WINDOW_CHOICE;
mMethod = DM_DEFAULT_METHOD;
mOldSensitivity = DEFAULT_OLD_SENSITIVITY;
#endif
#ifndef OLD_METHOD_AVAILABLE
if (mMethod == DM_OLD_METHOD)
mMethod = DM_DEFAULT_METHOD;
#endif
return true;
}
else {
writePrefs(this, prefix, doubleTable, doubleTableSize);
writePrefs(this, prefix, intTable, intTableSize);
return gPrefs->Flush();
}
}
bool EffectNoiseReduction::Settings::Validate(EffectNoiseReduction *effect) const
{
if (StepsPerWindow() < windowTypesInfo[mWindowTypes].minSteps) {
effect->Effect::MessageBox(
XO("Steps per block are too few for the window types.") );
return false;
}
if (StepsPerWindow() > WindowSize()) {
effect->Effect::MessageBox(
XO("Steps per block cannot exceed the window size.") );
return false;
}
if (mMethod == DM_MEDIAN && StepsPerWindow() > 4) {
effect->Effect::MessageBox(
XO(
"Median method is not implemented for more than four steps per window.") );
return false;
}
return true;
}
bool EffectNoiseReduction::Process()
{
// This same code will either reduce noise or profile it
this->CopyInputTracks(); // Set up mOutputTracks.
auto track = * (mOutputTracks->Selected< const WaveTrack >()).begin();
if (!track)
return false;
// Initialize statistics if gathering them, or check for mismatched (advanced)
// settings if reducing noise.
if (mSettings->mDoProfile) {
size_t spectrumSize = 1 + mSettings->WindowSize() / 2;
mStatistics = std::make_unique<Statistics>
(spectrumSize, track->GetRate(), mSettings->mWindowTypes);
}
else if (mStatistics->mWindowSize != mSettings->WindowSize()) {
// possible only with advanced settings
::Effect::MessageBox(
XO("You must specify the same window size for steps 1 and 2.") );
return false;
}
else if (mStatistics->mWindowTypes != mSettings->mWindowTypes) {
// A warning only
::Effect::MessageBox(
XO("Warning: window types are not the same as for profiling.") );
}
Worker worker(*mSettings, mStatistics->mRate
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
, mF0, mF1
#endif
);
bool bGoodResult = worker.Process
(*this, *mStatistics, *mFactory, *mOutputTracks, mT0, mT1);
if (mSettings->mDoProfile) {
if (bGoodResult)
mSettings->mDoProfile = false; // So that "repeat last effect" will reduce noise
else
mStatistics.reset(); // So that profiling must be done again before noise reduction
}
this->ReplaceProcessedTracks(bGoodResult);
return bGoodResult;
}
EffectNoiseReduction::Worker::~Worker()
{
}
bool EffectNoiseReduction::Worker::Process
(EffectNoiseReduction &effect, Statistics &statistics, TrackFactory &factory,
TrackList &tracks, double inT0, double inT1)
{
int count = 0;
for ( auto track : tracks.Selected< WaveTrack >() ) {
if (track->GetRate() != mSampleRate) {
if (mDoProfile)
effect.Effect::MessageBox(
XO("All noise profile data must have the same sample rate.") );
else
effect.Effect::MessageBox(
XO(
"The sample rate of the noise profile must match that of the sound to be processed.") );
return false;
}
double trackStart = track->GetStartTime();
double trackEnd = track->GetEndTime();
double t0 = std::max(trackStart, inT0);
double t1 = std::min(trackEnd, inT1);
if (t1 > t0) {
auto start = track->TimeToLongSamples(t0);
auto end = track->TimeToLongSamples(t1);
auto len = end - start;
if (!ProcessOne(effect, statistics, factory,
count, track, start, len))
return false;
}
++count;
}
if (mDoProfile) {
if (statistics.mTotalWindows == 0) {
effect.Effect::MessageBox(
XO("Selected noise profile is too short.") );
return false;
}
}
return true;
}
void EffectNoiseReduction::Worker::ApplyFreqSmoothing(FloatVector &gains)
{
// Given an array of gain mutipliers, average them
// GEOMETRICALLY. Don't multiply and take nth root --
// that may quickly cause underflows. Instead, average the logs.
if (mFreqSmoothingBins == 0)
return;
{
float *pScratch = &mFreqSmoothingScratch[0];
std::fill(pScratch, pScratch + mSpectrumSize, 0.0f);
}
for (size_t ii = 0; ii < mSpectrumSize; ++ii)
gains[ii] = log(gains[ii]);
// ii must be signed
for (int ii = 0; ii < (int)mSpectrumSize; ++ii) {
const int j0 = std::max(0, ii - (int)mFreqSmoothingBins);
const int j1 = std::min(mSpectrumSize - 1, ii + mFreqSmoothingBins);
for(int jj = j0; jj <= j1; ++jj) {
mFreqSmoothingScratch[ii] += gains[jj];
}
mFreqSmoothingScratch[ii] /= (j1 - j0 + 1);
}
for (size_t ii = 0; ii < mSpectrumSize; ++ii)
gains[ii] = exp(mFreqSmoothingScratch[ii]);
}
EffectNoiseReduction::Worker::Worker
(const Settings &settings, double sampleRate
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
, double f0, double f1
#endif
)
: mDoProfile(settings.mDoProfile)
, mSampleRate(sampleRate)
, mWindowSize(settings.WindowSize())
, hFFT(GetFFT(mWindowSize))
, mFFTBuffer(mWindowSize)
, mInWaveBuffer(mWindowSize)
, mOutOverlapBuffer(mWindowSize)
, mInWindow()
, mOutWindow()
, mSpectrumSize(1 + mWindowSize / 2)
, mFreqSmoothingScratch(mSpectrumSize)
, mFreqSmoothingBins((int)(settings.mFreqSmoothingBands))
, mBinLow(0)
, mBinHigh(mSpectrumSize)
, mNoiseReductionChoice(settings.mNoiseReductionChoice)
, mStepsPerWindow(settings.StepsPerWindow())
, mStepSize(mWindowSize / mStepsPerWindow)
, mMethod(settings.mMethod)
// Sensitivity setting is a base 10 log, turn it into a natural log
, mNewSensitivity(settings.mNewSensitivity * log(10.0))
, mInSampleCount(0)
, mOutStepCount(0)
, mInWavePos(0)
{
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
{
const double bin = mSampleRate / mWindowSize;
if (f0 >= 0.0 )
mBinLow = floor(f0 / bin);
if (f1 >= 0.0)
mBinHigh = ceil(f1 / bin);
}
#endif
const double noiseGain = -settings.mNoiseGain;
const unsigned nAttackBlocks = 1 + (int)(settings.mAttackTime * sampleRate / mStepSize);
const unsigned nReleaseBlocks = 1 + (int)(settings.mReleaseTime * sampleRate / mStepSize);
// Applies to amplitudes, divide by 20:
mNoiseAttenFactor = DB_TO_LINEAR(noiseGain);
// Apply to gain factors which apply to amplitudes, divide by 20:
mOneBlockAttack = DB_TO_LINEAR(noiseGain / nAttackBlocks);
mOneBlockRelease = DB_TO_LINEAR(noiseGain / nReleaseBlocks);
// Applies to power, divide by 10:
mOldSensitivityFactor = pow(10.0, settings.mOldSensitivity / 10.0);
mNWindowsToExamine = (mMethod == DM_OLD_METHOD)
? std::max(2, (int)(minSignalTime * sampleRate / mStepSize))
: 1 + mStepsPerWindow;
mCenter = mNWindowsToExamine / 2;
wxASSERT(mCenter >= 1); // release depends on this assumption
if (mDoProfile)
#ifdef OLD_METHOD_AVAILABLE
mHistoryLen = mNWindowsToExamine;
#else
mHistoryLen = 1;
#endif
else {
// Allow long enough queue for sufficient inspection of the middle
// and for attack processing
// See ReduceNoise()
mHistoryLen = std::max(mNWindowsToExamine, mCenter + nAttackBlocks);
}
mQueue.resize(mHistoryLen);
for (unsigned ii = 0; ii < mHistoryLen; ++ii)
mQueue[ii] = std::make_unique<Record>(mSpectrumSize);
// Create windows
const double constantTerm =
windowTypesInfo[settings.mWindowTypes].productConstantTerm;
// One or the other window must by multiplied by this to correct for
// overlap. Must scale down as steps get smaller, and overlaps larger.
const double multiplier = 1.0 / (constantTerm * mStepsPerWindow);
// Create the analysis window
switch (settings.mWindowTypes) {
case WT_RECTANGULAR_HANN:
break;
default:
{
const bool rectangularOut =
settings.mWindowTypes == WT_HAMMING_RECTANGULAR ||
settings.mWindowTypes == WT_HANN_RECTANGULAR;
const double m =
rectangularOut ? multiplier : 1;
const double *const coefficients =
windowTypesInfo[settings.mWindowTypes].inCoefficients;
const double c0 = coefficients[0];
const double c1 = coefficients[1];
const double c2 = coefficients[2];
mInWindow.resize(mWindowSize);
for (size_t ii = 0; ii < mWindowSize; ++ii)
mInWindow[ii] = m *
(c0 + c1 * cos((2.0*M_PI*ii) / mWindowSize)
+ c2 * cos((4.0*M_PI*ii) / mWindowSize));
}
break;
}
if (!mDoProfile) {
// Create the synthesis window
switch (settings.mWindowTypes) {
case WT_HANN_RECTANGULAR:
case WT_HAMMING_RECTANGULAR:
break;
case WT_HAMMING_INV_HAMMING:
{
mOutWindow.resize(mWindowSize);
for (size_t ii = 0; ii < mWindowSize; ++ii)
mOutWindow[ii] = multiplier / mInWindow[ii];
}
break;
default:
{
const double *const coefficients =
windowTypesInfo[settings.mWindowTypes].outCoefficients;
const double c0 = coefficients[0];
const double c1 = coefficients[1];
const double c2 = coefficients[2];
mOutWindow.resize(mWindowSize);
for (size_t ii = 0; ii < mWindowSize; ++ii)
mOutWindow[ii] = multiplier *
(c0 + c1 * cos((2.0 * M_PI * ii) / mWindowSize)
+ c2 * cos((4.0 * M_PI * ii) / mWindowSize));
}
break;
}
}
}
void EffectNoiseReduction::Worker::StartNewTrack()
{
float *pFill;
for(unsigned ii = 0; ii < mHistoryLen; ++ii) {
Record &record = *mQueue[ii];
pFill = &record.mSpectrums[0];
std::fill(pFill, pFill + mSpectrumSize, 0.0f);
pFill = &record.mRealFFTs[0];
std::fill(pFill, pFill + mSpectrumSize - 1, 0.0f);
pFill = &record.mImagFFTs[0];
std::fill(pFill, pFill + mSpectrumSize - 1, 0.0f);
pFill = &record.mGains[0];
std::fill(pFill, pFill + mSpectrumSize, mNoiseAttenFactor);
}
pFill = &mOutOverlapBuffer[0];
std::fill(pFill, pFill + mWindowSize, 0.0f);
pFill = &mInWaveBuffer[0];
std::fill(pFill, pFill + mWindowSize, 0.0f);
if (mDoProfile)
{
// We do not want leading zero padded windows
mInWavePos = 0;
mOutStepCount = -(int)(mHistoryLen - 1);
}
else
{
// So that the queue gets primed with some windows,
// zero-padded in front, the first having mStepSize
// samples of wave data:
mInWavePos = mWindowSize - mStepSize;
// This starts negative, to count up until the queue fills:
mOutStepCount = -(int)(mHistoryLen - 1)
// ... and then must pass over the padded windows,
// before the first full window:
- (int)(mStepsPerWindow - 1);
}
mInSampleCount = 0;
}
void EffectNoiseReduction::Worker::ProcessSamples
(Statistics &statistics, WaveTrack *outputTrack,
size_t len, float *buffer)
{
while (len && mOutStepCount * mStepSize < mInSampleCount) {
auto avail = std::min(len, mWindowSize - mInWavePos);
memmove(&mInWaveBuffer[mInWavePos], buffer, avail * sizeof(float));
buffer += avail;
len -= avail;
mInWavePos += avail;
if (mInWavePos == (int)mWindowSize) {
FillFirstHistoryWindow();
if (mDoProfile)
GatherStatistics(statistics);
else
ReduceNoise(statistics, outputTrack);
++mOutStepCount;
RotateHistoryWindows();
// Rotate for overlap-add
memmove(&mInWaveBuffer[0], &mInWaveBuffer[mStepSize],
(mWindowSize - mStepSize) * sizeof(float));
mInWavePos -= mStepSize;
}
}
}
void EffectNoiseReduction::Worker::FillFirstHistoryWindow()
{
// Transform samples to frequency domain, windowed as needed
if (mInWindow.size() > 0)
for (size_t ii = 0; ii < mWindowSize; ++ii)
mFFTBuffer[ii] = mInWaveBuffer[ii] * mInWindow[ii];
else
memmove(&mFFTBuffer[0], &mInWaveBuffer[0], mWindowSize * sizeof(float));
RealFFTf(&mFFTBuffer[0], hFFT.get());
Record &record = *mQueue[0];
// Store real and imaginary parts for later inverse FFT, and compute
// power
{
float *pReal = &record.mRealFFTs[1];
float *pImag = &record.mImagFFTs[1];
float *pPower = &record.mSpectrums[1];
int *pBitReversed = &hFFT->BitReversed[1];
const auto last = mSpectrumSize - 1;
for (unsigned int ii = 1; ii < last; ++ii) {
const int kk = *pBitReversed++;
const float realPart = *pReal++ = mFFTBuffer[kk];
const float imagPart = *pImag++ = mFFTBuffer[kk + 1];
*pPower++ = realPart * realPart + imagPart * imagPart;
}
// DC and Fs/2 bins need to be handled specially
const float dc = mFFTBuffer[0];
record.mRealFFTs[0] = dc;
record.mSpectrums[0] = dc*dc;
const float nyquist = mFFTBuffer[1];
record.mImagFFTs[0] = nyquist; // For Fs/2, not really imaginary
record.mSpectrums[last] = nyquist * nyquist;
}
if (mNoiseReductionChoice != NRC_ISOLATE_NOISE)
{
// Default all gains to the reduction factor,
// until we decide to raise some of them later
float *pGain = &record.mGains[0];
std::fill(pGain, pGain + mSpectrumSize, mNoiseAttenFactor);
}
}
void EffectNoiseReduction::Worker::RotateHistoryWindows()
{
std::rotate(mQueue.begin(), mQueue.end() - 1, mQueue.end());
}
void EffectNoiseReduction::Worker::FinishTrackStatistics(Statistics &statistics)
{
const int windows = statistics.mTrackWindows;
const int multiplier = statistics.mTotalWindows;
const int denom = windows + multiplier;
// Combine averages in case of multiple profile tracks.
if (windows)
for (int ii = 0, nn = statistics.mMeans.size(); ii < nn; ++ii) {
float &mean = statistics.mMeans[ii];
float &sum = statistics.mSums[ii];
mean = (mean * multiplier + sum) / denom;
// Reset for next track
sum = 0;
}
// Reset for next track
statistics.mTrackWindows = 0;
statistics.mTotalWindows = denom;
}
void EffectNoiseReduction::Worker::FinishTrack
(Statistics &statistics, WaveTrack *outputTrack)
{
// Keep flushing empty input buffers through the history
// windows until we've output exactly as many samples as
// were input.
// Well, not exactly, but not more than one step-size of extra samples
// at the end.
// We'll DELETE them later in ProcessOne.
FloatVector empty(mStepSize);
while (mOutStepCount * mStepSize < mInSampleCount) {
ProcessSamples(statistics, outputTrack, mStepSize, &empty[0]);
}
}
void EffectNoiseReduction::Worker::GatherStatistics(Statistics &statistics)
{
++statistics.mTrackWindows;
{
// NEW statistics
const float *pPower = &mQueue[0]->mSpectrums[0];
float *pSum = &statistics.mSums[0];
for (size_t jj = 0; jj < mSpectrumSize; ++jj) {
*pSum++ += *pPower++;
}
}
#ifdef OLD_METHOD_AVAILABLE
// The noise threshold for each frequency is the maximum
// level achieved at that frequency for a minimum of
// mMinSignalBlocks blocks in a row - the max of a min.
auto finish = mHistoryLen;
{
// old statistics
const float *pPower = &mQueue[0]->mSpectrums[0];
float *pThreshold = &statistics.mNoiseThreshold[0];
for (int jj = 0; jj < mSpectrumSize; ++jj) {
float min = *pPower++;
for (unsigned ii = 1; ii < finish; ++ii)
min = std::min(min, mQueue[ii]->mSpectrums[jj]);
*pThreshold = std::max(*pThreshold, min);
++pThreshold;
}
}
#endif
}
// Return true iff the given band of the "center" window looks like noise.
// Examine the band in a few neighboring windows to decide.
inline
bool EffectNoiseReduction::Worker::Classify(const Statistics &statistics, int band)
{
switch (mMethod) {
#ifdef OLD_METHOD_AVAILABLE
case DM_OLD_METHOD:
{
float min = mQueue[0]->mSpectrums[band];
for (unsigned ii = 1; ii < mNWindowsToExamine; ++ii)
min = std::min(min, mQueue[ii]->mSpectrums[band]);
return min <= mOldSensitivityFactor * statistics.mNoiseThreshold[band];
}
#endif
// New methods suppose an exponential distribution of power values
// in the noise; NEW sensitivity is meant to be log of probability
// that noise strays above the threshold. Call that probability
// 1 - F. The quantile function of an exponential distribution is
// log (1 - F) * mean. Thus simply multiply mean by sensitivity
// to get the threshold.
case DM_MEDIAN:
// This method examines the window and all windows
// that partly overlap it, and takes a median, to
// avoid being fooled by up and down excursions into
// either the mistake of classifying noise as not noise
// (leaving a musical noise chime), or the opposite
// (distorting the signal with a drop out).
if (mNWindowsToExamine == 3)
// No different from second greatest.
goto secondGreatest;
else if (mNWindowsToExamine == 5)
{
float greatest = 0.0, second = 0.0, third = 0.0;
for (unsigned ii = 0; ii < mNWindowsToExamine; ++ii) {
const float power = mQueue[ii]->mSpectrums[band];
if (power >= greatest)
third = second, second = greatest, greatest = power;
else if (power >= second)
third = second, second = power;
else if (power >= third)
third = power;
}
return third <= mNewSensitivity * statistics.mMeans[band];
}
else {
wxASSERT(false);
return true;
}
secondGreatest:
case DM_SECOND_GREATEST:
{
// This method just throws out the high outlier. It
// should be less prone to distortions and more prone to
// chimes.
float greatest = 0.0, second = 0.0;
for (unsigned ii = 0; ii < mNWindowsToExamine; ++ii) {
const float power = mQueue[ii]->mSpectrums[band];
if (power >= greatest)
second = greatest, greatest = power;
else if (power >= second)
second = power;
}
return second <= mNewSensitivity * statistics.mMeans[band];
}
default:
wxASSERT(false);
return true;
}
}
void EffectNoiseReduction::Worker::ReduceNoise
(const Statistics &statistics, WaveTrack *outputTrack)
{
// Raise the gain for elements in the center of the sliding history
// or, if isolating noise, zero out the non-noise
{
float *pGain = &mQueue[mCenter]->mGains[0];
if (mNoiseReductionChoice == NRC_ISOLATE_NOISE) {
// All above or below the selected frequency range is non-noise
std::fill(pGain, pGain + mBinLow, 0.0f);
std::fill(pGain + mBinHigh, pGain + mSpectrumSize, 0.0f);
pGain += mBinLow;
for (int jj = mBinLow; jj < mBinHigh; ++jj) {
const bool isNoise = Classify(statistics, jj);
*pGain++ = isNoise ? 1.0 : 0.0;
}
}
else {
// All above or below the selected frequency range is non-noise
std::fill(pGain, pGain + mBinLow, 1.0f);
std::fill(pGain + mBinHigh, pGain + mSpectrumSize, 1.0f);
pGain += mBinLow;
for (int jj = mBinLow; jj < mBinHigh; ++jj) {
const bool isNoise = Classify(statistics, jj);
if (!isNoise)
*pGain = 1.0;
++pGain;
}
}
}
if (mNoiseReductionChoice != NRC_ISOLATE_NOISE)
{
// In each direction, define an exponential decay of gain from the
// center; make actual gains the maximum of mNoiseAttenFactor, and
// the decay curve, and their prior values.
// First, the attack, which goes backward in time, which is,
// toward higher indices in the queue.
for (size_t jj = 0; jj < mSpectrumSize; ++jj) {
for (unsigned ii = mCenter + 1; ii < mHistoryLen; ++ii) {
const float minimum =
std::max(mNoiseAttenFactor,
mQueue[ii - 1]->mGains[jj] * mOneBlockAttack);
float &gain = mQueue[ii]->mGains[jj];
if (gain < minimum)
gain = minimum;
else
// We can stop now, our attack curve is intersecting
// the decay curve of some window previously processed.
break;
}
}
// Now, release. We need only look one window ahead. This part will
// be visited again when we examine the next window, and
// carry the decay further.
{
float *pNextGain = &mQueue[mCenter - 1]->mGains[0];
const float *pThisGain = &mQueue[mCenter]->mGains[0];
for (int nn = mSpectrumSize; nn--;) {
*pNextGain =
std::max(*pNextGain,
std::max(mNoiseAttenFactor,
*pThisGain++ * mOneBlockRelease));
++pNextGain;
}
}
}
if (mOutStepCount >= -(int)(mStepsPerWindow - 1)) {
Record &record = *mQueue[mHistoryLen - 1]; // end of the queue
const auto last = mSpectrumSize - 1;
if (mNoiseReductionChoice != NRC_ISOLATE_NOISE)
// Apply frequency smoothing to output gain
// Gains are not less than mNoiseAttenFactor
ApplyFreqSmoothing(record.mGains);
// Apply gain to FFT
{
const float *pGain = &record.mGains[1];
const float *pReal = &record.mRealFFTs[1];
const float *pImag = &record.mImagFFTs[1];
float *pBuffer = &mFFTBuffer[2];
auto nn = mSpectrumSize - 2;
if (mNoiseReductionChoice == NRC_LEAVE_RESIDUE) {
for (; nn--;) {
// Subtract the gain we would otherwise apply from 1, and
// negate that to flip the phase.
const double gain = *pGain++ - 1.0;
*pBuffer++ = *pReal++ * gain;
*pBuffer++ = *pImag++ * gain;
}
mFFTBuffer[0] = record.mRealFFTs[0] * (record.mGains[0] - 1.0);
// The Fs/2 component is stored as the imaginary part of the DC component
mFFTBuffer[1] = record.mImagFFTs[0] * (record.mGains[last] - 1.0);
}
else {
for (; nn--;) {
const double gain = *pGain++;
*pBuffer++ = *pReal++ * gain;
*pBuffer++ = *pImag++ * gain;
}
mFFTBuffer[0] = record.mRealFFTs[0] * record.mGains[0];
// The Fs/2 component is stored as the imaginary part of the DC component
mFFTBuffer[1] = record.mImagFFTs[0] * record.mGains[last];
}
}
// Invert the FFT into the output buffer
InverseRealFFTf(&mFFTBuffer[0], hFFT.get());
// Overlap-add
if (mOutWindow.size() > 0) {
float *pOut = &mOutOverlapBuffer[0];
float *pWindow = &mOutWindow[0];
int *pBitReversed = &hFFT->BitReversed[0];
for (unsigned int jj = 0; jj < last; ++jj) {
int kk = *pBitReversed++;
*pOut++ += mFFTBuffer[kk] * (*pWindow++);
*pOut++ += mFFTBuffer[kk + 1] * (*pWindow++);
}
}
else {
float *pOut = &mOutOverlapBuffer[0];
int *pBitReversed = &hFFT->BitReversed[0];
for (unsigned int jj = 0; jj < last; ++jj) {
int kk = *pBitReversed++;
*pOut++ += mFFTBuffer[kk];
*pOut++ += mFFTBuffer[kk + 1];
}
}
float *buffer = &mOutOverlapBuffer[0];
if (mOutStepCount >= 0) {
// Output the first portion of the overlap buffer, they're done
outputTrack->Append((samplePtr)buffer, floatSample, mStepSize);
}
// Shift the remainder over.
memmove(buffer, buffer + mStepSize, sizeof(float) * (mWindowSize - mStepSize));
std::fill(buffer + mWindowSize - mStepSize, buffer + mWindowSize, 0.0f);
}
}
bool EffectNoiseReduction::Worker::ProcessOne
(EffectNoiseReduction &effect, Statistics &statistics, TrackFactory &factory,
int count, WaveTrack * track, sampleCount start, sampleCount len)
{
if (track == NULL)
return false;
StartNewTrack();
WaveTrack::Holder outputTrack;
if(!mDoProfile)
outputTrack = factory.NewWaveTrack(track->GetSampleFormat(), track->GetRate());
auto bufferSize = track->GetMaxBlockSize();
FloatVector buffer(bufferSize);
bool bLoopSuccess = true;
auto samplePos = start;
while (bLoopSuccess && samplePos < start + len) {
//Get a blockSize of samples (smaller than the size of the buffer)
const auto blockSize = limitSampleBufferSize(
track->GetBestBlockSize(samplePos),
start + len - samplePos
);
//Get the samples from the track and put them in the buffer
track->Get((samplePtr)&buffer[0], floatSample, samplePos, blockSize);
samplePos += blockSize;
mInSampleCount += blockSize;
ProcessSamples(statistics, outputTrack.get(), blockSize, &buffer[0]);
// Update the Progress meter, let user cancel
bLoopSuccess =
!effect.TrackProgress(count,
( samplePos - start ).as_double() /
len.as_double() );
}
if (bLoopSuccess) {
if (mDoProfile)
FinishTrackStatistics(statistics);
else
FinishTrack(statistics, &*outputTrack);
}
if (bLoopSuccess && !mDoProfile) {
// Flush the output WaveTrack (since it's buffered)
outputTrack->Flush();
// Take the output track and insert it in place of the original
// sample data (as operated on -- this may not match mT0/mT1)
double t0 = outputTrack->LongSamplesToTime(start);
double tLen = outputTrack->LongSamplesToTime(len);
// Filtering effects always end up with more data than they started with. Delete this 'tail'.
outputTrack->HandleClear(tLen, outputTrack->GetEndTime(), false, false);
track->ClearAndPaste(t0, t0 + tLen, &*outputTrack, true, false);
}
return bLoopSuccess;
}
//----------------------------------------------------------------------------
// EffectNoiseReduction::Dialog
//----------------------------------------------------------------------------
enum {
ID_BUTTON_GETPROFILE = 10001,
ID_RADIOBUTTON_KEEPSIGNAL,
#ifdef ISOLATE_CHOICE
ID_RADIOBUTTON_KEEPNOISE,
#endif
#ifdef RESIDUE_CHOICE
ID_RADIOBUTTON_RESIDUE,
#endif
#ifdef ADVANCED_SETTINGS
ID_CHOICE_METHOD,
#endif
// Slider/text pairs
ID_GAIN_SLIDER,
ID_GAIN_TEXT,
ID_NEW_SENSITIVITY_SLIDER,
ID_NEW_SENSITIVITY_TEXT,
#ifdef ATTACK_AND_RELEASE
ID_ATTACK_TIME_SLIDER,
ID_ATTACK_TIME_TEXT,
ID_RELEASE_TIME_SLIDER,
ID_RELEASE_TIME_TEXT,
#endif
ID_FREQ_SLIDER,
ID_FREQ_TEXT,
END_OF_BASIC_SLIDERS,
#ifdef ADVANCED_SETTINGS
ID_OLD_SENSITIVITY_SLIDER = END_OF_BASIC_SLIDERS,
ID_OLD_SENSITIVITY_TEXT,
END_OF_ADVANCED_SLIDERS,
END_OF_SLIDERS = END_OF_ADVANCED_SLIDERS,
#else
END_OF_SLIDERS = END_OF_BASIC_SLIDERS,
#endif
FIRST_SLIDER = ID_GAIN_SLIDER,
};
namespace {
struct ControlInfo {
typedef double (EffectNoiseReduction::Settings::*MemberPointer);
double Value(long sliderSetting) const
{
return
valueMin +
(double(sliderSetting) / sliderMax) * (valueMax - valueMin);
}
long SliderSetting(double value) const
{
return TrapLong(
0.5 + sliderMax * (value - valueMin) / (valueMax - valueMin),
0, sliderMax);
}
wxString Text(double value) const
{
if (formatAsInt)
return wxString::Format(format, (int)(value));
else
return wxString::Format(format, value);
}
void CreateControls(int id, ShuttleGui &S) const
{
wxTextCtrl *const text = S.Id(id + 1)
.Validator<FloatingPointValidator<double>>(
formatAsInt ? 0 : 2,
nullptr,
NumValidatorStyle::DEFAULT,
valueMin, valueMax
)
.AddTextBox(textBoxCaption, wxT(""), 0);
wxSlider *const slider =
S.Id(id)
.Name( sliderName )
.Style(wxSL_HORIZONTAL)
.MinSize( { 150, -1 } )
.AddSlider( {}, 0, sliderMax);
}
MemberPointer field;
double valueMin;
double valueMax;
long sliderMax;
// (valueMin - valueMax) / sliderMax is the value increment of the slider
const wxChar* format;
bool formatAsInt;
const TranslatableString textBoxCaption;
const TranslatableString sliderName;
ControlInfo(MemberPointer f, double vMin, double vMax, long sMax, const wxChar* fmt, bool fAsInt,
const TranslatableString &caption, const TranslatableString &name)
: field(f), valueMin(vMin), valueMax(vMax), sliderMax(sMax), format(fmt), formatAsInt(fAsInt)
, textBoxCaption(caption), sliderName(name)
{
}
};
const ControlInfo *controlInfo() {
static const ControlInfo table[] = {
ControlInfo(&EffectNoiseReduction::Settings::mNoiseGain,
0.0, 48.0, 48, wxT("%d"), true,
XO("&Noise reduction (dB):"), XO("Noise reduction")),
ControlInfo(&EffectNoiseReduction::Settings::mNewSensitivity,
0.0, 24.0, 48, wxT("%.2f"), false,
XO("&Sensitivity:"), XO("Sensitivity")),
#ifdef ATTACK_AND_RELEASE
ControlInfo(&EffectNoiseReduction::Settings::mAttackTime,
0, 1.0, 100, wxT("%.2f"), false,
XO("Attac&k time (secs):"), XO("Attack time")),
ControlInfo(&EffectNoiseReduction::Settings::mReleaseTime,
0, 1.0, 100, wxT("%.2f"), false,
XO("R&elease time (secs):"), XO("Release time")),
#endif
ControlInfo(&EffectNoiseReduction::Settings::mFreqSmoothingBands,
0, 12, 12, wxT("%d"), true,
XO("&Frequency smoothing (bands):"), XO("Frequency smoothing")),
#ifdef ADVANCED_SETTINGS
ControlInfo(&EffectNoiseReduction::Settings::mOldSensitivity,
-20.0, 20.0, 4000, wxT("%.2f"), false,
XO("Sensiti&vity (dB):"), XO("Old Sensitivity")),
// add here
#endif
};
return table;
}
} // namespace
BEGIN_EVENT_TABLE(EffectNoiseReduction::Dialog, wxDialogWrapper)
EVT_BUTTON(wxID_OK, EffectNoiseReduction::Dialog::OnReduceNoise)
EVT_BUTTON(wxID_CANCEL, EffectNoiseReduction::Dialog::OnCancel)
EVT_BUTTON(ID_EFFECT_PREVIEW, EffectNoiseReduction::Dialog::OnPreview)
EVT_BUTTON(ID_BUTTON_GETPROFILE, EffectNoiseReduction::Dialog::OnGetProfile)
EVT_BUTTON(wxID_HELP, EffectNoiseReduction::Dialog::OnHelp)
EVT_RADIOBUTTON(ID_RADIOBUTTON_KEEPSIGNAL, EffectNoiseReduction::Dialog::OnNoiseReductionChoice)
#ifdef ISOLATE_CHOICE
EVT_RADIOBUTTON(ID_RADIOBUTTON_KEEPNOISE, EffectNoiseReduction::Dialog::OnNoiseReductionChoice)
#endif
#ifdef RESIDUE_CHOICE
EVT_RADIOBUTTON(ID_RADIOBUTTON_RESIDUE, EffectNoiseReduction::Dialog::OnNoiseReductionChoice)
#endif
#ifdef ADVANCED_SETTINGS
EVT_CHOICE(ID_CHOICE_METHOD, EffectNoiseReduction::Dialog::OnMethodChoice)
#endif
EVT_SLIDER(ID_GAIN_SLIDER, EffectNoiseReduction::Dialog::OnSlider)
EVT_TEXT(ID_GAIN_TEXT, EffectNoiseReduction::Dialog::OnText)
EVT_SLIDER(ID_NEW_SENSITIVITY_SLIDER, EffectNoiseReduction::Dialog::OnSlider)
EVT_TEXT(ID_NEW_SENSITIVITY_TEXT, EffectNoiseReduction::Dialog::OnText)
EVT_SLIDER(ID_FREQ_SLIDER, EffectNoiseReduction::Dialog::OnSlider)
EVT_TEXT(ID_FREQ_TEXT, EffectNoiseReduction::Dialog::OnText)
#ifdef ATTACK_AND_RELEASE
EVT_SLIDER(ID_ATTACK_TIME_SLIDER, EffectNoiseReduction::Dialog::OnSlider)
EVT_TEXT(ID_ATTACK_TIME_TEXT, EffectNoiseReduction::Dialog::OnText)
EVT_SLIDER(ID_RELEASE_TIME_SLIDER, EffectNoiseReduction::Dialog::OnSlider)
EVT_TEXT(ID_RELEASE_TIME_TEXT, EffectNoiseReduction::Dialog::OnText)
#endif
#ifdef ADVANCED_SETTINGS
EVT_SLIDER(ID_OLD_SENSITIVITY_SLIDER, EffectNoiseReduction::Dialog::OnSlider)
EVT_TEXT(ID_OLD_SENSITIVITY_TEXT, EffectNoiseReduction::Dialog::OnText)
#endif
END_EVENT_TABLE()
EffectNoiseReduction::Dialog::Dialog
(EffectNoiseReduction *effect,
EffectNoiseReduction::Settings *settings,
wxWindow *parent, bool bHasProfile, bool bAllowTwiddleSettings)
: EffectDialog( parent, XO("Noise Reduction"), EffectTypeProcess,wxDEFAULT_DIALOG_STYLE, eHelpButton )
, m_pEffect(effect)
, m_pSettings(settings) // point to
, mTempSettings(*settings) // copy
, mbHasProfile(bHasProfile)
, mbAllowTwiddleSettings(bAllowTwiddleSettings)
// NULL out the control members until the controls are created.
, mKeepSignal(NULL)
#ifdef ISOLATE_CHOICE
, mKeepNoise(NULL)
#endif
#ifdef RESIDUE_CHOICE
, mResidue(NULL)
#endif
{
EffectDialog::Init();
wxButton *const pButtonPreview =
(wxButton *)wxWindow::FindWindowById(ID_EFFECT_PREVIEW, this);
wxButton *const pButtonReduceNoise =
(wxButton *)wxWindow::FindWindowById(wxID_OK, this);
if (mbHasProfile || mbAllowTwiddleSettings) {
pButtonPreview->Enable(!mbAllowTwiddleSettings);
pButtonReduceNoise->SetFocus();
}
else {
pButtonPreview->Enable(false);
pButtonReduceNoise->Enable(false);
}
}
void EffectNoiseReduction::Dialog::DisableControlsIfIsolating()
{
// If Isolate is chosen, disable controls that define
// "what to do with noise" rather than "what is noise."
// Else, enable them.
// This does NOT include sensitivity, NEW or old, nor
// the choice of window functions, size, or step.
// The method choice is not included, because it affects
// which sensitivity slider is operative, and that is part
// of what defines noise.
static const int toDisable[] = {
ID_GAIN_SLIDER,
ID_GAIN_TEXT,
ID_FREQ_SLIDER,
ID_FREQ_TEXT,
#ifdef ATTACK_AND_RELEASE
ID_ATTACK_TIME_SLIDER,
ID_ATTACK_TIME_TEXT,
ID_RELEASE_TIME_SLIDER,
ID_RELEASE_TIME_TEXT,
#endif
};
static const int nToDisable = sizeof(toDisable) / sizeof(toDisable[0]);
bool bIsolating =
#ifdef ISOLATE_CHOICE
mKeepNoise->GetValue();
#else
false;
#endif
for (int ii = nToDisable; ii--;)
wxWindow::FindWindowById(toDisable[ii], this)->Enable(!bIsolating);
}
#ifdef ADVANCED_SETTINGS
void EffectNoiseReduction::Dialog::EnableDisableSensitivityControls()
{
wxChoice *const pChoice =
static_cast<wxChoice*>(wxWindow::FindWindowById(ID_CHOICE_METHOD, this));
const bool bOldMethod =
pChoice->GetSelection() == DM_OLD_METHOD;
wxWindow::FindWindowById(ID_OLD_SENSITIVITY_SLIDER, this)->Enable(bOldMethod);
wxWindow::FindWindowById(ID_OLD_SENSITIVITY_TEXT, this)->Enable(bOldMethod);
wxWindow::FindWindowById(ID_NEW_SENSITIVITY_SLIDER, this)->Enable(!bOldMethod);
wxWindow::FindWindowById(ID_NEW_SENSITIVITY_TEXT, this)->Enable(!bOldMethod);
}
#endif
void EffectNoiseReduction::Dialog::OnGetProfile(wxCommandEvent & WXUNUSED(event))
{
// Project has not be changed so skip pushing state
EffectManager::Get().SetSkipStateFlag(true);
if (!TransferDataFromWindow())
return;
// Return code distinguishes this first step from the actual effect
EndModal(1);
}
// This handles the whole radio group
void EffectNoiseReduction::Dialog::OnNoiseReductionChoice( wxCommandEvent & WXUNUSED(event))
{
if (mKeepSignal->GetValue())
mTempSettings.mNoiseReductionChoice = NRC_REDUCE_NOISE;
#ifdef ISOLATE_CHOICE
else if (mKeepNoise->GetValue())
mTempSettings.mNoiseReductionChoice = NRC_ISOLATE_NOISE;
#endif
#ifdef RESIDUE_CHOICE
else
mTempSettings.mNoiseReductionChoice = NRC_LEAVE_RESIDUE;
#endif
DisableControlsIfIsolating();
}
#ifdef ADVANCED_SETTINGS
void EffectNoiseReduction::Dialog::OnMethodChoice(wxCommandEvent &)
{
EnableDisableSensitivityControls();
}
#endif
void EffectNoiseReduction::Dialog::OnPreview(wxCommandEvent & WXUNUSED(event))
{
if (!TransferDataFromWindow())
return;
// Save & restore parameters around Preview, because we didn't do OK.
auto cleanup = valueRestorer( *m_pSettings );
*m_pSettings = mTempSettings;
m_pSettings->mDoProfile = false;
m_pEffect->Preview( false );
}
void EffectNoiseReduction::Dialog::OnReduceNoise( wxCommandEvent & WXUNUSED(event))
{
if (!TransferDataFromWindow())
return;
EndModal(2);
}
void EffectNoiseReduction::Dialog::OnCancel(wxCommandEvent & WXUNUSED(event))
{
EndModal(0);
}
void EffectNoiseReduction::Dialog::OnHelp(wxCommandEvent & WXUNUSED(event))
{
HelpSystem::ShowHelp(this, "Noise_Reduction", true);
}
void EffectNoiseReduction::Dialog::PopulateOrExchange(ShuttleGui & S)
{
S.StartStatic(XO("Step 1"));
{
S.AddVariableText(XO(
"Select a few seconds of just noise so Audacity knows what to filter out,\nthen click Get Noise Profile:"));
//m_pButton_GetProfile =
S.Id(ID_BUTTON_GETPROFILE).AddButton(XO("&Get Noise Profile"));
}
S.EndStatic();
S.StartStatic(XO("Step 2"));
{
S.AddVariableText(XO(
"Select all of the audio you want filtered, choose how much noise you want\nfiltered out, and then click 'OK' to reduce noise.\n"));
S.StartMultiColumn(3, wxEXPAND);
S.SetStretchyCol(2);
{
for (int id = FIRST_SLIDER; id < END_OF_BASIC_SLIDERS; id += 2) {
const ControlInfo &info = controlInfo()[(id - FIRST_SLIDER) / 2];
info.CreateControls(id, S);
}
}
S.EndMultiColumn();
S.StartMultiColumn(
2
#ifdef RESIDUE_CHOICE
+1
#endif
#ifdef ISOLATE_CHOICE
+1
#endif
,
wxALIGN_CENTER_HORIZONTAL);
{
S.AddPrompt(XO("Noise:"));
mKeepSignal = S.Id(ID_RADIOBUTTON_KEEPSIGNAL)
.AddRadioButton(XO("Re&duce")); /* i18n-hint: Translate differently from "Residue" ! */
#ifdef ISOLATE_CHOICE
mKeepNoise = S.Id(ID_RADIOBUTTON_KEEPNOISE)
.AddRadioButtonToGroup(XO("&Isolate"));
#endif
#ifdef RESIDUE_CHOICE
mResidue = S.Id(ID_RADIOBUTTON_RESIDUE)
.AddRadioButtonToGroup(XO("Resid&ue")); /* i18n-hint: Means the difference between effect and original sound. Translate differently from "Reduce" ! */
#endif
}
S.EndMultiColumn();
}
S.EndStatic();
#ifdef ADVANCED_SETTINGS
S.StartStatic(XO("Advanced Settings"));
{
S.StartMultiColumn(2);
{
S.TieChoice(XO("&Window types:"),
mTempSettings.mWindowTypes,
[]{
TranslatableStrings windowTypeChoices;
for (int ii = 0; ii < WT_N_WINDOW_TYPES; ++ii)
windowTypeChoices.push_back(windowTypesInfo[ii].name);
return windowTypeChoices;
}()
);
S.TieChoice(XO("Window si&ze:"),
mTempSettings.mWindowSizeChoice,
{
XO("8") ,
XO("16") ,
XO("32") ,
XO("64") ,
XO("128") ,
XO("256") ,
XO("512") ,
XO("1024") ,
XO("2048 (default)") ,
XO("4096") ,
XO("8192") ,
XO("16384") ,
}
);
S.TieChoice(XO("S&teps per window:"),
mTempSettings.mStepsPerWindowChoice,
{
XO("2") ,
XO("4 (default)") ,
XO("8") ,
XO("16") ,
XO("32") ,
XO("64") ,
}
);
S.Id(ID_CHOICE_METHOD)
.TieChoice(XO("Discrimination &method:"),
mTempSettings.mMethod,
[]{
TranslatableStrings methodChoices;
int nn = DM_N_METHODS;
#ifndef OLD_METHOD_AVAILABLE
--nn;
#endif
for (int ii = 0; ii < nn; ++ii)
methodChoices.push_back(discriminationMethodInfo[ii].name);
return methodChoices;
}());
}
S.EndMultiColumn();
S.StartMultiColumn(3, wxEXPAND);
S.SetStretchyCol(2);
{
for (int id = END_OF_BASIC_SLIDERS; id < END_OF_ADVANCED_SLIDERS; id += 2) {
const ControlInfo &info = controlInfo()[(id - FIRST_SLIDER) / 2];
info.CreateControls(id, S);
}
}
S.EndMultiColumn();
}
S.EndStatic();
#endif
}
bool EffectNoiseReduction::Dialog::TransferDataToWindow()
{
// Do the choice controls:
if (!EffectDialog::TransferDataToWindow())
return false;
for (int id = FIRST_SLIDER; id < END_OF_SLIDERS; id += 2) {
wxSlider* slider =
static_cast<wxSlider*>(wxWindow::FindWindowById(id, this));
wxTextCtrl* text =
static_cast<wxTextCtrl*>(wxWindow::FindWindowById(id + 1, this));
const ControlInfo &info = controlInfo()[(id - FIRST_SLIDER) / 2];
const double field = mTempSettings.*(info.field);
text->SetValue(info.Text(field));
slider->SetValue(info.SliderSetting(field));
}
mKeepSignal->SetValue(mTempSettings.mNoiseReductionChoice == NRC_REDUCE_NOISE);
#ifdef ISOLATE_CHOICE
mKeepNoise->SetValue(mTempSettings.mNoiseReductionChoice == NRC_ISOLATE_NOISE);
#endif
#ifdef RESIDUE_CHOICE
mResidue->SetValue(mTempSettings.mNoiseReductionChoice == NRC_LEAVE_RESIDUE);
#endif
// Set the enabled states of controls
DisableControlsIfIsolating();
#ifdef ADVANCED_SETTINGS
EnableDisableSensitivityControls();
#endif
return true;
}
bool EffectNoiseReduction::Dialog::TransferDataFromWindow()
{
if( !wxWindow::Validate() )
return false;
// Do the choice controls:
if (!EffectDialog::TransferDataFromWindow())
return false;
wxCommandEvent dummy;
OnNoiseReductionChoice(dummy);
return mTempSettings.Validate(m_pEffect);
}
void EffectNoiseReduction::Dialog::OnText(wxCommandEvent &event)
{
int id = event.GetId();
int idx = (id - FIRST_SLIDER - 1) / 2;
const ControlInfo &info = controlInfo()[idx];
wxTextCtrl* text =
static_cast<wxTextCtrl*>(wxWindow::FindWindowById(id, this));
wxSlider* slider =
static_cast<wxSlider*>(wxWindow::FindWindowById(id - 1, this));
double &field = mTempSettings.*(info.field);
text->GetValue().ToDouble(&field);
slider->SetValue(info.SliderSetting(field));
}
void EffectNoiseReduction::Dialog::OnSlider(wxCommandEvent &event)
{
int id = event.GetId();
int idx = (id - FIRST_SLIDER) / 2;
const ControlInfo &info = controlInfo()[idx];
wxSlider* slider =
static_cast<wxSlider*>(wxWindow::FindWindowById(id, this));
wxTextCtrl* text =
static_cast<wxTextCtrl*>(wxWindow::FindWindowById(id + 1, this));
double &field = mTempSettings.*(info.field);
field = info.Value(slider->GetValue());
text->SetValue(info.Text(field));
}