audacia/src/effects/TruncSilence.cpp

849 lines
26 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
TruncSilence.cpp
Lynn Allan (from DM's Normalize)
Philip Van Baren (more options and boundary fixes)
*******************************************************************//**
\class EffectTruncSilence
\brief Truncate Silence automatically reduces the length of passages
where the volume is below a set threshold level.
*//*******************************************************************/
#include "../Audacity.h"
#include "TruncSilence.h"
#include <limits>
#include <math.h>
#include <wx/valgen.h>
#include "../Prefs.h"
#include "../ShuttleGui.h"
#include "../WaveTrack.h"
#include "../widgets/valnum.h"
enum kActions
{
kTruncate,
kCompress,
kNumActions
};
static const wxChar *kActionStrings[kNumActions] =
{
XO("Truncate Detected Silence"),
XO("Compress Excess Silence")
};
// Define defaults, minimums, and maximums for each parameter
#define DefaultAndLimits(name, def, min, max) \
static const double DEF_ ## name = (def); \
static const double MIN_ ## name = (min); \
static const double MAX_ ## name = (max);
// Define keys, defaults, minimums, and maximums for the effect parameters
//
// Name Type Key Def Min Max Scale
Param( DbIndex, int, XO("Db"), 0, 0, Enums::NumDbChoices - 1, 1 );
Param( ActIndex, int, XO("Action"), kTruncate, 0, kNumActions - 1, 1 );
Param( Minimum, double, XO("Minimum"), 0.5, 0.001, 10000.0, 1 );
Param( Truncate, double, XO("Truncate"), 0.5, 0.0, 10000.0, 1 );
Param( Compress, double, XO("Compress"), 50.0, 0.0, 99.9, 1 );
static const sampleCount DEF_BlendFrameCount = 100;
// Lower bound on the amount of silence to find at a time -- this avoids
// detecting silence repeatedly in low-frequency sounds.
static const double DEF_MinTruncMs = 0.001;
// Typical fraction of total time taken by detection (better to guess low)
const double detectFrac = 0.4;
#include <wx/listimpl.cpp>
WX_DEFINE_LIST(RegionList);
BEGIN_EVENT_TABLE(EffectTruncSilence, wxEvtHandler)
EVT_CHOICE(wxID_ANY, EffectTruncSilence::OnControlChange)
EVT_TEXT(wxID_ANY, EffectTruncSilence::OnControlChange)
END_EVENT_TABLE()
EffectTruncSilence::EffectTruncSilence()
{
mDbChoices = wxArrayString(Enums::NumDbChoices, Enums::GetDbChoices());
mInitialAllowedSilence = DEF_Minimum;
mTruncLongestAllowedSilence = DEF_Truncate;
mSilenceCompressPercent = DEF_Compress;
mTruncDbChoiceIndex = DEF_DbIndex;
mActionIndex = DEF_ActIndex;
SetLinearEffectFlag(false);
// This used to be changeable via the audacity.cfg/registery. Doubtful that was
// ever done.
//
// Original comment:
//
// mBlendFrameCount only retrieved from prefs ... not using dialog
// Only way to change (for windows) is thru registry
// The values should be figured dynamically ... too many frames could be invalid
mBlendFrameCount = DEF_BlendFrameCount;
}
EffectTruncSilence::~EffectTruncSilence()
{
}
// IdentInterface implementation
wxString EffectTruncSilence::GetSymbol()
{
return TRUNCATESILENCE_PLUGIN_SYMBOL;
}
wxString EffectTruncSilence::GetDescription()
{
return XO("Automatically reduces the length of passages where the volume is below a specified level");
}
// EffectIdentInterface implementation
EffectType EffectTruncSilence::GetType()
{
return EffectTypeProcess;
}
// EffectClientInterface implementation
bool EffectTruncSilence::GetAutomationParameters(EffectAutomationParameters & parms)
{
parms.Write(KEY_DbIndex, Enums::DbChoices[mTruncDbChoiceIndex]);
parms.Write(KEY_ActIndex, kActionStrings[mActionIndex]);
parms.Write(KEY_Minimum, mInitialAllowedSilence);
parms.Write(KEY_Truncate, mTruncLongestAllowedSilence);
parms.Write(KEY_Compress, mSilenceCompressPercent);
return true;
}
bool EffectTruncSilence::SetAutomationParameters(EffectAutomationParameters & parms)
{
wxArrayString actions(kNumActions, kActionStrings);
actions.Insert(wxT("0"), 0); // Compatible with 2.1.0 and before
actions.Insert(wxT("1"), 1); // Compatible with 2.1.0 and before
ReadAndVerifyDouble(Minimum);
ReadAndVerifyDouble(Truncate);
ReadAndVerifyDouble(Compress);
ReadAndVerifyEnum(DbIndex, mDbChoices);
ReadAndVerifyEnum(ActIndex, actions);
mInitialAllowedSilence = Minimum;
mTruncLongestAllowedSilence = Truncate;
mSilenceCompressPercent = Compress;
mTruncDbChoiceIndex = DbIndex;
mActionIndex = ActIndex;
// Readjust for 2.1.0 or before
if (mActionIndex >= kNumActions)
{
mActionIndex -= kNumActions;
}
return true;
}
// Effect implementation
double EffectTruncSilence::CalcPreviewInputLength(double previewLength)
{
double inputLength = mT1 - mT0;
double minInputLength = inputLength;
// Master list of silent regions
RegionList silences;
silences.DeleteContents(true);
// Start with the whole selection silent
Region *sel = new Region;
sel->start = mT0;
sel->end = mT1;
silences.push_back(sel);
SelectedTrackListOfKindIterator iter(Track::Wave, mTracks);
int whichTrack = 0;
for (Track *t = iter.First(); t; t = iter.Next()) {
WaveTrack *wt = (WaveTrack *)t;
RegionList trackSilences;
trackSilences.DeleteContents(true);
sampleCount index = wt->TimeToLongSamples(mT0);
sampleCount silentFrame = 0; // length of the current silence
Analyze(silences, trackSilences, wt, &silentFrame, &index, whichTrack, &inputLength, &minInputLength);
whichTrack++;
}
return inputLength;
}
bool EffectTruncSilence::Startup()
{
wxString base = wxT("/Effects/TruncateSilence/");
// Migrate settings from 2.1.0 or before
// Already migrated, so bail
if (gPrefs->Exists(base + wxT("Migrated")))
{
return true;
}
// Load the old "current" settings
if (gPrefs->Exists(base))
{
mTruncDbChoiceIndex = gPrefs->Read(base + wxT("DbChoiceIndex"), 4L);
if ((mTruncDbChoiceIndex < 0) || (mTruncDbChoiceIndex >= Enums::NumDbChoices))
{ // corrupted Prefs?
mTruncDbChoiceIndex = 4L;
}
mActionIndex = gPrefs->Read(base + wxT("ProcessChoice"), 0L);
if ((mActionIndex < 0) || (mActionIndex > 1))
{ // corrupted Prefs?
mActionIndex = 0L;
}
gPrefs->Read(base + wxT("InitialAllowedSilence"), &mInitialAllowedSilence, 0.5);
if ((mInitialAllowedSilence < 0.001) || (mInitialAllowedSilence > 10000.0))
{ // corrupted Prefs?
mInitialAllowedSilence = 0.5;
}
gPrefs->Read(base + wxT("LongestAllowedSilence"), &mTruncLongestAllowedSilence, 0.5);
if ((mTruncLongestAllowedSilence < 0.0) || (mTruncLongestAllowedSilence > 10000.0))
{ // corrupted Prefs?
mTruncLongestAllowedSilence = 0.5;
}
gPrefs->Read(base + wxT("CompressPercent"), &mSilenceCompressPercent, 50.0);
if ((mSilenceCompressPercent < 0.0) || (mSilenceCompressPercent > 100.0))
{ // corrupted Prefs?
mSilenceCompressPercent = 50.0;
}
SaveUserPreset(GetCurrentSettingsGroup());
}
// Do not migrate again
gPrefs->Write(base + wxT("Migrated"), true);
return true;
}
bool EffectTruncSilence::Process()
{
// Copy tracks
CopyInputTracks(Track::All);
// Master list of silent regions; it is responsible for deleting them.
// This list should always be kept in order.
RegionList silences;
silences.DeleteContents(true);
// Start with the whole selection silent
Region *sel = new Region;
sel->start = mT0;
sel->end = mT1;
silences.push_back(sel);
// Remove non-silent regions in each track
SelectedTrackListOfKindIterator iter(Track::Wave, mTracks);
int whichTrack = 0;
for (Track *t = iter.First(); t; t = iter.Next())
{
WaveTrack *wt = (WaveTrack *)t;
// Smallest silent region to detect in frames
sampleCount minSilenceFrames =
sampleCount(wxMax( mInitialAllowedSilence, DEF_MinTruncMs) * wt->GetRate());
//
// Scan the track for silences
//
RegionList trackSilences;
trackSilences.DeleteContents(true);
sampleCount index = wt->TimeToLongSamples(mT0);
sampleCount silentFrame = 0;
// Detect silences
bool cancelled = !(Analyze(silences, trackSilences, wt, &silentFrame, &index, whichTrack));
// Buffer has been freed, so we're OK to return if cancelled
if (cancelled)
{
ReplaceProcessedTracks(false);
return false;
}
if (silentFrame >= minSilenceFrames)
{
// Track ended in silence -- record region
Region *r = new Region;
r->start = wt->LongSamplesToTime(index - silentFrame);
r->end = wt->LongSamplesToTime(index);
trackSilences.push_back(r);
}
// Intersect with the overall silent region list
Intersect(silences, trackSilences);
whichTrack++;
}
//
// Now remove the silent regions from all selected / sync-lock selected tracks.
//
// Loop over detected regions in reverse (so cuts don't change time values
// down the line)
int whichReg = 0;
RegionList::reverse_iterator rit;
double totalCutLen = 0.0; // For cutting selection at the end
for (rit = silences.rbegin(); rit != silences.rend(); ++rit)
{
Region *r = *rit;
// Progress dialog and cancellation. Do additional cleanup before return.
if (TotalProgress(detectFrac + (1 - detectFrac) * whichReg / (double)silences.size()))
{
ReplaceProcessedTracks(false);
return false;
}
// Intersection may create regions smaller than allowed; ignore them.
// Allow one nanosecond extra for consistent results with exact milliseconds of allowed silence.
if ((r->end - r->start) < (mInitialAllowedSilence - 0.000000001))
continue;
// Find new silence length as requested
double inLength = r->end - r->start;
double outLength;
switch (mActionIndex)
{
case kTruncate:
outLength = wxMin(mTruncLongestAllowedSilence, inLength);
break;
case kCompress:
outLength = mInitialAllowedSilence +
(inLength - mInitialAllowedSilence) * mSilenceCompressPercent / 100.0;
break;
default: // Not currently used.
outLength = wxMin(mInitialAllowedSilence +
(inLength - mInitialAllowedSilence) * mSilenceCompressPercent / 100.0,
mTruncLongestAllowedSilence);
}
double cutLen = inLength - outLength;
totalCutLen += cutLen;
TrackListIterator iterOut(mOutputTracks);
for (Track *t = iterOut.First(); t; t = iterOut.Next())
{
// Don't waste time past the end of a track
if (t->GetEndTime() < r->start)
{
continue;
}
if (t->GetKind() == Track::Wave && (
t->GetSelected() || t->IsSyncLockSelected()))
{
// In WaveTracks, clear with a cross-fade
WaveTrack *wt = (WaveTrack *)t;
sampleCount blendFrames = mBlendFrameCount;
double cutStart = (r->start + r->end - cutLen) / 2;
double cutEnd = cutStart + cutLen;
// Round start/end times to frame boundaries
cutStart = wt->LongSamplesToTime(wt->TimeToLongSamples(cutStart));
cutEnd = wt->LongSamplesToTime(wt->TimeToLongSamples(cutEnd));
// Make sure the cross-fade does not affect non-silent frames
if (wt->LongSamplesToTime(blendFrames) > inLength)
{
blendFrames = wt->TimeToLongSamples(inLength);
}
// Perform cross-fade in memory
float *buf1 = new float[blendFrames];
float *buf2 = new float[blendFrames];
sampleCount t1 = wt->TimeToLongSamples(cutStart) - blendFrames / 2;
sampleCount t2 = wt->TimeToLongSamples(cutEnd) - blendFrames / 2;
wt->Get((samplePtr)buf1, floatSample, t1, blendFrames);
wt->Get((samplePtr)buf2, floatSample, t2, blendFrames);
for (sampleCount i = 0; i < blendFrames; ++i)
{
buf1[i] = ((blendFrames-i) * buf1[i] + i * buf2[i]) /
(double)blendFrames;
}
// Perform the cut
wt->Clear(cutStart, cutEnd);
// Write cross-faded data
wt->Set((samplePtr)buf1, floatSample, t1, blendFrames);
delete [] buf1;
delete [] buf2;
}
else if (t->GetSelected() || t->IsSyncLockSelected())
{
// Non-wave tracks: just do a sync-lock adjust
double cutStart = (r->start + r->end - cutLen) / 2;
double cutEnd = cutStart + cutLen;
t->SyncLockAdjust(cutEnd, cutStart);
}
}
++whichReg;
}
mT1 -= totalCutLen;
ReplaceProcessedTracks(true);
return true;
}
bool EffectTruncSilence::Analyze(RegionList& silenceList,
RegionList& trackSilences,
WaveTrack* wt,
sampleCount* silentFrame,
sampleCount* index,
int whichTrack,
double* inputLength /*= NULL*/,
double* minInputLength /*= NULL*/)
{
// Smallest silent region to detect in frames
sampleCount minSilenceFrames = sampleCount(wxMax( mInitialAllowedSilence, DEF_MinTruncMs) * wt->GetRate());
double truncDbSilenceThreshold = Enums::Db2Signal[mTruncDbChoiceIndex];
sampleCount blockLen = wt->GetMaxBlockSize();
sampleCount start = wt->TimeToLongSamples(mT0);
sampleCount end = wt->TimeToLongSamples(mT1);
sampleCount outLength = 0;
double previewLength;
gPrefs->Read(wxT("/AudioIO/EffectsPreviewLen"), &previewLength, 6.0);
// Minimum required length in samples.
const sampleCount previewLen = previewLength * wt->GetRate();
// Keep position in overall silences list for optimization
RegionList::iterator rit(silenceList.begin());
// Allocate buffer
float *buffer = new float[blockLen];
// Loop through current track
while (*index < end) {
if (inputLength && ((outLength >= previewLen) || (*index - start > wt->TimeToLongSamples(*minInputLength)))) {
*inputLength = std::min<double>(*inputLength, *minInputLength);
if (outLength >= previewLen) {
*minInputLength = *inputLength;
}
return true;
}
if (!inputLength) {
// Show progress dialog, test for cancellation
bool cancelled = TotalProgress(
detectFrac * (whichTrack + (*index - start) / (double)(end - start)) /
(double)GetNumWaveTracks());
if (cancelled) {
delete [] buffer;
return false;
}
}
// Optimization: if not in a silent region skip ahead to the next one
double curTime = wt->LongSamplesToTime(*index);
for ( ; rit != silenceList.end(); ++rit) {
// Find the first silent region ending after current time
if ((*rit)->end >= curTime) {
break;
}
}
if (rit == silenceList.end()) {
// No more regions -- no need to process the rest of the track
if (inputLength) {
// Add available samples up to previewLength.
sampleCount remainingTrackSamples = wt->TimeToLongSamples(wt->GetEndTime()) - *index;
sampleCount requiredTrackSamples = previewLen - outLength;
outLength += (remainingTrackSamples > requiredTrackSamples)? requiredTrackSamples : remainingTrackSamples;
}
break;
}
else if ((*rit)->start > curTime) {
// End current silent region, skip ahead
if (*silentFrame >= minSilenceFrames) {
Region *r = new Region;
r->start = wt->LongSamplesToTime(*index - *silentFrame);
r->end = wt->LongSamplesToTime(*index);
trackSilences.push_back(r);
}
*silentFrame = 0;
sampleCount newIndex = wt->TimeToLongSamples((*rit)->start);
if (inputLength) {
sampleCount requiredTrackSamples = previewLen - outLength;
// Add non-silent sample to outLength
outLength += ((newIndex - *index) > requiredTrackSamples)? requiredTrackSamples : newIndex - *index;
}
*index = newIndex;
}
// End of optimization
// Limit size of current block if we've reached the end
sampleCount count = blockLen;
if ((*index + count) > end) {
count = end - *index;
}
// Fill buffer
wt->Get((samplePtr)(buffer), floatSample, *index, count);
// Look for silenceList in current block
for (sampleCount i = 0; i < count; ++i) {
if (inputLength && ((outLength >= previewLen) || (outLength > wt->TimeToLongSamples(*minInputLength)))) {
*inputLength = wt->LongSamplesToTime(*index + i) - wt->LongSamplesToTime(start);
break;
}
if (fabs(buffer[i]) < truncDbSilenceThreshold) {
(*silentFrame)++;
}
else {
sampleCount allowed = 0;
if (*silentFrame >= minSilenceFrames) {
if (inputLength) {
switch (mActionIndex) {
case kTruncate:
outLength += wt->TimeToLongSamples(mTruncLongestAllowedSilence);
break;
case kCompress:
allowed = wt->TimeToLongSamples(mInitialAllowedSilence);
outLength += allowed +
(*silentFrame - allowed) * mSilenceCompressPercent / 100.0;
break;
// default: // Not currently used.
}
}
// Record the silent region
Region *r = new Region;
r->start = wt->LongSamplesToTime(*index + i - *silentFrame);
r->end = wt->LongSamplesToTime(*index + i);
trackSilences.push_back(r);
}
else if (inputLength) { // included as part of non-silence
outLength += *silentFrame;
}
*silentFrame = 0;
if (inputLength) {
++outLength; // Add non-silent sample to outLength
}
}
}
// Next block
*index += count;
}
delete [] buffer;
if (inputLength) {
*inputLength = std::min<double>(*inputLength, *minInputLength);
if (outLength >= previewLen) {
*minInputLength = *inputLength;
}
}
return true;
}
void EffectTruncSilence::PopulateOrExchange(ShuttleGui & S)
{
wxASSERT(kNumActions == WXSIZEOF(kActionStrings));
wxArrayString actionChoices;
for (int i = 0; i < kNumActions; i++)
{
actionChoices.Add(wxGetTranslation(kActionStrings[i]));
}
S.AddSpace(0, 5);
S.StartStatic(_("Detect Silence"));
{
S.StartMultiColumn(3, wxALIGN_CENTER_HORIZONTAL);
{
// Threshold
mTruncDbChoice = S.AddChoice(_("Level:"), wxT(""), &mDbChoices);
mTruncDbChoice->SetValidator(wxGenericValidator(&mTruncDbChoiceIndex));
S.SetSizeHints(-1, -1);
S.AddSpace(0); // 'choices' already includes units.
// Ignored silence
FloatingPointValidator<double> vldDur(3, &mInitialAllowedSilence, NUM_VAL_NO_TRAILING_ZEROES);
vldDur.SetRange(MIN_Minimum, MAX_Minimum);
mInitialAllowedSilenceT = S.AddTextBox(_("Duration:"), wxT(""), 12);
mInitialAllowedSilenceT->SetValidator(vldDur);
S.AddUnits(wxT("seconds"));
}
S.EndMultiColumn();
}
S.EndStatic();
S.StartStatic(_("Action"));
{
S.StartHorizontalLay();
{
// Action choices
mActionChoice = S.AddChoice(wxT(""), wxT(""), &actionChoices);
mActionChoice->SetValidator(wxGenericValidator(&mActionIndex));
S.SetSizeHints(-1, -1);
}
S.EndHorizontalLay();
S.StartMultiColumn(3, wxALIGN_CENTER_HORIZONTAL);
{
// Truncation / Compression factor
FloatingPointValidator<double> vldTrunc(3, &mTruncLongestAllowedSilence, NUM_VAL_NO_TRAILING_ZEROES);
vldTrunc.SetRange(MIN_Truncate, MAX_Truncate);
mTruncLongestAllowedSilenceT = S.AddTextBox(_("Truncate to:"), wxT(""), 12);
mTruncLongestAllowedSilenceT->SetValidator(vldTrunc);
S.AddUnits(wxT("seconds"));
FloatingPointValidator<double> vldComp(3, &mSilenceCompressPercent, NUM_VAL_NO_TRAILING_ZEROES);
vldComp.SetRange(MIN_Compress, MAX_Compress);
mSilenceCompressPercentT = S.AddTextBox(_("Compress to:"), wxT(""), 12);
mSilenceCompressPercentT->SetValidator(vldComp);
S.AddUnits(wxT("percent"));
}
S.EndMultiColumn();
}
S.EndStatic();
UpdateUI();
}
bool EffectTruncSilence::TransferDataToWindow()
{
if (!mUIParent->TransferDataToWindow())
{
return false;
}
return true;
}
bool EffectTruncSilence::TransferDataFromWindow()
{
if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
{
return false;
}
return true;
}
// EffectTruncSilence implementation
// Finds the intersection of the ordered region lists, stores in dest
void EffectTruncSilence::Intersect(RegionList &dest, const RegionList &src)
{
RegionList::iterator destIter;
destIter = dest.begin();
// Any time we reach the end of the dest list we're finished
if (destIter == dest.end())
return;
Region *curDest = *destIter;
// Operation: find non-silent regions in src, remove them from dest.
double nsStart = curDest->start;
double nsEnd;
bool lastRun = false; // must run the loop one extra time
RegionList::const_iterator srcIter = src.begin();
// This logic, causing the loop to run once after end of src, must occur
// each time srcIter is updated
if (srcIter == src.end())
{
lastRun = true;
}
while (srcIter != src.end() || lastRun)
{
// Don't use curSrc unless lastRun is false!
Region *curSrc;
if (lastRun)
{
// The last non-silent region extends as far as possible
curSrc = NULL;
nsEnd = std::numeric_limits<double>::max();
}
else
{
curSrc = *srcIter;
nsEnd = curSrc->start;
}
if (nsEnd > nsStart)
{
// Increment through dest until we have a region that could be affected
while (curDest->end <= nsStart)
{
++destIter;
if (destIter == dest.end())
{
return;
}
curDest = *destIter;
}
// Check for splitting dest region in two
if (nsStart > curDest->start && nsEnd < curDest->end)
{
// The second region
Region *r = new Region;
r->start = nsEnd;
r->end = curDest->end;
// The first region
curDest->end = nsStart;
// Insert second region after first
RegionList::iterator nextIt(destIter);
++nextIt;
// This should just read: destIter = dest.insert(nextIt, r); but we
// work around two two wxList::insert() bugs. First, in some
// versions it returns the wrong value. Second, in some versions,
// it crashes when you insert at list end.
if (nextIt == dest.end())
{
dest.Append(r);
}
else
{
dest.insert(nextIt, r);
}
++destIter; // (now points at the newly-inserted region)
curDest = *destIter;
}
// Check for truncating the end of dest region
if (nsStart > curDest->start && nsStart < curDest->end &&
nsEnd >= curDest->end)
{
curDest->end = nsStart;
++destIter;
if (destIter == dest.end())
{
return;
}
curDest = *destIter;
}
// Check for all dest regions that need to be removed completely
while (nsStart <= curDest->start && nsEnd >= curDest->end)
{
destIter = dest.erase(destIter);
if (destIter == dest.end())
{
return;
}
curDest = *destIter;
}
// Check for truncating the beginning of dest region
if (nsStart <= curDest->start &&
nsEnd > curDest->start && nsEnd < curDest->end)
{
curDest->start = nsEnd;
}
}
if (lastRun)
{
// done
lastRun = false;
}
else
{
// Next non-silent region starts at the end of this silent region
nsStart = curSrc->end;
++srcIter;
if (srcIter == src.end())
{
lastRun = true;
}
}
}
}
void EffectTruncSilence::BlendFrames(float* buffer, int blendFrameCount, int leftIndex, int rightIndex)
{
float* bufOutput = &buffer[leftIndex];
float* bufBefore = &buffer[leftIndex];
float* bufAfter = &buffer[rightIndex];
double beforeFactor = 1.0;
double afterFactor = 0.0;
double adjFactor = 1.0 / (double)blendFrameCount;
for (int j = 0; j < blendFrameCount; ++j)
{
bufOutput[j] = (float)((bufBefore[j] * beforeFactor) + (bufAfter[j] * afterFactor));
beforeFactor -= adjFactor;
afterFactor += adjFactor;
}
}
void EffectTruncSilence::UpdateUI()
{
switch (mActionIndex)
{
case kTruncate:
mTruncLongestAllowedSilenceT->Enable(true);
mSilenceCompressPercentT->Enable(false);
break;
case kCompress:
mTruncLongestAllowedSilenceT->Enable(false);
mSilenceCompressPercentT->Enable(true);
}
}
void EffectTruncSilence::OnControlChange(wxCommandEvent & WXUNUSED(evt))
{
mActionChoice->GetValidator()->TransferFromWindow();
UpdateUI();
if (!EnableApply(mUIParent->TransferDataFromWindow()))
{
return;
}
}