/********************************************************************** 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 #include #include #include #include "Sequence.h" #include "Spectrum.h" #include "Prefs.h" #include "Envelope.h" #include "Resample.h" #include "WaveTrack.h" #include "Profiler.h" #include "InconsistencyException.h" #include "UserException.h" #include "prefs/SpectrogramSettings.h" #include "widgets/ProgressDialog.h" #ifdef _OPENMP #include #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 where; std::vector min; std::vector max; std::vector rms; std::vector 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= 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= 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(), 0)); } protected: std::vector 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 &projDirManager, sampleFormat format, int rate, int colourIndex) { mRate = rate; mColourIndex = colourIndex; mSequence = std::make_unique(projDirManager, format); mEnvelope = std::make_unique(true, 1e-7, 2.0, 1.0); mWaveCache = std::make_unique(); mSpecCache = std::make_unique(); mSpecPxCache = std::make_unique(1); } WaveClip::WaveClip(const WaveClip& orig, const std::shared_ptr &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(*orig.mSequence, projDirManager); mEnvelope = std::make_unique(*orig.mEnvelope); mWaveCache = std::make_unique(); mSpecCache = std::make_unique(); mSpecPxCache = std::make_unique(1); if ( copyCutlines ) for (const auto &clip: orig.mCutLines) mCutLines.push_back ( std::make_unique( *clip, projDirManager, true ) ); mIsPlaceholder = orig.GetIsPlaceholder(); } WaveClip::WaveClip(const WaveClip& orig, const std::shared_ptr &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(); mSpecCache = std::make_unique(); mSpecPxCache = std::make_unique(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( *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(); } const BlockArray* WaveClip::GetSequenceBlockArray() const { 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(); } ///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 &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 &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 *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 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(numPixels, std::max(0, -oldX0)); copyEnd = std::min(numPixels, std::max(0, (int)oldCache->len - oldX0 )); } if (!(copyEnd > copyBegin)) oldCache.reset(0); mWaveCache = std::make_unique(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 &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(), 0)) > 0; } return true; } namespace { void ComputeSpectrogramGainFactors (size_t fftLen, double rate, int frequencyGain, std::vector &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 &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 scratch(scratchSize); std::vector 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.GetTrack()); scratch.resize(scratchSize); } } std::unique_ptr cache; std::vector 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(); } 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 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::AppendBlockFile( const BlockFileFactory &factory, size_t len) // STRONG-GUARANTEE { // use STRONG-GUARANTEE mSequence->AppendBlockFile( factory, len ); // 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(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 newClip; const WaveClip* pastedClip; if (clipNeedsResampling || clipNeedsNewFormat) { newClip = std::make_unique(*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 ( *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(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(); // Invalidate the spectrum display cache mSpecCache = std::make_unique(); 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; }