audacia/src/effects/TruncSilence.cpp

1016 lines
31 KiB
C++
Raw Normal View History

/**********************************************************************
Audacity: A Digital Audio Editor
TruncSilence.cpp
Lynn Allan (from DM's Normalize)
Philip Van Baren (more options and boundary fixes)
*******************************************************************//**
\class EffectTruncSilence
2014-06-03 20:30:19 +00:00
\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 <algorithm>
2016-02-01 21:01:14 +00:00
#include <list>
#include <limits>
#include <math.h>
#include <wx/checkbox.h>
#include <wx/choice.h>
#include <wx/valgen.h>
#include "../Prefs.h"
2016-01-23 14:08:27 +00:00
#include "../Project.h"
#include "../ShuttleGui.h"
#include "../WaveTrack.h"
#include "../widgets/valnum.h"
#include "../widgets/ErrorDialog.h"
2018-03-27 00:30:31 +00:00
class Enums {
public:
static const size_t NumDbChoices;
static const double Db2Signal[];
static const wxString DbChoices[];
};
const wxString Enums::DbChoices[] = {
wxT("-20 dB"),
wxT("-25 dB"),
wxT("-30 dB"),
wxT("-35 dB"),
wxT("-40 dB"),
wxT("-45 dB"),
wxT("-50 dB"),
wxT("-55 dB"),
wxT("-60 dB"),
wxT("-65 dB"),
wxT("-70 dB"),
wxT("-75 dB"),
wxT("-80 dB")
};
const double Enums::Db2Signal[] =
// -20dB -25dB -30dB -35dB -40dB -45dB -50dB -55dB -60dB -65dB -70dB -75dB -80dB
{ 0.10000, 0.05620, 0.03160, 0.01780, 0.01000, 0.00562, 0.00316, 0.00178, 0.00100, 0.000562, 0.000316, 0.000178, 0.0001000 };
const size_t Enums::NumDbChoices = WXSIZEOF(Enums::DbChoices);
static_assert( Enums::NumDbChoices == WXSIZEOF( Enums::Db2Signal ),
"size mismatch" );
2016-02-01 21:01:14 +00:00
// Declaration of RegionList
class RegionList : public std::list < Region > {};
enum kActions
{
kTruncate,
kCompress,
nActions
};
static const wxChar *kActionStrings[nActions] =
{
XO("Truncate Detected Silence"),
XO("Compress Excess Silence")
};
static CommandParameters::ObsoleteMap kObsoleteActions[] = {
// Compatible with 2.1.0 and before
{ wxT("0"), 0 }, // Remap to Truncate Detected Silence
{ wxT("1"), 1 }, // Remap to Compress Excess Silence
};
static const size_t nObsoleteActions = WXSIZEOF( kObsoleteActions );
// 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, wxT("Db"), 0, 0, Enums::NumDbChoices - 1, 1 );
Param( ActIndex, int, wxT("Action"), kTruncate, 0, nActions - 1, 1 );
Param( Minimum, double, wxT("Minimum"), 0.5, 0.001, 10000.0, 1 );
Param( Truncate, double, wxT("Truncate"), 0.5, 0.0, 10000.0, 1 );
Param( Compress, double, wxT("Compress"), 50.0, 0.0, 99.9, 1 );
Param( Independent, bool, wxT("Independent"), false, false, true, 1 );
static const size_t 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;
BEGIN_EVENT_TABLE(EffectTruncSilence, wxEvtHandler)
EVT_CHOICE(wxID_ANY, EffectTruncSilence::OnControlChange)
EVT_TEXT(wxID_ANY, EffectTruncSilence::OnControlChange)
END_EVENT_TABLE()
EffectTruncSilence::EffectTruncSilence()
{
2018-03-27 00:30:31 +00:00
mDbChoices = wxArrayString(Enums::NumDbChoices, Enums::DbChoices);
mInitialAllowedSilence = DEF_Minimum;
mTruncLongestAllowedSilence = DEF_Truncate;
mSilenceCompressPercent = DEF_Compress;
mTruncDbChoiceIndex = DEF_DbIndex;
mActionIndex = DEF_ActIndex;
mbIndependent = DEF_Independent;
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 _("Automatically reduces the length of passages where the volume is below a specified level");
}
wxString EffectTruncSilence::ManualPage()
{
return wxT("Truncate_Silence");
}
Automation: AudacityCommand This is a squash of 50 commits. This merges the capabilities of BatchCommands and Effects using a new AudacityCommand class. AudacityCommand provides one function to specify the parameters, and then we leverage that one function in automation, whether by chains, mod-script-pipe or (future) Nyquist. - Now have AudacityCommand which is using the same mechanism as Effect - Has configurable parameters - Has data-entry GUI (built using shuttle GUI) - Registers with PluginManager. - Menu commands now provided in chains, and to python batch. - Tested with Zoom Toggle. - ShuttleParams now can set, get, set defaults, validate and specify the parameters. - Bugfix: Don't overwrite values with defaults first time out. - Add DefineParams function for all built-in effects. - Extend CommandContext to carry output channels for results. We abuse EffectsManager. It handles both Effects and AudacityCommands now. In time an Effect should become a special case of AudacityCommand and we'll split and rename the EffectManager class. - Don't use 'default' as a parameter name. - Massive renaming for CommandDefinitionInterface - EffectIdentInterface becomes EffectDefinitionInterface - EffectAutomationParameters becomes CommandAutomationParameters - PluginType is now a bit field. This way we can search for related types at the same time. - Most old batch commands made into AudacityCommands. The ones that weren't are for a reason. They are used by mod-script-pipe to carry commands and responses across from a non-GUI thread to the GUI thread. - Major tidy up of ScreenshotCommand - Reworking of SelectCommand - GetPreferenceCommand and SetPreferenceCommand - GetTrackInfo and SetTrackInfo - GetInfoCommand - Help, Open, Save, Import and Export commands. - Removed obsolete commands ExecMenu, GetProjectInfo and SetProjectInfo which are now better handled by other commands. - JSONify "GetInfo: Commands" output, i.e. commas in the right places. - General work on better Doxygen. - Lyrics -> LyricsPanel - Meter -> MeterPanel - Updated Linux makefile. - Scripting commands added into Extra menu. - Distinct names for previously duplicated find-clipping parameters. - Fixed longstanding error with erroneous status field number which previously caused an ASSERT in debug. - Sensible formatting of numbers in Chains, 0.1 not 0.1000000000137
2018-01-14 18:51:41 +00:00
// EffectDefinitionInterface implementation
EffectType EffectTruncSilence::GetType()
{
return EffectTypeProcess;
}
// EffectClientInterface implementation
Automation: AudacityCommand This is a squash of 50 commits. This merges the capabilities of BatchCommands and Effects using a new AudacityCommand class. AudacityCommand provides one function to specify the parameters, and then we leverage that one function in automation, whether by chains, mod-script-pipe or (future) Nyquist. - Now have AudacityCommand which is using the same mechanism as Effect - Has configurable parameters - Has data-entry GUI (built using shuttle GUI) - Registers with PluginManager. - Menu commands now provided in chains, and to python batch. - Tested with Zoom Toggle. - ShuttleParams now can set, get, set defaults, validate and specify the parameters. - Bugfix: Don't overwrite values with defaults first time out. - Add DefineParams function for all built-in effects. - Extend CommandContext to carry output channels for results. We abuse EffectsManager. It handles both Effects and AudacityCommands now. In time an Effect should become a special case of AudacityCommand and we'll split and rename the EffectManager class. - Don't use 'default' as a parameter name. - Massive renaming for CommandDefinitionInterface - EffectIdentInterface becomes EffectDefinitionInterface - EffectAutomationParameters becomes CommandAutomationParameters - PluginType is now a bit field. This way we can search for related types at the same time. - Most old batch commands made into AudacityCommands. The ones that weren't are for a reason. They are used by mod-script-pipe to carry commands and responses across from a non-GUI thread to the GUI thread. - Major tidy up of ScreenshotCommand - Reworking of SelectCommand - GetPreferenceCommand and SetPreferenceCommand - GetTrackInfo and SetTrackInfo - GetInfoCommand - Help, Open, Save, Import and Export commands. - Removed obsolete commands ExecMenu, GetProjectInfo and SetProjectInfo which are now better handled by other commands. - JSONify "GetInfo: Commands" output, i.e. commas in the right places. - General work on better Doxygen. - Lyrics -> LyricsPanel - Meter -> MeterPanel - Updated Linux makefile. - Scripting commands added into Extra menu. - Distinct names for previously duplicated find-clipping parameters. - Fixed longstanding error with erroneous status field number which previously caused an ASSERT in debug. - Sensible formatting of numbers in Chains, 0.1 not 0.1000000000137
2018-01-14 18:51:41 +00:00
bool EffectTruncSilence::DefineParams( ShuttleParams & S ){
wxArrayString actions(nActions, kActionStrings);
Automation: AudacityCommand This is a squash of 50 commits. This merges the capabilities of BatchCommands and Effects using a new AudacityCommand class. AudacityCommand provides one function to specify the parameters, and then we leverage that one function in automation, whether by chains, mod-script-pipe or (future) Nyquist. - Now have AudacityCommand which is using the same mechanism as Effect - Has configurable parameters - Has data-entry GUI (built using shuttle GUI) - Registers with PluginManager. - Menu commands now provided in chains, and to python batch. - Tested with Zoom Toggle. - ShuttleParams now can set, get, set defaults, validate and specify the parameters. - Bugfix: Don't overwrite values with defaults first time out. - Add DefineParams function for all built-in effects. - Extend CommandContext to carry output channels for results. We abuse EffectsManager. It handles both Effects and AudacityCommands now. In time an Effect should become a special case of AudacityCommand and we'll split and rename the EffectManager class. - Don't use 'default' as a parameter name. - Massive renaming for CommandDefinitionInterface - EffectIdentInterface becomes EffectDefinitionInterface - EffectAutomationParameters becomes CommandAutomationParameters - PluginType is now a bit field. This way we can search for related types at the same time. - Most old batch commands made into AudacityCommands. The ones that weren't are for a reason. They are used by mod-script-pipe to carry commands and responses across from a non-GUI thread to the GUI thread. - Major tidy up of ScreenshotCommand - Reworking of SelectCommand - GetPreferenceCommand and SetPreferenceCommand - GetTrackInfo and SetTrackInfo - GetInfoCommand - Help, Open, Save, Import and Export commands. - Removed obsolete commands ExecMenu, GetProjectInfo and SetProjectInfo which are now better handled by other commands. - JSONify "GetInfo: Commands" output, i.e. commas in the right places. - General work on better Doxygen. - Lyrics -> LyricsPanel - Meter -> MeterPanel - Updated Linux makefile. - Scripting commands added into Extra menu. - Distinct names for previously duplicated find-clipping parameters. - Fixed longstanding error with erroneous status field number which previously caused an ASSERT in debug. - Sensible formatting of numbers in Chains, 0.1 not 0.1000000000137
2018-01-14 18:51:41 +00:00
S.SHUTTLE_ENUM_PARAM( mTruncDbChoiceIndex, DbIndex, mDbChoices );
S.SHUTTLE_ENUM_PARAM( mActionIndex, ActIndex, actions );
S.SHUTTLE_PARAM( mInitialAllowedSilence, Minimum );
S.SHUTTLE_PARAM( mTruncLongestAllowedSilence, Truncate );
S.SHUTTLE_PARAM( mSilenceCompressPercent, Compress );
S.SHUTTLE_PARAM( mbIndependent, Independent );
return true;
}
2018-02-21 14:24:25 +00:00
bool EffectTruncSilence::GetAutomationParameters(CommandParameters & 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);
parms.Write(KEY_Independent, mbIndependent);
return true;
}
2018-02-21 14:24:25 +00:00
bool EffectTruncSilence::SetAutomationParameters(CommandParameters & parms)
{
wxArrayString actions(nActions, kActionStrings);
ReadAndVerifyDouble(Minimum);
ReadAndVerifyDouble(Truncate);
ReadAndVerifyDouble(Compress);
ReadAndVerifyEnum(DbIndex, mDbChoices);
ReadAndVerifyEnumWithObsoletes(ActIndex, actions,
kObsoleteActions, nObsoleteActions);
ReadAndVerifyBool(Independent);
mInitialAllowedSilence = Minimum;
mTruncLongestAllowedSilence = Truncate;
mSilenceCompressPercent = Compress;
mTruncDbChoiceIndex = DbIndex;
mActionIndex = ActIndex;
mbIndependent = Independent;
return true;
}
// Effect implementation
double EffectTruncSilence::CalcPreviewInputLength(double /* previewLength */)
{
double inputLength = mT1 - mT0;
double minInputLength = inputLength;
// Master list of silent regions
RegionList silences;
// Start with the whole selection silent
2016-02-01 21:01:14 +00:00
silences.push_back(Region(mT0, mT1));
2016-12-29 15:36:37 +00:00
SelectedTrackListOfKindIterator iter(Track::Wave, inputTracks());
int whichTrack = 0;
for (Track *t = iter.First(); t; t = iter.Next()) {
WaveTrack *const wt = static_cast<WaveTrack *>(t);
RegionList trackSilences;
auto 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()
2014-06-03 20:30:19 +00:00
{
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()
{
const bool success =
mbIndependent
? ProcessIndependently()
: ProcessAll();
if (success)
ReplaceProcessedTracks(true);
return success;
}
bool EffectTruncSilence::ProcessIndependently()
{
unsigned nGroups = 0;
2016-01-23 14:08:27 +00:00
const bool syncLock = ::GetActiveProject()->IsSyncLocked();
// Check if it's permissible
{
2016-12-29 15:36:37 +00:00
SelectedTrackListOfKindIterator iter(Track::Wave, inputTracks());
for (Track *track = iter.First(); track;
track = iter.Next(true) // skip linked tracks
) {
2016-01-23 14:08:27 +00:00
if (syncLock) {
Track *const link = track->GetLink();
2016-12-29 15:36:37 +00:00
SyncLockedTracksIterator syncIter(inputTracks());
for (Track *track2 = syncIter.StartWith(track); track2; track2 = syncIter.Next()) {
2016-01-23 14:08:27 +00:00
if (track2->GetKind() == Track::Wave &&
!(track2 == track || track2 == link) &&
track2->GetSelected()) {
::Effect::MessageBox(_("When truncating independently, there may only be one selected audio track in each Sync-Locked Track Group."));
2016-01-23 14:08:27 +00:00
return false;
}
}
}
++nGroups;
}
}
if (nGroups == 0)
// nothing to do
return true;
// Now do the work
// Copy tracks
CopyInputTracks(Track::All);
double newT1 = 0.0;
{
unsigned iGroup = 0;
SelectedTrackListOfKindIterator iter(Track::Wave, mOutputTracks.get());
for (Track *track = iter.First(); track;
++iGroup, track = iter.Next(true) // skip linked tracks
) {
Track *const link = track->GetLink();
Track *const last = link ? link : track;
RegionList silences;
if (!FindSilences(silences, mOutputTracks.get(), track, last))
return false;
// Treat tracks in the sync lock group only
2016-01-23 14:08:27 +00:00
Track *groupFirst, *groupLast;
if (syncLock) {
SyncLockedTracksIterator syncIter(mOutputTracks.get());
groupFirst = syncIter.StartWith(track);
2016-01-29 15:14:52 +00:00
groupLast = syncIter.Last();
2016-01-23 14:08:27 +00:00
}
else {
groupFirst = track;
groupLast = last;
}
double totalCutLen = 0.0;
2016-01-23 14:08:27 +00:00
if (!DoRemoval(silences, iGroup, nGroups, groupFirst, groupLast, totalCutLen))
return false;
newT1 = std::max(newT1, mT1 - totalCutLen);
}
}
mT1 = newT1;
return true;
}
bool EffectTruncSilence::ProcessAll()
{
// Copy tracks
CopyInputTracks(Track::All);
2016-02-01 21:01:14 +00:00
// Master list of silent regions.
// This list should always be kept in order.
RegionList silences;
2016-12-29 15:36:37 +00:00
SelectedTrackListOfKindIterator iter(Track::Wave, inputTracks());
if (FindSilences(silences, inputTracks(), iter.First(), iter.Last())) {
TrackListIterator iterOut(mOutputTracks.get());
double totalCutLen = 0.0;
Track *const first = iterOut.First();
if (DoRemoval(silences, 0, 1, first, iterOut.Last(), totalCutLen)) {
mT1 -= totalCutLen;
return true;
}
}
return false;
}
bool EffectTruncSilence::FindSilences
(RegionList &silences, TrackList *list, Track *firstTrack, Track *lastTrack)
{
// Start with the whole selection silent
2016-02-01 21:01:14 +00:00
silences.push_back(Region(mT0, mT1));
// Remove non-silent regions in each track
SelectedTrackListOfKindIterator iter(Track::Wave, list);
int whichTrack = 0;
bool lastSeen = false;
for (Track *t = iter.StartWith(firstTrack); !lastSeen && t; t = iter.Next())
{
lastSeen = (t == lastTrack);
WaveTrack *const wt = static_cast<WaveTrack *>(t);
// Smallest silent region to detect in frames
auto minSilenceFrames =
sampleCount(std::max(mInitialAllowedSilence, DEF_MinTruncMs) * wt->GetRate());
//
// Scan the track for silences
//
2010-09-03 05:39:17 +00:00
RegionList trackSilences;
auto index = wt->TimeToLongSamples(mT0);
sampleCount silentFrame = 0;
// Detect silences
bool cancelled = !(Analyze(silences, trackSilences, wt, &silentFrame, &index, whichTrack));
2010-09-03 05:39:17 +00:00
// Buffer has been freed, so we're OK to return if cancelled
2014-06-03 20:30:19 +00:00
if (cancelled)
2010-09-07 23:37:45 +00:00
{
ReplaceProcessedTracks(false);
return false;
}
if (silentFrame >= minSilenceFrames)
{
// Track ended in silence -- record region
2016-02-01 21:01:14 +00:00
trackSilences.push_back(Region(
wt->LongSamplesToTime(index - silentFrame),
wt->LongSamplesToTime(index)
));
}
// Intersect with the overall silent region list
Intersect(silences, trackSilences);
whichTrack++;
}
return true;
}
bool EffectTruncSilence::DoRemoval
(const RegionList &silences, unsigned iGroup, unsigned nGroups, Track *firstTrack, Track *lastTrack,
double &totalCutLen)
{
//
// 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::const_reverse_iterator rit;
for (rit = silences.rbegin(); rit != silences.rend(); ++rit)
{
2016-02-01 21:01:14 +00:00
const Region &region = *rit;
const Region *const r = &region;
2010-09-07 23:37:45 +00:00
// Progress dialog and cancellation. Do additional cleanup before return.
const double frac = detectFrac +
(1 - detectFrac) * (iGroup + whichReg / double(silences.size())) / nGroups;
if (TotalProgress(frac))
{
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 = std::min(mTruncLongestAllowedSilence, inLength);
break;
case kCompress:
outLength = mInitialAllowedSilence +
(inLength - mInitialAllowedSilence) * mSilenceCompressPercent / 100.0;
break;
default: // Not currently used.
outLength = std::min(mInitialAllowedSilence +
(inLength - mInitialAllowedSilence) * mSilenceCompressPercent / 100.0,
mTruncLongestAllowedSilence);
}
double cutLen = std::max(0.0, inLength - outLength);
totalCutLen += cutLen;
TrackListIterator iterOut(mOutputTracks.get());
bool lastSeen = false;
for (Track *t = iterOut.StartWith(firstTrack); t && !lastSeen; t = iterOut.Next())
{
lastSeen = (t == lastTrack);
if (!(t->GetSelected() || t->IsSyncLockSelected()))
continue;
// Don't waste time past the end of a track
if (t->GetEndTime() < r->start)
continue;
// Don't waste time cutting nothing.
if( cutLen == 0.0 )
continue;
double cutStart = (r->start + r->end - cutLen) / 2;
double cutEnd = cutStart + cutLen;
if (t->GetKind() == Track::Wave)
{
// In WaveTracks, clear with a cross-fade
WaveTrack *const wt = static_cast<WaveTrack*>(t);
auto blendFrames = mBlendFrameCount;
// 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)
{
// Result is not more than blendFrames:
blendFrames = wt->TimeToLongSamples(inLength).as_size_t();
}
// Perform cross-fade in memory
2016-04-14 16:35:15 +00:00
Floats buf1{ blendFrames };
Floats buf2{ blendFrames };
auto t1 = wt->TimeToLongSamples(cutStart) - blendFrames / 2;
auto t2 = wt->TimeToLongSamples(cutEnd) - blendFrames / 2;
2016-04-14 16:35:15 +00:00
wt->Get((samplePtr)buf1.get(), floatSample, t1, blendFrames);
wt->Get((samplePtr)buf2.get(), floatSample, t2, blendFrames);
for (decltype(blendFrames) 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
2016-04-14 16:35:15 +00:00
wt->Set((samplePtr)buf1.get(), floatSample, t1, blendFrames);
}
else
// Non-wave tracks: just do a sync-lock adjust
t->SyncLockAdjust(cutEnd, cutStart);
}
++whichReg;
}
return true;
}
bool EffectTruncSilence::Analyze(RegionList& silenceList,
RegionList& trackSilences,
2017-02-22 19:23:35 +00:00
const WaveTrack *wt,
sampleCount* silentFrame,
sampleCount* index,
int whichTrack,
double* inputLength /*= NULL*/,
double* minInputLength /*= NULL*/)
{
// Smallest silent region to detect in frames
auto minSilenceFrames = sampleCount(std::max( mInitialAllowedSilence, DEF_MinTruncMs) * wt->GetRate());
double truncDbSilenceThreshold = Enums::Db2Signal[mTruncDbChoiceIndex];
auto blockLen = wt->GetMaxBlockSize();
auto start = wt->TimeToLongSamples(mT0);
auto 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
2016-04-14 16:35:15 +00:00
Floats buffer{ 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).as_double() /
(end - start).as_double()) /
(double)GetNumWaveTracks());
2016-04-14 16:35:15 +00:00
if (cancelled)
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
2016-02-01 21:01:14 +00:00
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.
auto remainingTrackSamples = wt->TimeToLongSamples(wt->GetEndTime()) - *index;
auto requiredTrackSamples = previewLen - outLength;
outLength += (remainingTrackSamples > requiredTrackSamples)? requiredTrackSamples : remainingTrackSamples;
}
break;
}
2016-02-01 21:01:14 +00:00
else if (rit->start > curTime) {
// End current silent region, skip ahead
if (*silentFrame >= minSilenceFrames) {
2016-02-01 21:01:14 +00:00
trackSilences.push_back(Region(
wt->LongSamplesToTime(*index - *silentFrame),
wt->LongSamplesToTime(*index)
));
}
*silentFrame = 0;
auto newIndex = wt->TimeToLongSamples(rit->start);
if (inputLength) {
auto 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
2016-08-24 16:13:53 +00:00
auto count = limitSampleBufferSize( blockLen, end - *index );
// Fill buffer
2016-04-14 16:35:15 +00:00
wt->Get((samplePtr)(buffer.get()), floatSample, *index, count);
// Look for silenceList in current block
2016-08-24 16:13:53 +00:00
for (decltype(count) 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 += sampleCount(
allowed.as_double() +
(*silentFrame - allowed).as_double()
* mSilenceCompressPercent / 100.0
);
break;
// default: // Not currently used.
}
}
// Record the silent region
2016-02-01 21:01:14 +00:00
trackSilences.push_back(Region(
wt->LongSamplesToTime(*index + i - *silentFrame),
wt->LongSamplesToTime(*index + i)
));
}
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;
}
if (inputLength) {
*inputLength = std::min<double>(*inputLength, *minInputLength);
if (outLength >= previewLen) {
*minInputLength = *inputLength;
}
}
return true;
}
void EffectTruncSilence::PopulateOrExchange(ShuttleGui & S)
{
wxASSERT(nActions == WXSIZEOF(kActionStrings));
wxArrayString actionChoices;
for (int i = 0; i < nActions; 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, NumValidatorStyle::NO_TRAILING_ZEROES);
vldDur.SetRange(MIN_Minimum, MAX_Minimum);
mInitialAllowedSilenceT = S.AddTextBox(_("Duration:"), wxT(""), 12);
mInitialAllowedSilenceT->SetValidator(vldDur);
S.AddUnits(_("seconds"));
}
S.EndMultiColumn();
}
S.EndStatic();
S.StartStatic(_("Action"));
{
S.StartHorizontalLay();
{
// Action choices
mActionChoice = S.AddChoice( {}, 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, NumValidatorStyle::NO_TRAILING_ZEROES);
vldTrunc.SetRange(MIN_Truncate, MAX_Truncate);
mTruncLongestAllowedSilenceT = S.AddTextBox(_("Truncate to:"), wxT(""), 12);
mTruncLongestAllowedSilenceT->SetValidator(vldTrunc);
S.AddUnits(_("seconds"));
FloatingPointValidator<double> vldComp(3, &mSilenceCompressPercent, NumValidatorStyle::NO_TRAILING_ZEROES);
vldComp.SetRange(MIN_Compress, MAX_Compress);
mSilenceCompressPercentT = S.AddTextBox(_("Compress to:"), wxT(""), 12);
mSilenceCompressPercentT->SetValidator(vldComp);
S.AddUnits(_("%"));
}
S.EndMultiColumn();
S.StartMultiColumn(2, wxALIGN_CENTER_HORIZONTAL);
{
mIndependent = S.AddCheckBox(_("Truncate tracks independently"),
mbIndependent ? wxT("true") : wxT("false"));
}
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;
}
mbIndependent = mIndependent->IsChecked();
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;
2016-02-01 21:01:14 +00:00
RegionList::iterator 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();
2014-06-03 20:30:19 +00:00
// 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!
2016-02-01 21:01:14 +00:00
RegionList::const_iterator curSrc;
if (lastRun)
{
// The last non-silent region extends as far as possible
nsEnd = std::numeric_limits<double>::max();
}
else
{
2016-02-01 21:01:14 +00:00
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;
}
2016-02-01 21:01:14 +00:00
curDest = destIter;
}
// Check for splitting dest region in two
if (nsStart > curDest->start && nsEnd < curDest->end)
{
// The second region
2016-02-01 21:01:14 +00:00
Region r(nsEnd, curDest->end);
// The first region
curDest->end = nsStart;
// Insert second region after first
RegionList::iterator nextIt(destIter);
++nextIt;
2014-06-03 20:30:19 +00:00
// 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())
2016-02-01 21:01:14 +00:00
dest.push_back(r);
else
dest.insert(nextIt, r);
++destIter; // (now points at the newly-inserted region)
2016-02-01 21:01:14 +00:00
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;
}
2016-02-01 21:01:14 +00:00
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;
}
2016-02-01 21:01:14 +00:00
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;
}
}