audacia/src/WaveClip.cpp
Paul Licameli 906e55f047 Experimental.h in all .h or .cpp files that directly use EXPERIMENTALs...
... except Audacity.h; and in no others.

Do so even if Experimental.h gets multiply included, as in both the .h and
.cpp files.

This makes it easier to do a text scan to be sure there are no unintended quiet
changes of meaning because of omission of Experimental.h when the flag is
an enabled one.

Also move inclusions of Experimental.h earlier.

Also don't require Experimental.h to be preceded by Audacity.h to define
EXPERIMENTAL_MIDI_OUT correctly.
2019-03-17 22:54:00 -04:00

1996 lines
61 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
WaveClip.cpp
?? Dominic Mazzoni
?? Markus Meyer
*******************************************************************//**
\class WaveClip
\brief This allows multiple clips to be a part of one WaveTrack.
*//****************************************************************//**
\class WaveCache
\brief Cache used with WaveClip to cache wave information (for drawing).
*//*******************************************************************/
#include "WaveClip.h"
#include "Experimental.h"
#include <math.h>
#include "MemoryX.h"
#include <functional>
#include <vector>
#include <wx/log.h>
#include "Sequence.h"
#include "Spectrum.h"
#include "Prefs.h"
#include "Envelope.h"
#include "Resample.h"
#include "Project.h"
#include "WaveTrack.h"
#include "FFT.h"
#include "Profiler.h"
#include "InconsistencyException.h"
#include "UserException.h"
#include "prefs/SpectrogramSettings.h"
#include <wx/listimpl.cpp>
#ifdef _OPENMP
#include <omp.h>
#endif
class WaveCache {
public:
WaveCache()
: dirty(-1)
, start(-1)
, pps(0)
, rate(-1)
, where(0)
, min(0)
, max(0)
, rms(0)
, bl(0)
, numODPixels(0)
{
}
WaveCache(size_t len_, double pixelsPerSecond, double rate_, double t0, int dirty_)
: dirty(dirty_)
, len(len_)
, start(t0)
, pps(pixelsPerSecond)
, rate(rate_)
, where(1 + len)
, min(len)
, max(len)
, rms(len)
, bl(len)
, numODPixels(0)
{
//find the number of OD pixels - the only way to do this is by recounting since we've lost some old cache.
numODPixels = CountODPixels(0, len);
}
~WaveCache()
{
ClearInvalidRegions();
}
int dirty;
const size_t len { 0 }; // counts pixels, not samples
const double start;
const double pps;
const int rate;
std::vector<sampleCount> where;
std::vector<float> min;
std::vector<float> max;
std::vector<float> rms;
std::vector<int> bl;
int numODPixels;
class InvalidRegion
{
public:
InvalidRegion(size_t s, size_t e)
: start(s), end(e)
{}
//start and end pixel count. (not samples)
size_t start;
size_t end;
};
//Thread safe call to add a NEW region to invalidate. If it overlaps with other regions, it unions the them.
void AddInvalidRegion(sampleCount sampleStart, sampleCount sampleEnd)
{
//use pps to figure out where we are. (pixels per second)
if(pps ==0)
return;
double samplesPerPixel = rate/pps;
//rate is SR, start is first time of the waveform (in second) on cache
long invalStart = (sampleStart.as_double() - start*rate) / samplesPerPixel ;
long invalEnd = (sampleEnd.as_double() - start*rate)/samplesPerPixel +1; //we should cover the end..
//if they are both off the cache boundary in the same direction, the cache is missed,
//so we are safe, and don't need to track this one.
if((invalStart<0 && invalEnd <0) || (invalStart>=(long)len && invalEnd >= (long)len))
return;
//in all other cases, we need to clip the boundries so they make sense with the cache.
//for some reason, the cache is set up to access up to array[len], not array[len-1]
if(invalStart <0)
invalStart =0;
else if(invalStart > (long)len)
invalStart = len;
if(invalEnd <0)
invalEnd =0;
else if(invalEnd > (long)len)
invalEnd = len;
ODLocker locker(&mRegionsMutex);
//look thru the region array for a place to insert. We could make this more spiffy than a linear search
//but right now it is not needed since there will usually only be one region (which grows) for OD loading.
bool added=false;
if(mRegions.size())
{
for(size_t i=0;i<mRegions.size();i++)
{
//if the regions intersect OR are pixel adjacent
InvalidRegion &region = mRegions[i];
if((long)region.start <= (invalEnd+1)
&& ((long)region.end + 1) >= invalStart)
{
//take the union region
if((long)region.start > invalStart)
region.start = invalStart;
if((long)region.end < invalEnd)
region.end = invalEnd;
added=true;
break;
}
//this bit doesn't make sense because it assumes we add in order - now we go backwards after the initial OD finishes
// //this array is sorted by start/end points and has no overlaps. If we've passed all possible intersections, insert. The array will remain sorted.
// if(region.end < invalStart)
// {
// mRegions.insert(
// mRegions.begin() + i,
// InvalidRegion{ invalStart, invalEnd }
// );
// break;
// }
}
}
if(!added)
{
InvalidRegion newRegion(invalStart, invalEnd);
mRegions.insert(mRegions.begin(), newRegion);
}
//now we must go and patch up all the regions that overlap. Overlapping regions will be adjacent.
for(size_t i=1;i<mRegions.size();i++)
{
//if the regions intersect OR are pixel adjacent
InvalidRegion &region = mRegions[i];
InvalidRegion &prevRegion = mRegions[i - 1];
if(region.start <= prevRegion.end+1
&& region.end + 1 >= prevRegion.start)
{
//take the union region
if(region.start > prevRegion.start)
region.start = prevRegion.start;
if(region.end < prevRegion.end)
region.end = prevRegion.end;
mRegions.erase(mRegions.begin()+i-1);
//musn't forget to reset cursor
i--;
}
//if we are past the end of the region we added, we are past the area of regions that might be oversecting.
if(invalEnd < 0 || (long)region.start > invalEnd)
{
break;
}
}
}
//lock before calling these in a section. unlock after finished.
int GetNumInvalidRegions() const {return mRegions.size();}
size_t GetInvalidRegionStart(int i) const {return mRegions[i].start;}
size_t GetInvalidRegionEnd(int i) const {return mRegions[i].end;}
void ClearInvalidRegions()
{
mRegions.clear();
}
void LoadInvalidRegion(int ii, Sequence *sequence, bool updateODCount)
{
const auto invStart = GetInvalidRegionStart(ii);
const auto invEnd = GetInvalidRegionEnd(ii);
//before check number of ODPixels
int regionODPixels = 0;
if (updateODCount)
regionODPixels = CountODPixels(invStart, invEnd);
sequence->GetWaveDisplay(&min[invStart],
&max[invStart],
&rms[invStart],
&bl[invStart],
invEnd - invStart,
&where[invStart]);
//after check number of ODPixels
if (updateODCount)
{
const int regionODPixelsAfter = CountODPixels(invStart, invEnd);
numODPixels -= (regionODPixels - regionODPixelsAfter);
}
}
void LoadInvalidRegions(Sequence *sequence, bool updateODCount)
{
//invalid regions are kept in a sorted array.
for (int i = 0; i < GetNumInvalidRegions(); i++)
LoadInvalidRegion(i, sequence, updateODCount);
}
int CountODPixels(size_t startIn, size_t endIn)
{
using namespace std;
const int *begin = &bl[0];
return count_if(begin + startIn, begin + endIn, bind2nd(less<int>(), 0));
}
protected:
std::vector<InvalidRegion> mRegions;
ODLock mRegionsMutex;
};
static void ComputeSpectrumUsingRealFFTf
(float * __restrict buffer, const FFTParam *hFFT,
const float * __restrict window, size_t len, float * __restrict out)
{
size_t i;
if(len > hFFT->Points * 2)
len = hFFT->Points * 2;
for(i = 0; i < len; i++)
buffer[i] *= window[i];
for( ; i < (hFFT->Points * 2); i++)
buffer[i] = 0; // zero pad as needed
RealFFTf(buffer, hFFT);
// Handle the (real-only) DC
float power = buffer[0] * buffer[0];
if(power <= 0)
out[0] = -160.0;
else
out[0] = 10.0 * log10f(power);
for(i = 1; i < hFFT->Points; i++) {
const int index = hFFT->BitReversed[i];
const float re = buffer[index], im = buffer[index + 1];
power = re * re + im * im;
if(power <= 0)
out[i] = -160.0;
else
out[i] = 10.0*log10f(power);
}
}
WaveClip::WaveClip(const std::shared_ptr<DirManager> &projDirManager,
sampleFormat format, int rate, int colourIndex)
{
mRate = rate;
mColourIndex = colourIndex;
mSequence = std::make_unique<Sequence>(projDirManager, format);
mEnvelope = std::make_unique<Envelope>(true, 1e-7, 2.0, 1.0);
mWaveCache = std::make_unique<WaveCache>();
mSpecCache = std::make_unique<SpecCache>();
mSpecPxCache = std::make_unique<SpecPxCache>(1);
}
WaveClip::WaveClip(const WaveClip& orig,
const std::shared_ptr<DirManager> &projDirManager,
bool copyCutlines)
{
// essentially a copy constructor - but you must pass in the
// current project's DirManager, because we might be copying
// from one project to another
mOffset = orig.mOffset;
mRate = orig.mRate;
mColourIndex = orig.mColourIndex;
mSequence = std::make_unique<Sequence>(*orig.mSequence, projDirManager);
mEnvelope = std::make_unique<Envelope>(*orig.mEnvelope);
mWaveCache = std::make_unique<WaveCache>();
mSpecCache = std::make_unique<SpecCache>();
mSpecPxCache = std::make_unique<SpecPxCache>(1);
if ( copyCutlines )
for (const auto &clip: orig.mCutLines)
mCutLines.push_back
( std::make_unique<WaveClip>( *clip, projDirManager, true ) );
mIsPlaceholder = orig.GetIsPlaceholder();
}
WaveClip::WaveClip(const WaveClip& orig,
const std::shared_ptr<DirManager> &projDirManager,
bool copyCutlines,
double t0, double t1)
{
// Copy only a range of the other WaveClip
mOffset = orig.mOffset;
mRate = orig.mRate;
mColourIndex = orig.mColourIndex;
mWaveCache = std::make_unique<WaveCache>();
mSpecCache = std::make_unique<SpecCache>();
mSpecPxCache = std::make_unique<SpecPxCache>(1);
mIsPlaceholder = orig.GetIsPlaceholder();
sampleCount s0, s1;
orig.TimeToSamplesClip(t0, &s0);
orig.TimeToSamplesClip(t1, &s1);
mSequence = orig.mSequence->Copy(s0, s1);
mEnvelope = std::make_unique<Envelope>(
*orig.mEnvelope,
mOffset + s0.as_double()/mRate,
mOffset + s1.as_double()/mRate
);
if ( copyCutlines )
// Copy cutline clips that fall in the range
for (const auto &ppClip : orig.mCutLines)
{
const WaveClip* clip = ppClip.get();
double cutlinePosition = orig.mOffset + clip->GetOffset();
if (cutlinePosition >= t0 && cutlinePosition <= t1)
{
auto newCutLine =
std::make_unique< WaveClip >( *clip, projDirManager, true );
newCutLine->SetOffset( cutlinePosition - t0 );
mCutLines.push_back(std::move(newCutLine));
}
}
}
WaveClip::~WaveClip()
{
}
void WaveClip::SetOffset(double offset)
// NOFAIL-GUARANTEE
{
mOffset = offset;
mEnvelope->SetOffset(mOffset);
}
bool WaveClip::GetSamples(samplePtr buffer, sampleFormat format,
sampleCount start, size_t len, bool mayThrow) const
{
return mSequence->Get(buffer, format, start, len, mayThrow);
}
void WaveClip::SetSamples(samplePtr buffer, sampleFormat format,
sampleCount start, size_t len)
// STRONG-GUARANTEE
{
// use STRONG-GUARANTEE
mSequence->SetSamples(buffer, format, start, len);
// use NOFAIL-GUARANTEE
MarkChanged();
}
BlockArray* WaveClip::GetSequenceBlockArray()
{
return &mSequence->GetBlockArray();
}
double WaveClip::GetStartTime() const
{
// JS: mOffset is the minimum value and it is returned; no clipping to 0
return mOffset;
}
double WaveClip::GetEndTime() const
{
auto numSamples = mSequence->GetNumSamples();
double maxLen = mOffset + (numSamples+mAppendBufferLen).as_double()/mRate;
// JS: calculated value is not the length;
// it is a maximum value and can be negative; no clipping to 0
return maxLen;
}
sampleCount WaveClip::GetStartSample() const
{
return sampleCount( floor(mOffset * mRate + 0.5) );
}
sampleCount WaveClip::GetEndSample() const
{
return GetStartSample() + mSequence->GetNumSamples();
}
sampleCount WaveClip::GetNumSamples() const
{
return mSequence->GetNumSamples();
}
bool WaveClip::WithinClip(double t) const
{
auto ts = (sampleCount)floor(t * mRate + 0.5);
return ts > GetStartSample() && ts < GetEndSample() + mAppendBufferLen;
}
bool WaveClip::BeforeClip(double t) const
{
auto ts = (sampleCount)floor(t * mRate + 0.5);
return ts <= GetStartSample();
}
bool WaveClip::AfterClip(double t) const
{
auto ts = (sampleCount)floor(t * mRate + 0.5);
return ts >= GetEndSample() + mAppendBufferLen;
}
///Delete the wave cache - force redraw. Thread-safe
void WaveClip::ClearWaveCache()
{
ODLocker locker(&mWaveCacheMutex);
mWaveCache = std::make_unique<WaveCache>();
}
///Adds an invalid region to the wavecache so it redraws that portion only.
void WaveClip::AddInvalidRegion(sampleCount startSample, sampleCount endSample)
{
ODLocker locker(&mWaveCacheMutex);
if(mWaveCache!=NULL)
mWaveCache->AddInvalidRegion(startSample,endSample);
}
namespace {
inline
void findCorrection(const std::vector<sampleCount> &oldWhere, size_t oldLen,
size_t newLen,
double t0, double rate, double samplesPerPixel,
int &oldX0, double &correction)
{
// Mitigate the accumulation of location errors
// in copies of copies of ... of caches.
// Look at the loop that populates "where" below to understand this.
// Find the sample position that is the origin in the old cache.
const double oldWhere0 = oldWhere[1].as_double() - samplesPerPixel;
const double oldWhereLast = oldWhere0 + oldLen * samplesPerPixel;
// Find the length in samples of the old cache.
const double denom = oldWhereLast - oldWhere0;
// What sample would go in where[0] with no correction?
const double guessWhere0 = t0 * rate;
if ( // Skip if old and NEW are disjoint:
oldWhereLast <= guessWhere0 ||
guessWhere0 + newLen * samplesPerPixel <= oldWhere0 ||
// Skip unless denom rounds off to at least 1.
denom < 0.5)
{
// The computation of oldX0 in the other branch
// may underflow and the assertion would be violated.
oldX0 = oldLen;
correction = 0.0;
}
else
{
// What integer position in the old cache array does that map to?
// (even if it is out of bounds)
oldX0 = floor(0.5 + oldLen * (guessWhere0 - oldWhere0) / denom);
// What sample count would the old cache have put there?
const double where0 = oldWhere0 + double(oldX0) * samplesPerPixel;
// What correction is needed to align the NEW cache with the old?
const double correction0 = where0 - guessWhere0;
correction = std::max(-samplesPerPixel, std::min(samplesPerPixel, correction0));
wxASSERT(correction == correction0);
}
}
inline void
fillWhere(std::vector<sampleCount> &where, size_t len, double bias, double correction,
double t0, double rate, double samplesPerPixel)
{
// Be careful to make the first value non-negative
const double w0 = 0.5 + correction + bias + t0 * rate;
where[0] = sampleCount( std::max(0.0, floor(w0)) );
for (decltype(len) x = 1; x < len + 1; x++)
where[x] = sampleCount( floor(w0 + double(x) * samplesPerPixel) );
}
}
//
// Getting high-level data from the track for screen display and
// clipping calculations
//
bool WaveClip::GetWaveDisplay(WaveDisplay &display, double t0,
double pixelsPerSecond, bool &isLoadingOD) const
{
const bool allocated = (display.where != 0);
const size_t numPixels = (int)display.width;
size_t p0 = 0; // least column requiring computation
size_t p1 = numPixels; // greatest column requiring computation, plus one
float *min;
float *max;
float *rms;
int *bl;
std::vector<sampleCount> *pWhere;
if (allocated) {
// assume ownWhere is filled.
min = &display.min[0];
max = &display.max[0];
rms = &display.rms[0];
bl = &display.bl[0];
pWhere = &display.ownWhere;
}
else {
// Lock the list of invalid regions
ODLocker locker(&mWaveCacheMutex);
const double tstep = 1.0 / pixelsPerSecond;
const double samplesPerPixel = mRate * tstep;
// Make a tolerant comparison of the pps values in this wise:
// accumulated difference of times over the number of pixels is less than
// a sample period.
const bool ppsMatch = mWaveCache &&
(fabs(tstep - 1.0 / mWaveCache->pps) * numPixels < (1.0 / mRate));
const bool match =
mWaveCache &&
ppsMatch &&
mWaveCache->len > 0 &&
mWaveCache->dirty == mDirty;
if (match &&
mWaveCache->start == t0 &&
mWaveCache->len >= numPixels) {
mWaveCache->LoadInvalidRegions(mSequence.get(), true);
mWaveCache->ClearInvalidRegions();
// Satisfy the request completely from the cache
display.min = &mWaveCache->min[0];
display.max = &mWaveCache->max[0];
display.rms = &mWaveCache->rms[0];
display.bl = &mWaveCache->bl[0];
display.where = &mWaveCache->where[0];
isLoadingOD = mWaveCache->numODPixels > 0;
return true;
}
std::unique_ptr<WaveCache> oldCache(std::move(mWaveCache));
int oldX0 = 0;
double correction = 0.0;
size_t copyBegin = 0, copyEnd = 0;
if (match) {
findCorrection(oldCache->where, oldCache->len, numPixels,
t0, mRate, samplesPerPixel,
oldX0, correction);
// Remember our first pixel maps to oldX0 in the old cache,
// possibly out of bounds.
// For what range of pixels can data be copied?
copyBegin = std::min<size_t>(numPixels, std::max(0, -oldX0));
copyEnd = std::min<size_t>(numPixels, std::max(0,
(int)oldCache->len - oldX0
));
}
if (!(copyEnd > copyBegin))
oldCache.reset(0);
mWaveCache = std::make_unique<WaveCache>(numPixels, pixelsPerSecond, mRate, t0, mDirty);
min = &mWaveCache->min[0];
max = &mWaveCache->max[0];
rms = &mWaveCache->rms[0];
bl = &mWaveCache->bl[0];
pWhere = &mWaveCache->where;
fillWhere(*pWhere, numPixels, 0.0, correction,
t0, mRate, samplesPerPixel);
// The range of pixels we must fetch from the Sequence:
p0 = (copyBegin > 0) ? 0 : copyEnd;
p1 = (copyEnd >= numPixels) ? copyBegin : numPixels;
// Optimization: if the old cache is good and overlaps
// with the current one, re-use as much of the cache as
// possible
if (oldCache) {
//TODO: only load inval regions if
//necessary. (usually is the case, so no rush.)
//also, we should be updating the NEW cache, but here we are patching the old one up.
oldCache->LoadInvalidRegions(mSequence.get(), false);
oldCache->ClearInvalidRegions();
// Copy what we can from the old cache.
const int length = copyEnd - copyBegin;
const size_t sizeFloats = length * sizeof(float);
const int srcIdx = (int)copyBegin + oldX0;
memcpy(&min[copyBegin], &oldCache->min[srcIdx], sizeFloats);
memcpy(&max[copyBegin], &oldCache->max[srcIdx], sizeFloats);
memcpy(&rms[copyBegin], &oldCache->rms[srcIdx], sizeFloats);
memcpy(&bl[copyBegin], &oldCache->bl[srcIdx], length * sizeof(int));
}
}
if (p1 > p0) {
// Cache was not used or did not satisfy the whole request
std::vector<sampleCount> &where = *pWhere;
/* handle values in the append buffer */
auto numSamples = mSequence->GetNumSamples();
auto a = p0;
// Not all of the required columns might be in the sequence.
// Some might be in the append buffer.
for (; a < p1; ++a) {
if (where[a + 1] > numSamples)
break;
}
// Handle the columns that land in the append buffer.
//compute the values that are outside the overlap from scratch.
if (a < p1) {
sampleFormat seqFormat = mSequence->GetSampleFormat();
bool didUpdate = false;
for(auto i = a; i < p1; i++) {
auto left = std::max(sampleCount{ 0 },
where[i] - numSamples);
auto right = std::min(sampleCount{ mAppendBufferLen },
where[i + 1] - numSamples);
//wxCriticalSectionLocker locker(mAppendCriticalSection);
if (right > left) {
Floats b;
float *pb{};
// left is nonnegative and at most mAppendBufferLen:
auto sLeft = left.as_size_t();
// The difference is at most mAppendBufferLen:
size_t len = ( right - left ).as_size_t();
if (seqFormat == floatSample)
pb = &((float *)mAppendBuffer.ptr())[sLeft];
else {
b.reinit(len);
pb = b.get();
CopySamples(mAppendBuffer.ptr() + sLeft * SAMPLE_SIZE(seqFormat),
seqFormat,
(samplePtr)pb, floatSample, len);
}
float theMax, theMin, sumsq;
{
const float val = pb[0];
theMax = theMin = val;
sumsq = val * val;
}
for(decltype(len) j = 1; j < len; j++) {
const float val = pb[j];
theMax = std::max(theMax, val);
theMin = std::min(theMin, val);
sumsq += val * val;
}
min[i] = theMin;
max[i] = theMax;
rms[i] = (float)sqrt(sumsq / len);
bl[i] = 1; //for now just fake it.
didUpdate=true;
}
}
// Shrink the right end of the range to fetch from Sequence
if(didUpdate)
p1 = a;
}
// Done with append buffer, now fetch the rest of the cache miss
// from the sequence
if (p1 > p0) {
if (!mSequence->GetWaveDisplay(&min[p0],
&max[p0],
&rms[p0],
&bl[p0],
p1-p0,
&where[p0]))
{
isLoadingOD=false;
return false;
}
}
}
//find the number of OD pixels - the only way to do this is by recounting
if (!allocated) {
// Now report the results
display.min = min;
display.max = max;
display.rms = rms;
display.bl = bl;
display.where = &(*pWhere)[0];
isLoadingOD = mWaveCache->numODPixels > 0;
}
else {
using namespace std;
isLoadingOD =
count_if(display.ownBl.begin(), display.ownBl.end(),
bind2nd(less<int>(), 0)) > 0;
}
return true;
}
namespace {
void ComputeSpectrogramGainFactors
(size_t fftLen, double rate, int frequencyGain, std::vector<float> &gainFactors)
{
if (frequencyGain > 0) {
// Compute a frequency-dependent gain factor
// scaled such that 1000 Hz gets a gain of 0dB
// This is the reciprocal of the bin number of 1000 Hz:
const double factor = ((double)rate / (double)fftLen) / 1000.0;
auto half = fftLen / 2;
gainFactors.reserve(half);
// Don't take logarithm of zero! Let bin 0 replicate the gain factor for bin 1.
gainFactors.push_back(frequencyGain*log10(factor));
for (decltype(half) x = 1; x < half; x++) {
gainFactors.push_back(frequencyGain*log10(factor * x));
}
}
}
}
bool SpecCache::Matches
(int dirty_, double pixelsPerSecond,
const SpectrogramSettings &settings, double rate) const
{
// Make a tolerant comparison of the pps values in this wise:
// accumulated difference of times over the number of pixels is less than
// a sample period.
const double tstep = 1.0 / pixelsPerSecond;
const bool ppsMatch =
(fabs(tstep - 1.0 / pps) * len < (1.0 / rate));
return
ppsMatch &&
dirty == dirty_ &&
windowType == settings.windowType &&
windowSize == settings.WindowSize() &&
zeroPaddingFactor == settings.ZeroPaddingFactor() &&
frequencyGain == settings.frequencyGain &&
algorithm == settings.algorithm;
}
bool SpecCache::CalculateOneSpectrum
(const SpectrogramSettings &settings,
WaveTrackCache &waveTrackCache,
const int xx, const sampleCount numSamples,
double offset, double rate, double pixelsPerSecond,
int lowerBoundX, int upperBoundX,
const std::vector<float> &gainFactors,
float* __restrict scratch, float* __restrict out) const
{
bool result = false;
const bool reassignment =
(settings.algorithm == SpectrogramSettings::algReassignment);
const size_t windowSizeSetting = settings.WindowSize();
sampleCount from;
// xx may be for a column that is out of the visible bounds, but only
// when we are calculating reassignment contributions that may cross into
// the visible area.
if (xx < 0)
from = sampleCount(
where[0].as_double() + xx * (rate / pixelsPerSecond)
);
else if (xx > (int)len)
from = sampleCount(
where[len].as_double() + (xx - len) * (rate / pixelsPerSecond)
);
else
from = where[xx];
const bool autocorrelation =
settings.algorithm == SpectrogramSettings::algPitchEAC;
const size_t zeroPaddingFactorSetting = settings.ZeroPaddingFactor();
const size_t padding = (windowSizeSetting * (zeroPaddingFactorSetting - 1)) / 2;
const size_t fftLen = windowSizeSetting * zeroPaddingFactorSetting;
auto nBins = settings.NBins();
if (from < 0 || from >= numSamples) {
if (xx >= 0 && xx < (int)len) {
// Pixel column is out of bounds of the clip! Should not happen.
float *const results = &out[nBins * xx];
std::fill(results, results + nBins, 0.0f);
}
}
else {
// We can avoid copying memory when ComputeSpectrum is used below
bool copy = !autocorrelation || (padding > 0) || reassignment;
float *useBuffer = 0;
float *adj = scratch + padding;
{
auto myLen = windowSizeSetting;
// Take a window of the track centered at this sample.
from -= windowSizeSetting >> 1;
if (from < 0) {
// Near the start of the clip, pad left with zeroes as needed.
// from is at least -windowSize / 2
for (auto ii = from; ii < 0; ++ii)
*adj++ = 0;
myLen += from.as_long_long(); // add a negative
from = 0;
copy = true;
}
if (from + myLen >= numSamples) {
// Near the end of the clip, pad right with zeroes as needed.
// newlen is bounded by myLen:
auto newlen = ( numSamples - from ).as_size_t();
for (decltype(myLen) ii = newlen; ii < myLen; ++ii)
adj[ii] = 0;
myLen = newlen;
copy = true;
}
if (myLen > 0) {
useBuffer = (float*)(waveTrackCache.Get(
floatSample, sampleCount(
floor(0.5 + from.as_double() + offset * rate)
),
myLen,
// Don't throw in this drawing operation
false)
);
if (copy) {
if (useBuffer)
memcpy(adj, useBuffer, myLen * sizeof(float));
else
memset(adj, 0, myLen * sizeof(float));
}
}
}
if (copy || !useBuffer)
useBuffer = scratch;
if (autocorrelation) {
// not reassignment, xx is surely within bounds.
wxASSERT(xx >= 0);
float *const results = &out[nBins * xx];
// This function does not mutate useBuffer
ComputeSpectrum(useBuffer, windowSizeSetting, windowSizeSetting,
rate, results,
autocorrelation, settings.windowType);
}
else if (reassignment) {
static const double epsilon = 1e-16;
const auto hFFT = settings.hFFT.get();
float *const scratch2 = scratch + fftLen;
std::copy(scratch, scratch2, scratch2);
float *const scratch3 = scratch + 2 * fftLen;
std::copy(scratch, scratch2, scratch3);
{
const float *const window = settings.window.get();
for (size_t ii = 0; ii < fftLen; ++ii)
scratch[ii] *= window[ii];
RealFFTf(scratch, hFFT);
}
{
const float *const dWindow = settings.dWindow.get();
for (size_t ii = 0; ii < fftLen; ++ii)
scratch2[ii] *= dWindow[ii];
RealFFTf(scratch2, hFFT);
}
{
const float *const tWindow = settings.tWindow.get();
for (size_t ii = 0; ii < fftLen; ++ii)
scratch3[ii] *= tWindow[ii];
RealFFTf(scratch3, hFFT);
}
for (size_t ii = 0; ii < hFFT->Points; ++ii) {
const int index = hFFT->BitReversed[ii];
const float
denomRe = scratch[index],
denomIm = ii == 0 ? 0 : scratch[index + 1];
const double power = denomRe * denomRe + denomIm * denomIm;
if (power < epsilon)
// Avoid dividing by near-zero below
continue;
double freqCorrection;
{
const double multiplier = -(fftLen / (2.0f * M_PI));
const float
numRe = scratch2[index],
numIm = ii == 0 ? 0 : scratch2[index + 1];
// Find complex quotient --
// Which means, multiply numerator by conjugate of denominator,
// then divide by norm squared of denominator --
// Then just take its imaginary part.
const double
quotIm = (-numRe * denomIm + numIm * denomRe) / power;
// With appropriate multiplier, that becomes the correction of
// the frequency bin.
freqCorrection = multiplier * quotIm;
}
const int bin = (int)((int)ii + freqCorrection + 0.5f);
// Must check if correction takes bin out of bounds, above or below!
// bin is signed!
if (bin >= 0 && bin < (int)hFFT->Points) {
double timeCorrection;
{
const float
numRe = scratch3[index],
numIm = ii == 0 ? 0 : scratch3[index + 1];
// Find another complex quotient --
// Then just take its real part.
// The result has sample interval as unit.
timeCorrection =
(numRe * denomRe + numIm * denomIm) / power;
}
int correctedX = (floor(0.5 + xx + timeCorrection * pixelsPerSecond / rate));
if (correctedX >= lowerBoundX && correctedX < upperBoundX)
{
result = true;
// This is non-negative, because bin and correctedX are
auto ind = (int)nBins * correctedX + bin;
#ifdef _OPENMP
// This assignment can race if index reaches into another thread's bins.
// The probability of a race very low, so this carries little overhead,
// about 5% slower vs allowing it to race.
#pragma omp atomic update
#endif
out[ind] += power;
}
}
}
}
else {
// not reassignment, xx is surely within bounds.
wxASSERT(xx >= 0);
float *const results = &out[nBins * xx];
// Do the FFT. Note that useBuffer is multiplied by the window,
// and the window is initialized with leading and trailing zeroes
// when there is padding. Therefore we did not need to reinitialize
// the part of useBuffer in the padding zones.
// This function mutates useBuffer
ComputeSpectrumUsingRealFFTf
(useBuffer, settings.hFFT.get(), settings.window.get(), fftLen, results);
if (!gainFactors.empty()) {
// Apply a frequency-dependant gain factor
for (size_t ii = 0; ii < nBins; ++ii)
results[ii] += gainFactors[ii];
}
}
}
return result;
}
void SpecCache::Grow(size_t len_, const SpectrogramSettings& settings,
double pixelsPerSecond, double start_)
{
settings.CacheWindows();
// len columns, and so many rows, column-major.
// Don't take column literally -- this isn't pixel data yet, it's the
// raw data to be mapped onto the display.
freq.resize(len_ * settings.NBins());
// Sample counts corresponding to the columns, and to one past the end.
where.resize(len_ + 1);
len = len_;
algorithm = settings.algorithm;
pps = pixelsPerSecond;
start = start_;
windowType = settings.windowType;
windowSize = settings.WindowSize();
zeroPaddingFactor = settings.ZeroPaddingFactor();
frequencyGain = settings.frequencyGain;
}
void SpecCache::Populate
(const SpectrogramSettings &settings, WaveTrackCache &waveTrackCache,
int copyBegin, int copyEnd, size_t numPixels,
sampleCount numSamples,
double offset, double rate, double pixelsPerSecond)
{
const int &frequencyGainSetting = settings.frequencyGain;
const size_t windowSizeSetting = settings.WindowSize();
const bool autocorrelation =
settings.algorithm == SpectrogramSettings::algPitchEAC;
const bool reassignment =
settings.algorithm == SpectrogramSettings::algReassignment;
#ifdef EXPERIMENTAL_ZERO_PADDED_SPECTROGRAMS
const size_t zeroPaddingFactorSetting = settings.ZeroPaddingFactor();
#else
const size_t zeroPaddingFactorSetting = 1;
#endif
// FFT length may be longer than the window of samples that affect results
// because of zero padding done for increased frequency resolution
const size_t fftLen = windowSizeSetting * zeroPaddingFactorSetting;
const auto nBins = settings.NBins();
const size_t bufferSize = fftLen;
const size_t scratchSize = reassignment ? 3 * bufferSize : bufferSize;
std::vector<float> scratch(scratchSize);
std::vector<float> gainFactors;
if (!autocorrelation)
ComputeSpectrogramGainFactors(fftLen, rate, frequencyGainSetting, gainFactors);
// Loop over the ranges before and after the copied portion and compute anew.
// One of the ranges may be empty.
for (int jj = 0; jj < 2; ++jj) {
const int lowerBoundX = jj == 0 ? 0 : copyEnd;
const int upperBoundX = jj == 0 ? copyBegin : numPixels;
#ifdef _OPENMP
// Storage for mutable per-thread data.
// private clause ensures one copy per thread
struct ThreadLocalStorage {
ThreadLocalStorage() { }
~ThreadLocalStorage() { }
void init(WaveTrackCache &waveTrackCache, size_t scratchSize) {
if (!cache) {
cache = std::make_unique<WaveTrackCache>(waveTrackCache.GetTrack());
scratch.resize(scratchSize);
}
}
std::unique_ptr<WaveTrackCache> cache;
std::vector<float> scratch;
} tls;
#pragma omp parallel for private(tls)
#endif
for (auto xx = lowerBoundX; xx < upperBoundX; ++xx)
{
#ifdef _OPENMP
tls.init(waveTrackCache, scratchSize);
WaveTrackCache& cache = *tls.cache;
float* buffer = &tls.scratch[0];
#else
WaveTrackCache& cache = waveTrackCache;
float* buffer = &scratch[0];
#endif
CalculateOneSpectrum(
settings, cache, xx, numSamples,
offset, rate, pixelsPerSecond,
lowerBoundX, upperBoundX,
gainFactors, buffer, &freq[0]);
}
if (reassignment) {
// Need to look beyond the edges of the range to accumulate more
// time reassignments.
// I'm not sure what's a good stopping criterion?
auto xx = lowerBoundX;
const double pixelsPerSample = pixelsPerSecond / rate;
const int limit = std::min((int)(0.5 + fftLen * pixelsPerSample), 100);
for (int ii = 0; ii < limit; ++ii)
{
const bool result =
CalculateOneSpectrum(
settings, waveTrackCache, --xx, numSamples,
offset, rate, pixelsPerSecond,
lowerBoundX, upperBoundX,
gainFactors, &scratch[0], &freq[0]);
if (!result)
break;
}
xx = upperBoundX;
for (int ii = 0; ii < limit; ++ii)
{
const bool result =
CalculateOneSpectrum(
settings, waveTrackCache, xx++, numSamples,
offset, rate, pixelsPerSecond,
lowerBoundX, upperBoundX,
gainFactors, &scratch[0], &freq[0]);
if (!result)
break;
}
// Now Convert to dB terms. Do this only after accumulating
// power values, which may cross columns with the time correction.
#ifdef _OPENMP
#pragma omp parallel for
#endif
for (xx = lowerBoundX; xx < upperBoundX; ++xx) {
float *const results = &freq[nBins * xx];
for (size_t ii = 0; ii < nBins; ++ii) {
float &power = results[ii];
if (power <= 0)
power = -160.0;
else
power = 10.0*log10f(power);
}
if (!gainFactors.empty()) {
// Apply a frequency-dependant gain factor
for (size_t ii = 0; ii < nBins; ++ii)
results[ii] += gainFactors[ii];
}
}
}
}
}
bool WaveClip::GetSpectrogram(WaveTrackCache &waveTrackCache,
const float *& spectrogram,
const sampleCount *& where,
size_t numPixels,
double t0, double pixelsPerSecond) const
{
const WaveTrack *const track = waveTrackCache.GetTrack().get();
const SpectrogramSettings &settings = track->GetSpectrogramSettings();
bool match =
mSpecCache &&
mSpecCache->len > 0 &&
mSpecCache->Matches
(mDirty, pixelsPerSecond, settings, mRate);
if (match &&
mSpecCache->start == t0 &&
mSpecCache->len >= numPixels) {
spectrogram = &mSpecCache->freq[0];
where = &mSpecCache->where[0];
return false; //hit cache completely
}
// Caching is not implemented for reassignment, unless for
// a complete hit, because of the complications of time reassignment
if (settings.algorithm == SpectrogramSettings::algReassignment)
match = false;
// Free the cache when it won't cause a major stutter.
// If the window size changed, we know there is nothing to be copied
// If we zoomed out, or resized, we can give up memory. But not too much -
// up to 2x extra is needed at the end of the clip to prevent stutter.
if (mSpecCache->freq.capacity() > 2.1 * mSpecCache->freq.size() ||
mSpecCache->windowSize*mSpecCache->zeroPaddingFactor <
settings.WindowSize()*settings.ZeroPaddingFactor())
{
match = false;
mSpecCache = std::make_unique<SpecCache>();
}
const double tstep = 1.0 / pixelsPerSecond;
const double samplesPerPixel = mRate * tstep;
int oldX0 = 0;
double correction = 0.0;
int copyBegin = 0, copyEnd = 0;
if (match) {
findCorrection(mSpecCache->where, mSpecCache->len, numPixels,
t0, mRate, samplesPerPixel,
oldX0, correction);
// Remember our first pixel maps to oldX0 in the old cache,
// possibly out of bounds.
// For what range of pixels can data be copied?
copyBegin = std::min((int)numPixels, std::max(0, -oldX0));
copyEnd = std::min((int)numPixels, std::max(0,
(int)mSpecCache->len - oldX0
));
}
// Resize the cache, keep the contents unchanged.
mSpecCache->Grow(numPixels, settings, pixelsPerSecond, t0);
auto nBins = settings.NBins();
// Optimization: if the old cache is good and overlaps
// with the current one, re-use as much of the cache as
// possible
if (copyEnd > copyBegin)
{
// memmove is required since dst/src overlap
memmove(&mSpecCache->freq[nBins * copyBegin],
&mSpecCache->freq[nBins * (copyBegin + oldX0)],
nBins * (copyEnd - copyBegin) * sizeof(float));
}
// Reassignment accumulates, so it needs a zeroed buffer
if (settings.algorithm == SpectrogramSettings::algReassignment)
{
// The cache could theoretically copy from the middle, resulting
// in two regions to update. This won't happen in zoom, since
// old cache doesn't match. It won't happen in resize, since the
// spectrum view is pinned to left side of window.
wxASSERT(
(copyBegin >= 0 && copyEnd == (int)numPixels) || // copied the end
(copyBegin == 0 && copyEnd <= (int)numPixels) // copied the beginning
);
int zeroBegin = copyBegin > 0 ? 0 : copyEnd-copyBegin;
int zeroEnd = copyBegin > 0 ? copyBegin : numPixels;
memset(&mSpecCache->freq[nBins*zeroBegin], 0, nBins*(zeroEnd-zeroBegin)*sizeof(float));
}
// purposely offset the display 1/2 sample to the left (as compared
// to waveform display) to properly center response of the FFT
fillWhere(mSpecCache->where, numPixels, 0.5, correction,
t0, mRate, samplesPerPixel);
mSpecCache->Populate
(settings, waveTrackCache, copyBegin, copyEnd, numPixels,
mSequence->GetNumSamples(),
mOffset, mRate, pixelsPerSecond);
mSpecCache->dirty = mDirty;
spectrogram = &mSpecCache->freq[0];
where = &mSpecCache->where[0];
return true;
}
std::pair<float, float> WaveClip::GetMinMax(
double t0, double t1, bool mayThrow) const
{
if (t0 > t1) {
if (mayThrow)
THROW_INCONSISTENCY_EXCEPTION;
return {
0.f, // harmless, but unused since Sequence::GetMinMax does not use these values
0.f // harmless, but unused since Sequence::GetMinMax does not use these values
};
}
if (t0 == t1)
return{ 0.f, 0.f };
sampleCount s0, s1;
TimeToSamplesClip(t0, &s0);
TimeToSamplesClip(t1, &s1);
return mSequence->GetMinMax(s0, s1-s0, mayThrow);
}
float WaveClip::GetRMS(double t0, double t1, bool mayThrow) const
{
if (t0 > t1) {
if (mayThrow)
THROW_INCONSISTENCY_EXCEPTION;
return 0.f;
}
if (t0 == t1)
return 0.f;
sampleCount s0, s1;
TimeToSamplesClip(t0, &s0);
TimeToSamplesClip(t1, &s1);
return mSequence->GetRMS(s0, s1-s0, mayThrow);
}
void WaveClip::ConvertToSampleFormat(sampleFormat format)
{
// Note: it is not necessary to do this recursively to cutlines.
// They get converted as needed when they are expanded.
auto bChanged = mSequence->ConvertToSampleFormat(format);
if (bChanged)
MarkChanged();
}
void WaveClip::UpdateEnvelopeTrackLen()
// NOFAIL-GUARANTEE
{
mEnvelope->SetTrackLen
((mSequence->GetNumSamples().as_double()) / mRate, 1.0 / GetRate());
}
void WaveClip::TimeToSamplesClip(double t0, sampleCount *s0) const
{
if (t0 < mOffset)
*s0 = 0;
else if (t0 > mOffset + mSequence->GetNumSamples().as_double()/mRate)
*s0 = mSequence->GetNumSamples();
else
*s0 = sampleCount( floor(((t0 - mOffset) * mRate) + 0.5) );
}
void WaveClip::ClearDisplayRect() const
{
mDisplayRect.x = mDisplayRect.y = -1;
mDisplayRect.width = mDisplayRect.height = -1;
}
void WaveClip::SetDisplayRect(const wxRect& r) const
{
mDisplayRect = r;
}
void WaveClip::GetDisplayRect(wxRect* r)
{
*r = mDisplayRect;
}
void WaveClip::Append(samplePtr buffer, sampleFormat format,
size_t len, unsigned int stride /* = 1 */,
XMLWriter* blockFileLog /*=NULL*/)
// PARTIAL-GUARANTEE in case of exceptions:
// Some prefix (maybe none) of the buffer is appended, and no content already
// flushed to disk is lost.
{
//wxLogDebug(wxT("Append: len=%lli"), (long long) len);
auto maxBlockSize = mSequence->GetMaxBlockSize();
auto blockSize = mSequence->GetIdealAppendLen();
sampleFormat seqFormat = mSequence->GetSampleFormat();
if (!mAppendBuffer.ptr())
mAppendBuffer.Allocate(maxBlockSize, seqFormat);
auto cleanup = finally( [&] {
// use NOFAIL-GUARANTEE
UpdateEnvelopeTrackLen();
MarkChanged();
} );
for(;;) {
if (mAppendBufferLen >= blockSize) {
// flush some previously appended contents
// use STRONG-GUARANTEE
mSequence->Append(mAppendBuffer.ptr(), seqFormat, blockSize,
blockFileLog);
// use NOFAIL-GUARANTEE for rest of this "if"
memmove(mAppendBuffer.ptr(),
mAppendBuffer.ptr() + blockSize * SAMPLE_SIZE(seqFormat),
(mAppendBufferLen - blockSize) * SAMPLE_SIZE(seqFormat));
mAppendBufferLen -= blockSize;
blockSize = mSequence->GetIdealAppendLen();
}
if (len == 0)
break;
// use NOFAIL-GUARANTEE for rest of this "for"
wxASSERT(mAppendBufferLen <= maxBlockSize);
auto toCopy = std::min(len, maxBlockSize - mAppendBufferLen);
CopySamples(buffer, format,
mAppendBuffer.ptr() + mAppendBufferLen * SAMPLE_SIZE(seqFormat),
seqFormat,
toCopy,
true, // high quality
stride);
mAppendBufferLen += toCopy;
buffer += toCopy * SAMPLE_SIZE(format) * stride;
len -= toCopy;
}
}
void WaveClip::AppendAlias(const FilePath &fName, sampleCount start,
size_t len, int channel,bool useOD)
// STRONG-GUARANTEE
{
// use STRONG-GUARANTEE
mSequence->AppendAlias(fName, start, len, channel,useOD);
// use NOFAIL-GUARANTEE
UpdateEnvelopeTrackLen();
MarkChanged();
}
void WaveClip::AppendCoded(const FilePath &fName, sampleCount start,
size_t len, int channel, int decodeType)
// STRONG-GUARANTEE
{
// use STRONG-GUARANTEE
mSequence->AppendCoded(fName, start, len, channel, decodeType);
// use NOFAIL-GUARANTEE
UpdateEnvelopeTrackLen();
MarkChanged();
}
void WaveClip::Flush()
// NOFAIL-GUARANTEE that the clip will be in a flushed state.
// PARTIAL-GUARANTEE in case of exceptions:
// Some initial portion (maybe none) of the append buffer of the
// clip gets appended; no previously flushed contents are lost.
{
//wxLogDebug(wxT("WaveClip::Flush"));
//wxLogDebug(wxT(" mAppendBufferLen=%lli"), (long long) mAppendBufferLen);
//wxLogDebug(wxT(" previous sample count %lli"), (long long) mSequence->GetNumSamples());
if (mAppendBufferLen > 0) {
auto cleanup = finally( [&] {
// Blow away the append buffer even in case of failure. May lose some
// data but don't leave the track in an un-flushed state.
// Use NOFAIL-GUARANTEE of these steps.
mAppendBufferLen = 0;
UpdateEnvelopeTrackLen();
MarkChanged();
} );
mSequence->Append(mAppendBuffer.ptr(), mSequence->GetSampleFormat(),
mAppendBufferLen);
}
//wxLogDebug(wxT("now sample count %lli"), (long long) mSequence->GetNumSamples());
}
bool WaveClip::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
{
if (!wxStrcmp(tag, wxT("waveclip")))
{
double dblValue;
long longValue;
while (*attrs)
{
const wxChar *attr = *attrs++;
const wxChar *value = *attrs++;
if (!value)
break;
const wxString strValue = value;
if (!wxStrcmp(attr, wxT("offset")))
{
if (!XMLValueChecker::IsGoodString(strValue) ||
!Internat::CompatibleToDouble(strValue, &dblValue))
return false;
SetOffset(dblValue);
}
if (!wxStrcmp(attr, wxT("colorindex")))
{
if (!XMLValueChecker::IsGoodString(strValue) ||
!strValue.ToLong( &longValue))
return false;
SetColourIndex(longValue);
}
}
return true;
}
return false;
}
void WaveClip::HandleXMLEndTag(const wxChar *tag)
{
if (!wxStrcmp(tag, wxT("waveclip")))
UpdateEnvelopeTrackLen();
}
XMLTagHandler *WaveClip::HandleXMLChild(const wxChar *tag)
{
if (!wxStrcmp(tag, wxT("sequence")))
return mSequence.get();
else if (!wxStrcmp(tag, wxT("envelope")))
return mEnvelope.get();
else if (!wxStrcmp(tag, wxT("waveclip")))
{
// Nested wave clips are cut lines
mCutLines.push_back(
std::make_unique<WaveClip>(mSequence->GetDirManager(),
mSequence->GetSampleFormat(), mRate, 0 /*colourindex*/));
return mCutLines.back().get();
}
else
return NULL;
}
void WaveClip::WriteXML(XMLWriter &xmlFile) const
// may throw
{
xmlFile.StartTag(wxT("waveclip"));
xmlFile.WriteAttr(wxT("offset"), mOffset, 8);
xmlFile.WriteAttr(wxT("colorindex"), mColourIndex );
mSequence->WriteXML(xmlFile);
mEnvelope->WriteXML(xmlFile);
for (const auto &clip: mCutLines)
clip->WriteXML(xmlFile);
xmlFile.EndTag(wxT("waveclip"));
}
void WaveClip::Paste(double t0, const WaveClip* other)
// STRONG-GUARANTEE
{
const bool clipNeedsResampling = other->mRate != mRate;
const bool clipNeedsNewFormat =
other->mSequence->GetSampleFormat() != mSequence->GetSampleFormat();
std::unique_ptr<WaveClip> newClip;
const WaveClip* pastedClip;
if (clipNeedsResampling || clipNeedsNewFormat)
{
newClip =
std::make_unique<WaveClip>(*other, mSequence->GetDirManager(), true);
if (clipNeedsResampling)
// The other clip's rate is different from ours, so resample
newClip->Resample(mRate);
if (clipNeedsNewFormat)
// Force sample formats to match.
newClip->ConvertToSampleFormat(mSequence->GetSampleFormat());
pastedClip = newClip.get();
}
else
{
// No resampling or format change needed, just use original clip without making a copy
pastedClip = other;
}
// Paste cut lines contained in pasted clip
WaveClipHolders newCutlines;
for (const auto &cutline: pastedClip->mCutLines)
{
newCutlines.push_back(
std::make_unique<WaveClip>
( *cutline, mSequence->GetDirManager(),
// Recursively copy cutlines of cutlines. They don't need
// their offsets adjusted.
true));
newCutlines.back()->Offset(t0 - mOffset);
}
sampleCount s0;
TimeToSamplesClip(t0, &s0);
// Assume STRONG-GUARANTEE from Sequence::Paste
mSequence->Paste(s0, pastedClip->mSequence.get());
// Assume NOFAIL-GUARANTEE in the remaining
MarkChanged();
auto sampleTime = 1.0 / GetRate();
mEnvelope->PasteEnvelope
(s0.as_double()/mRate + mOffset, pastedClip->mEnvelope.get(), sampleTime);
OffsetCutLines(t0, pastedClip->GetEndTime() - pastedClip->GetStartTime());
for (auto &holder : newCutlines)
mCutLines.push_back(std::move(holder));
}
void WaveClip::InsertSilence( double t, double len, double *pEnvelopeValue )
// STRONG-GUARANTEE
{
sampleCount s0;
TimeToSamplesClip(t, &s0);
auto slen = (sampleCount)floor(len * mRate + 0.5);
// use STRONG-GUARANTEE
GetSequence()->InsertSilence(s0, slen);
// use NOFAIL-GUARANTEE
OffsetCutLines(t, len);
const auto sampleTime = 1.0 / GetRate();
auto pEnvelope = GetEnvelope();
if ( pEnvelopeValue ) {
// Preserve limit value at the end
auto oldLen = pEnvelope->GetTrackLen();
auto newLen = oldLen + len;
pEnvelope->Cap( sampleTime );
// Ramp across the silence to the given value
pEnvelope->SetTrackLen( newLen, sampleTime );
pEnvelope->InsertOrReplace
( pEnvelope->GetOffset() + newLen, *pEnvelopeValue );
}
else
pEnvelope->InsertSpace( t, len );
MarkChanged();
}
void WaveClip::AppendSilence( double len, double envelopeValue )
// STRONG-GUARANTEE
{
auto t = GetEndTime();
InsertSilence( t, len, &envelopeValue );
}
void WaveClip::Clear(double t0, double t1)
// STRONG-GUARANTEE
{
sampleCount s0, s1;
TimeToSamplesClip(t0, &s0);
TimeToSamplesClip(t1, &s1);
// use STRONG-GUARANTEE
GetSequence()->Delete(s0, s1-s0);
// use NOFAIL-GUARANTEE in the remaining
// msmeyer
//
// Delete all cutlines that are within the given area, if any.
//
// Note that when cutlines are active, two functions are used:
// Clear() and ClearAndAddCutLine(). ClearAndAddCutLine() is called
// whenever the user directly calls a command that removes some audio, e.g.
// "Cut" or "Clear" from the menu. This command takes care about recursive
// preserving of cutlines within clips. Clear() is called when internal
// operations want to remove audio. In the latter case, it is the right
// thing to just remove all cutlines within the area.
//
double clip_t0 = t0;
double clip_t1 = t1;
if (clip_t0 < GetStartTime())
clip_t0 = GetStartTime();
if (clip_t1 > GetEndTime())
clip_t1 = GetEndTime();
// May DELETE as we iterate, so don't use range-for
for (auto it = mCutLines.begin(); it != mCutLines.end();)
{
WaveClip* clip = it->get();
double cutlinePosition = mOffset + clip->GetOffset();
if (cutlinePosition >= t0 && cutlinePosition <= t1)
{
// This cutline is within the area, DELETE it
it = mCutLines.erase(it);
}
else
{
if (cutlinePosition >= t1)
{
clip->Offset(clip_t0 - clip_t1);
}
++it;
}
}
// Collapse envelope
auto sampleTime = 1.0 / GetRate();
GetEnvelope()->CollapseRegion( t0, t1, sampleTime );
if (t0 < GetStartTime())
Offset(-(GetStartTime() - t0));
MarkChanged();
}
void WaveClip::ClearAndAddCutLine(double t0, double t1)
// WEAK-GUARANTEE
// this WaveClip remains destructible in case of AudacityException.
// But some cutlines may be deleted
{
if (t0 > GetEndTime() || t1 < GetStartTime())
return; // time out of bounds
const double clip_t0 = std::max( t0, GetStartTime() );
const double clip_t1 = std::min( t1, GetEndTime() );
auto newClip = std::make_unique< WaveClip >
(*this, mSequence->GetDirManager(), true, clip_t0, clip_t1);
newClip->SetOffset( clip_t0 - mOffset );
// Remove cutlines from this clip that were in the selection, shift
// left those that were after the selection
// May DELETE as we iterate, so don't use range-for
for (auto it = mCutLines.begin(); it != mCutLines.end();)
{
WaveClip* clip = it->get();
double cutlinePosition = mOffset + clip->GetOffset();
if (cutlinePosition >= t0 && cutlinePosition <= t1)
it = mCutLines.erase(it);
else
{
if (cutlinePosition >= t1)
{
clip->Offset(clip_t0 - clip_t1);
}
++it;
}
}
// Clear actual audio data
sampleCount s0, s1;
TimeToSamplesClip(t0, &s0);
TimeToSamplesClip(t1, &s1);
// use WEAK-GUARANTEE
GetSequence()->Delete(s0, s1-s0);
// Collapse envelope
auto sampleTime = 1.0 / GetRate();
GetEnvelope()->CollapseRegion( t0, t1, sampleTime );
if (t0 < GetStartTime())
Offset(-(GetStartTime() - t0));
MarkChanged();
mCutLines.push_back(std::move(newClip));
}
bool WaveClip::FindCutLine(double cutLinePosition,
double* cutlineStart /* = NULL */,
double* cutlineEnd /* = NULL */) const
{
for (const auto &cutline: mCutLines)
{
if (fabs(mOffset + cutline->GetOffset() - cutLinePosition) < 0.0001)
{
if (cutlineStart)
*cutlineStart = mOffset+cutline->GetStartTime();
if (cutlineEnd)
*cutlineEnd = mOffset+cutline->GetEndTime();
return true;
}
}
return false;
}
void WaveClip::ExpandCutLine(double cutLinePosition)
// STRONG-GUARANTEE
{
auto end = mCutLines.end();
auto it = std::find_if( mCutLines.begin(), end,
[&](const WaveClipHolder &cutline) {
return fabs(mOffset + cutline->GetOffset() - cutLinePosition) < 0.0001;
} );
if ( it != end ) {
auto cutline = it->get();
// assume STRONG-GUARANTEE from Paste
// Envelope::Paste takes offset into account, WaveClip::Paste doesn't!
// Do this to get the right result:
cutline->mEnvelope->SetOffset(0);
Paste(mOffset+cutline->GetOffset(), cutline);
// Now erase the cutline,
// but be careful to find it again, because Paste above may
// have modified the array of cutlines (if our cutline contained
// another cutline!), invalidating the iterator we had.
end = mCutLines.end();
it = std::find_if(mCutLines.begin(), end,
[=](const WaveClipHolder &p) { return p.get() == cutline; });
if (it != end)
mCutLines.erase(it); // deletes cutline!
else {
wxASSERT(false);
}
}
}
bool WaveClip::RemoveCutLine(double cutLinePosition)
{
for (auto it = mCutLines.begin(); it != mCutLines.end(); ++it)
{
const auto &cutline = *it;
if (fabs(mOffset + cutline->GetOffset() - cutLinePosition) < 0.0001)
{
mCutLines.erase(it); // deletes cutline!
return true;
}
}
return false;
}
void WaveClip::OffsetCutLines(double t0, double len)
// NOFAIL-GUARANTEE
{
for (const auto &cutLine : mCutLines)
{
if (mOffset + cutLine->GetOffset() >= t0)
cutLine->Offset(len);
}
}
void WaveClip::Lock()
{
GetSequence()->Lock();
for (const auto &cutline: mCutLines)
cutline->Lock();
}
void WaveClip::CloseLock()
{
GetSequence()->CloseLock();
for (const auto &cutline: mCutLines)
cutline->CloseLock();
}
void WaveClip::Unlock()
{
GetSequence()->Unlock();
for (const auto &cutline: mCutLines)
cutline->Unlock();
}
void WaveClip::SetRate(int rate)
{
mRate = rate;
auto newLength = mSequence->GetNumSamples().as_double() / mRate;
mEnvelope->RescaleTimes( newLength );
MarkChanged();
}
void WaveClip::Resample(int rate, ProgressDialog *progress)
// STRONG-GUARANTEE
{
// Note: it is not necessary to do this recursively to cutlines.
// They get resampled as needed when they are expanded.
if (rate == mRate)
return; // Nothing to do
double factor = (double)rate / (double)mRate;
::Resample resample(true, factor, factor); // constant rate resampling
const size_t bufsize = 65536;
Floats inBuffer{ bufsize };
Floats outBuffer{ bufsize };
sampleCount pos = 0;
bool error = false;
int outGenerated = 0;
auto numSamples = mSequence->GetNumSamples();
auto newSequence =
std::make_unique<Sequence>(mSequence->GetDirManager(), mSequence->GetSampleFormat());
/**
* We want to keep going as long as we have something to feed the resampler
* with OR as long as the resampler spews out samples (which could continue
* for a few iterations after we stop feeding it)
*/
while (pos < numSamples || outGenerated > 0)
{
const auto inLen = limitSampleBufferSize( bufsize, numSamples - pos );
bool isLast = ((pos + inLen) == numSamples);
if (!mSequence->Get((samplePtr)inBuffer.get(), floatSample, pos, inLen, true))
{
error = true;
break;
}
const auto results = resample.Process(factor, inBuffer.get(), inLen, isLast,
outBuffer.get(), bufsize);
outGenerated = results.second;
pos += results.first;
if (outGenerated < 0)
{
error = true;
break;
}
newSequence->Append((samplePtr)outBuffer.get(), floatSample,
outGenerated);
if (progress)
{
auto updateResult = progress->Update(
pos.as_long_long(),
numSamples.as_long_long()
);
error = (updateResult != ProgressResult::Success);
if (error)
throw UserException{};
}
}
if (error)
throw SimpleMessageBoxException{
_("Resampling failed.")
};
else
{
// Use NOFAIL-GUARANTEE in these steps
// Invalidate wave display cache
mWaveCache = std::make_unique<WaveCache>();
// Invalidate the spectrum display cache
mSpecCache = std::make_unique<SpecCache>();
mSequence = std::move(newSequence);
mRate = rate;
}
}
// Used by commands which interact with clips using the keyboard.
// When two clips are immediately next to each other, the GetEndTime()
// of the first clip and the GetStartTime() of the second clip may not
// be exactly equal due to rounding errors.
bool WaveClip::SharesBoundaryWithNextClip(const WaveClip* next) const
{
double endThis = GetRate() * GetOffset() + GetNumSamples().as_double();
double startNext = next->GetRate() * next->GetOffset();
// given that a double has about 15 significant digits, using a criterion
// of half a sample should be safe in all normal usage.
return fabs(startNext - endThis) < 0.5;
}