1832 lines
54 KiB
C++
1832 lines
54 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 <math.h>
|
|
#include <memory>
|
|
#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 "prefs/SpectrogramSettings.h"
|
|
|
|
#include <wx/listimpl.cpp>
|
|
|
|
#include "Experimental.h"
|
|
|
|
WX_DEFINE_LIST(WaveClipList);
|
|
|
|
class WaveCache {
|
|
public:
|
|
WaveCache()
|
|
: dirty(-1)
|
|
, len(-1)
|
|
, start(-1)
|
|
, pps(0)
|
|
, rate(-1)
|
|
, where(0)
|
|
, min(0)
|
|
, max(0)
|
|
, rms(0)
|
|
, bl(0)
|
|
, numODPixels(0)
|
|
{
|
|
}
|
|
|
|
WaveCache(int 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 int len; // 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(int s, int e):start(s),end(e){}
|
|
//start and end pixel count. (not samples)
|
|
int start;
|
|
int 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 - start*rate)/samplesPerPixel ;
|
|
|
|
long invalEnd = (sampleEnd - 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>=len && invalEnd >= 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 > len)
|
|
invalStart = len;
|
|
|
|
if(invalEnd <0)
|
|
invalEnd =0;
|
|
else if(invalEnd > 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
|
|
if(mRegions[i]->start <= invalEnd+1
|
|
&& mRegions[i]->end >= invalStart-1)
|
|
{
|
|
//take the union region
|
|
if(mRegions[i]->start > invalStart)
|
|
mRegions[i]->start = invalStart;
|
|
if(mRegions[i]->end < invalEnd)
|
|
mRegions[i]->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(mRegions[i]->end < invalStart)
|
|
// {
|
|
// InvalidRegion* newRegion = new InvalidRegion(invalStart,invalEnd);
|
|
// mRegions.insert(mRegions.begin()+i,newRegion);
|
|
// break;
|
|
// }
|
|
}
|
|
}
|
|
|
|
if(!added)
|
|
{
|
|
InvalidRegion* newRegion = new InvalidRegion(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
|
|
if(mRegions[i]->start <= mRegions[i-1]->end+1
|
|
&& mRegions[i]->end >= mRegions[i-1]->start-1)
|
|
{
|
|
//take the union region
|
|
if(mRegions[i]->start > mRegions[i-1]->start)
|
|
mRegions[i]->start = mRegions[i-1]->start;
|
|
if(mRegions[i]->end < mRegions[i-1]->end)
|
|
mRegions[i]->end = mRegions[i-1]->end;
|
|
|
|
//now we must delete the previous region
|
|
delete mRegions[i-1];
|
|
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(mRegions[i]->start > invalEnd)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//lock before calling these in a section. unlock after finished.
|
|
int GetNumInvalidRegions() const {return mRegions.size();}
|
|
int GetInvalidRegionStart(int i) const {return mRegions[i]->start;}
|
|
int GetInvalidRegionEnd(int i) const {return mRegions[i]->end;}
|
|
|
|
void ClearInvalidRegions()
|
|
{
|
|
for(size_t i =0;i<mRegions.size();i++)
|
|
{
|
|
delete mRegions[i];
|
|
}
|
|
mRegions.clear();
|
|
}
|
|
|
|
void LoadInvalidRegion(int ii, Sequence *sequence, bool updateODCount)
|
|
{
|
|
const int invStart = GetInvalidRegionStart(ii);
|
|
const int 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(int start, int end)
|
|
{
|
|
using namespace std;
|
|
const int *begin = &bl[0];
|
|
return count_if(begin + start, begin + end, bind2nd(less<int>(), 0));
|
|
}
|
|
|
|
protected:
|
|
std::vector<InvalidRegion*> mRegions;
|
|
ODLock mRegionsMutex;
|
|
|
|
};
|
|
|
|
#ifdef EXPERIMENTAL_USE_REALFFTF
|
|
#include "FFT.h"
|
|
static void ComputeSpectrumUsingRealFFTf
|
|
(float *buffer, HFFT hFFT, const float *window, int len, float *out)
|
|
{
|
|
int 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*log10(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);
|
|
}
|
|
}
|
|
#endif // EXPERIMENTAL_USE_REALFFTF
|
|
|
|
WaveClip::WaveClip(DirManager *projDirManager, sampleFormat format, int rate)
|
|
{
|
|
mOffset = 0;
|
|
mRate = rate;
|
|
mSequence = new Sequence(projDirManager, format);
|
|
mEnvelope = new Envelope();
|
|
mWaveCache = new WaveCache();
|
|
mSpecCache = new SpecCache();
|
|
mSpecPxCache = new SpecPxCache(1);
|
|
mAppendBuffer = NULL;
|
|
mAppendBufferLen = 0;
|
|
mDirty = 0;
|
|
mIsPlaceholder = false;
|
|
}
|
|
|
|
WaveClip::WaveClip(const WaveClip& orig, DirManager *projDirManager)
|
|
{
|
|
// 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;
|
|
mSequence = new Sequence(*orig.mSequence, projDirManager);
|
|
mEnvelope = new Envelope();
|
|
mEnvelope->Paste(0.0, orig.mEnvelope);
|
|
mEnvelope->SetOffset(orig.GetOffset());
|
|
mEnvelope->SetTrackLen(((double)orig.mSequence->GetNumSamples()) / orig.mRate);
|
|
mWaveCache = new WaveCache();
|
|
mSpecCache = new SpecCache();
|
|
mSpecPxCache = new SpecPxCache(1);
|
|
|
|
for (WaveClipList::compatibility_iterator it=orig.mCutLines.GetFirst(); it; it=it->GetNext())
|
|
mCutLines.Append(new WaveClip(*it->GetData(), projDirManager));
|
|
|
|
mAppendBuffer = NULL;
|
|
mAppendBufferLen = 0;
|
|
mDirty = 0;
|
|
mIsPlaceholder = orig.GetIsPlaceholder();
|
|
}
|
|
|
|
WaveClip::~WaveClip()
|
|
{
|
|
delete mSequence;
|
|
|
|
delete mEnvelope;
|
|
mEnvelope = NULL;
|
|
|
|
delete mWaveCache;
|
|
delete mSpecCache;
|
|
delete mSpecPxCache;
|
|
|
|
if (mAppendBuffer)
|
|
DeleteSamples(mAppendBuffer);
|
|
|
|
mCutLines.DeleteContents(true);
|
|
mCutLines.Clear();
|
|
}
|
|
|
|
void WaveClip::SetOffset(double offset)
|
|
{
|
|
mOffset = offset;
|
|
mEnvelope->SetOffset(mOffset);
|
|
}
|
|
|
|
bool WaveClip::GetSamples(samplePtr buffer, sampleFormat format,
|
|
sampleCount start, sampleCount len) const
|
|
{
|
|
return mSequence->Get(buffer, format, start, len);
|
|
}
|
|
|
|
bool WaveClip::SetSamples(samplePtr buffer, sampleFormat format,
|
|
sampleCount start, sampleCount len)
|
|
{
|
|
bool bResult = mSequence->Set(buffer, format, start, len);
|
|
MarkChanged();
|
|
return bResult;
|
|
}
|
|
|
|
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
|
|
{
|
|
sampleCount numSamples = mSequence->GetNumSamples();
|
|
|
|
double maxLen = mOffset + double(numSamples+mAppendBufferLen)/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
|
|
{
|
|
sampleCount ts = (sampleCount)floor(t * mRate + 0.5);
|
|
return ts > GetStartSample() && ts < GetEndSample() + mAppendBufferLen;
|
|
}
|
|
|
|
bool WaveClip::BeforeClip(double t) const
|
|
{
|
|
sampleCount ts = (sampleCount)floor(t * mRate + 0.5);
|
|
return ts <= GetStartSample();
|
|
}
|
|
|
|
bool WaveClip::AfterClip(double t) const
|
|
{
|
|
sampleCount ts = (sampleCount)floor(t * mRate + 0.5);
|
|
return ts >= GetEndSample() + mAppendBufferLen;
|
|
}
|
|
|
|
///Delete the wave cache - force redraw. Thread-safe
|
|
void WaveClip::DeleteWaveCache()
|
|
{
|
|
ODLocker locker(mWaveCacheMutex);
|
|
if(mWaveCache!=NULL)
|
|
delete mWaveCache;
|
|
mWaveCache = new WaveCache();
|
|
}
|
|
|
|
///Adds an invalid region to the wavecache so it redraws that portion only.
|
|
void WaveClip::AddInvalidRegion(long startSample, long endSample)
|
|
{
|
|
ODLocker locker(mWaveCacheMutex);
|
|
if(mWaveCache!=NULL)
|
|
mWaveCache->AddInvalidRegion(startSample,endSample);
|
|
}
|
|
|
|
namespace {
|
|
|
|
inline
|
|
void findCorrection(const std::vector<sampleCount> &oldWhere, int oldLen, int 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] - 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, int 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 (sampleCount 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 bool allocated = (display.where != 0);
|
|
|
|
const int numPixels = display.width;
|
|
|
|
int p0 = 0; // least column requiring computation
|
|
int 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, 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::auto_ptr<WaveCache> oldCache(mWaveCache);
|
|
mWaveCache = 0;
|
|
|
|
int oldX0 = 0;
|
|
double correction = 0.0;
|
|
int 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,
|
|
copyBegin + oldCache->len - std::max(0, oldX0)
|
|
);
|
|
}
|
|
if (!(copyEnd > copyBegin))
|
|
oldCache.reset(0);
|
|
|
|
mWaveCache = new 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.get()) {
|
|
|
|
//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, 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 = 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 */
|
|
|
|
int numSamples = mSequence->GetNumSamples();
|
|
int a;
|
|
|
|
// Not all of the required columns might be in the sequence.
|
|
// Some might be in the append buffer.
|
|
for (a = p0; 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) {
|
|
int i;
|
|
|
|
sampleFormat seqFormat = mSequence->GetSampleFormat();
|
|
bool didUpdate = false;
|
|
for(i=a; i<p1; i++) {
|
|
sampleCount left;
|
|
left = where[i] - numSamples;
|
|
sampleCount right;
|
|
right = where[i + 1] - numSamples;
|
|
|
|
//wxCriticalSectionLocker locker(mAppendCriticalSection);
|
|
|
|
if (left < 0)
|
|
left = 0;
|
|
if (right > mAppendBufferLen)
|
|
right = mAppendBufferLen;
|
|
|
|
if (right > left) {
|
|
float *b;
|
|
sampleCount len = right-left;
|
|
sampleCount j;
|
|
|
|
if (seqFormat == floatSample)
|
|
b = &((float *)mAppendBuffer)[left];
|
|
else {
|
|
b = new float[len];
|
|
CopySamples(mAppendBuffer + left*SAMPLE_SIZE(seqFormat),
|
|
seqFormat,
|
|
(samplePtr)b, floatSample, len);
|
|
}
|
|
|
|
float theMax, theMin, sumsq;
|
|
{
|
|
const float val = b[0];
|
|
theMax = theMin = val;
|
|
sumsq = val * val;
|
|
}
|
|
for(j=1; j<len; j++) {
|
|
const float val = b[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.
|
|
|
|
if (seqFormat != floatSample)
|
|
delete[] b;
|
|
|
|
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
|
|
(int 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;
|
|
|
|
const int 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 (sampleCount 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,
|
|
int xx, sampleCount numSamples,
|
|
double offset, double rate, double pixelsPerSecond,
|
|
int lowerBoundX, int upperBoundX,
|
|
const std::vector<float> &gainFactors,
|
|
float *scratch)
|
|
{
|
|
bool result = false;
|
|
const bool reassignment =
|
|
(settings.algorithm == SpectrogramSettings::algReassignment);
|
|
const int windowSize = settings.windowSize;
|
|
|
|
sampleCount start;
|
|
if (xx < 0)
|
|
start = where[0] + xx * (rate / pixelsPerSecond);
|
|
else if (xx > len)
|
|
start = where[len] + (xx - len) * (rate / pixelsPerSecond);
|
|
else
|
|
start = where[xx];
|
|
|
|
const bool autocorrelation =
|
|
settings.algorithm == SpectrogramSettings::algPitchEAC;
|
|
const int zeroPaddingFactor = (autocorrelation ? 1 : settings.zeroPaddingFactor);
|
|
const int padding = (windowSize * (zeroPaddingFactor - 1)) / 2;
|
|
const int fftLen = windowSize * zeroPaddingFactor;
|
|
const int half = fftLen / 2;
|
|
|
|
if (start <= 0 || start >= numSamples) {
|
|
if (xx >= 0 && xx < len) {
|
|
// Pixel column is out of bounds of the clip! Should not happen.
|
|
float *const results = &freq[half * xx];
|
|
std::fill(results, results + half, 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;
|
|
|
|
{
|
|
sampleCount myLen = windowSize;
|
|
// Take a window of the track centered at this sample.
|
|
start -= windowSize >> 1;
|
|
if (start < 0) {
|
|
// Near the start of the clip, pad left with zeroes as needed.
|
|
for (sampleCount ii = start; ii < 0; ++ii)
|
|
*adj++ = 0;
|
|
myLen += start;
|
|
start = 0;
|
|
copy = true;
|
|
}
|
|
|
|
if (start + myLen > numSamples) {
|
|
// Near the end of the clip, pad right with zeroes as needed.
|
|
int newlen = numSamples - start;
|
|
for (sampleCount ii = newlen; ii < (sampleCount)myLen; ++ii)
|
|
adj[ii] = 0;
|
|
myLen = newlen;
|
|
copy = true;
|
|
}
|
|
|
|
if (myLen > 0) {
|
|
useBuffer = (float*)(waveTrackCache.Get(floatSample,
|
|
floor(0.5 + start + offset * rate), myLen));
|
|
if (copy)
|
|
memcpy(adj, useBuffer, myLen * sizeof(float));
|
|
}
|
|
}
|
|
|
|
if (copy)
|
|
useBuffer = scratch;
|
|
|
|
#ifdef EXPERIMENTAL_USE_REALFFTF
|
|
if (autocorrelation) {
|
|
float *const results = &freq[half * xx];
|
|
// This function does not mutate useBuffer
|
|
ComputeSpectrum(useBuffer, windowSize, windowSize,
|
|
rate, results,
|
|
autocorrelation, settings.windowType);
|
|
}
|
|
else if (reassignment) {
|
|
static const double epsilon = 1e-16;
|
|
const HFFT hFFT = settings.hFFT;
|
|
|
|
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;
|
|
for (int ii = 0; ii < fftLen; ++ii)
|
|
scratch[ii] *= window[ii];
|
|
RealFFTf(scratch, hFFT);
|
|
}
|
|
|
|
{
|
|
const float *const dWindow = settings.dWindow;
|
|
for (int ii = 0; ii < fftLen; ++ii)
|
|
scratch2[ii] *= dWindow[ii];
|
|
RealFFTf(scratch2, hFFT);
|
|
}
|
|
|
|
{
|
|
const float *const tWindow = settings.tWindow;
|
|
for (int ii = 0; ii < fftLen; ++ii)
|
|
scratch3[ii] *= tWindow[ii];
|
|
RealFFTf(scratch3, hFFT);
|
|
}
|
|
|
|
for (int 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(ii + freqCorrection + 0.5f);
|
|
if (bin >= 0 && bin < 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,
|
|
freq[half * correctedX + bin] += power;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
float *const results = &freq[half * 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, settings.window, fftLen, results);
|
|
if (!gainFactors.empty()) {
|
|
// Apply a frequency-dependant gain factor
|
|
for (int ii = 0; ii < half; ++ii)
|
|
results[ii] += gainFactors[ii];
|
|
}
|
|
}
|
|
#else // EXPERIMENTAL_USE_REALFFTF
|
|
// This function does not mutate scratch
|
|
ComputeSpectrum(scratch, windowSize, windowSize,
|
|
rate, results,
|
|
autocorrelation, settings.windowType);
|
|
#endif // EXPERIMENTAL_USE_REALFFTF
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void SpecCache::Populate
|
|
(const SpectrogramSettings &settings, WaveTrackCache &waveTrackCache,
|
|
int copyBegin, int copyEnd, int numPixels,
|
|
sampleCount numSamples,
|
|
double offset, double rate, double pixelsPerSecond)
|
|
{
|
|
#ifdef EXPERIMENTAL_USE_REALFFTF
|
|
settings.CacheWindows();
|
|
#endif
|
|
|
|
const int &frequencyGain = settings.frequencyGain;
|
|
const int &windowSize = settings.windowSize;
|
|
const bool autocorrelation =
|
|
settings.algorithm == SpectrogramSettings::algPitchEAC;
|
|
const bool reassignment =
|
|
settings.algorithm == SpectrogramSettings::algReassignment;
|
|
#ifdef EXPERIMENTAL_ZERO_PADDED_SPECTROGRAMS
|
|
const int &zeroPaddingFactor = autocorrelation ? 1 : settings.zeroPaddingFactor;
|
|
#else
|
|
const int zeroPaddingFactor = 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 int fftLen = windowSize * zeroPaddingFactor;
|
|
const int half = fftLen / 2;
|
|
|
|
const size_t bufferSize = fftLen;
|
|
|
|
std::vector<float> buffer(reassignment ? 3 * bufferSize : bufferSize);
|
|
|
|
std::vector<float> gainFactors;
|
|
if (!autocorrelation)
|
|
ComputeSpectrogramGainFactors(fftLen, rate, frequencyGain, 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;
|
|
for (sampleCount xx = lowerBoundX; xx < upperBoundX; ++xx)
|
|
CalculateOneSpectrum(
|
|
settings, waveTrackCache, xx, numSamples,
|
|
offset, rate, pixelsPerSecond,
|
|
lowerBoundX, upperBoundX,
|
|
gainFactors, &buffer[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?
|
|
sampleCount 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, &buffer[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, &buffer[0]);
|
|
if (!result)
|
|
break;
|
|
}
|
|
|
|
// Now Convert to dB terms. Do this only after accumulating
|
|
// power values, which may cross columns with the time correction.
|
|
for (sampleCount xx = lowerBoundX; xx < upperBoundX; ++xx) {
|
|
float *const results = &freq[half * xx];
|
|
const HFFT hFFT = settings.hFFT;
|
|
for (int ii = 0; ii < hFFT->Points; ++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 (int ii = 0; ii < half; ++ii)
|
|
results[ii] += gainFactors[ii];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool WaveClip::GetSpectrogram(WaveTrackCache &waveTrackCache,
|
|
const float *& spectrogram, const sampleCount *& where,
|
|
int numPixels,
|
|
double t0, double pixelsPerSecond)
|
|
{
|
|
const WaveTrack *const track = waveTrackCache.GetTrack();
|
|
const SpectrogramSettings &settings = track->GetSpectrogramSettings();
|
|
const bool autocorrelation =
|
|
settings.algorithm == SpectrogramSettings::algPitchEAC;
|
|
const int &frequencyGain = settings.frequencyGain;
|
|
const int &windowSize = settings.windowSize;
|
|
const int &windowType = settings.windowType;
|
|
#ifdef EXPERIMENTAL_ZERO_PADDED_SPECTROGRAMS
|
|
const int &zeroPaddingFactor = autocorrelation ? 1 : settings.zeroPaddingFactor;
|
|
#else
|
|
const int zeroPaddingFactor = 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 int fftLen = windowSize * zeroPaddingFactor;
|
|
const int half = fftLen / 2;
|
|
|
|
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
|
|
}
|
|
|
|
if (settings.algorithm == SpectrogramSettings::algReassignment)
|
|
// Caching is not implemented for reassignment, unless for
|
|
// a complete hit, because of the complications of time reassignment
|
|
match = false;
|
|
|
|
std::auto_ptr<SpecCache> oldCache(mSpecCache);
|
|
mSpecCache = 0;
|
|
|
|
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(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,
|
|
copyBegin + oldCache->len - std::max(0, oldX0)
|
|
);
|
|
}
|
|
|
|
if (!(copyEnd > copyBegin))
|
|
oldCache.reset(0);
|
|
|
|
mSpecCache = new SpecCache(
|
|
numPixels, settings.algorithm, pixelsPerSecond, t0,
|
|
windowType, windowSize, zeroPaddingFactor, frequencyGain);
|
|
|
|
// 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);
|
|
|
|
// Optimization: if the old cache is good and overlaps
|
|
// with the current one, re-use as much of the cache as
|
|
// possible
|
|
if (oldCache.get()) {
|
|
memcpy(&mSpecCache->freq[half * copyBegin],
|
|
&oldCache->freq[half * (copyBegin + oldX0)],
|
|
half * (copyEnd - copyBegin) * sizeof(float));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool WaveClip::GetMinMax(float *min, float *max,
|
|
double t0, double t1)
|
|
{
|
|
*min = float(0.0); // harmless, but unused since Sequence::GetMinMax does not use these values
|
|
*max = float(0.0); // harmless, but unused since Sequence::GetMinMax does not use these values
|
|
|
|
if (t0 > t1)
|
|
return false;
|
|
|
|
if (t0 == t1)
|
|
return true;
|
|
|
|
sampleCount s0, s1;
|
|
|
|
TimeToSamplesClip(t0, &s0);
|
|
TimeToSamplesClip(t1, &s1);
|
|
|
|
return mSequence->GetMinMax(s0, s1-s0, min, max);
|
|
}
|
|
|
|
bool WaveClip::GetRMS(float *rms, double t0,
|
|
double t1)
|
|
{
|
|
*rms = float(0.0);
|
|
|
|
if (t0 > t1)
|
|
return false;
|
|
|
|
if (t0 == t1)
|
|
return true;
|
|
|
|
sampleCount s0, s1;
|
|
|
|
TimeToSamplesClip(t0, &s0);
|
|
TimeToSamplesClip(t1, &s1);
|
|
|
|
return mSequence->GetRMS(s0, s1-s0, rms);
|
|
}
|
|
|
|
void WaveClip::ConvertToSampleFormat(sampleFormat format)
|
|
{
|
|
bool bChanged;
|
|
bool bResult = mSequence->ConvertToSampleFormat(format, &bChanged);
|
|
if (bResult && bChanged)
|
|
MarkChanged();
|
|
wxASSERT(bResult); // TODO: Throw an actual error.
|
|
}
|
|
|
|
void WaveClip::UpdateEnvelopeTrackLen()
|
|
{
|
|
mEnvelope->SetTrackLen(((double)mSequence->GetNumSamples()) / mRate);
|
|
}
|
|
|
|
void WaveClip::TimeToSamplesClip(double t0, sampleCount *s0) const
|
|
{
|
|
if (t0 < mOffset)
|
|
*s0 = 0;
|
|
else if (t0 > mOffset + double(mSequence->GetNumSamples())/mRate)
|
|
*s0 = mSequence->GetNumSamples();
|
|
else
|
|
*s0 = (sampleCount)floor(((t0 - mOffset) * mRate) + 0.5);
|
|
}
|
|
|
|
void WaveClip::ClearDisplayRect()
|
|
{
|
|
mDisplayRect.x = mDisplayRect.y = -1;
|
|
mDisplayRect.width = mDisplayRect.height = -1;
|
|
}
|
|
|
|
void WaveClip::SetDisplayRect(const wxRect& r)
|
|
{
|
|
mDisplayRect = r;
|
|
}
|
|
|
|
void WaveClip::GetDisplayRect(wxRect* r)
|
|
{
|
|
*r = mDisplayRect;
|
|
}
|
|
|
|
bool WaveClip::Append(samplePtr buffer, sampleFormat format,
|
|
sampleCount len, unsigned int stride /* = 1 */,
|
|
XMLWriter* blockFileLog /*=NULL*/)
|
|
{
|
|
//wxLogDebug(wxT("Append: len=%lli"), (long long) len);
|
|
|
|
sampleCount maxBlockSize = mSequence->GetMaxBlockSize();
|
|
sampleCount blockSize = mSequence->GetIdealAppendLen();
|
|
sampleFormat seqFormat = mSequence->GetSampleFormat();
|
|
|
|
if (!mAppendBuffer)
|
|
mAppendBuffer = NewSamples(maxBlockSize, seqFormat);
|
|
|
|
for(;;) {
|
|
if (mAppendBufferLen >= blockSize) {
|
|
bool success =
|
|
mSequence->Append(mAppendBuffer, seqFormat, blockSize,
|
|
blockFileLog);
|
|
if (!success)
|
|
return false;
|
|
memmove(mAppendBuffer,
|
|
mAppendBuffer + blockSize * SAMPLE_SIZE(seqFormat),
|
|
(mAppendBufferLen - blockSize) * SAMPLE_SIZE(seqFormat));
|
|
mAppendBufferLen -= blockSize;
|
|
blockSize = mSequence->GetIdealAppendLen();
|
|
}
|
|
|
|
if (len == 0)
|
|
break;
|
|
|
|
int toCopy = maxBlockSize - mAppendBufferLen;
|
|
if (toCopy > len)
|
|
toCopy = len;
|
|
|
|
CopySamples(buffer, format,
|
|
mAppendBuffer + mAppendBufferLen * SAMPLE_SIZE(seqFormat),
|
|
seqFormat,
|
|
toCopy,
|
|
true, // high quality
|
|
stride);
|
|
|
|
mAppendBufferLen += toCopy;
|
|
buffer += toCopy * SAMPLE_SIZE(format) * stride;
|
|
len -= toCopy;
|
|
}
|
|
|
|
UpdateEnvelopeTrackLen();
|
|
MarkChanged();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool WaveClip::AppendAlias(wxString fName, sampleCount start,
|
|
sampleCount len, int channel,bool useOD)
|
|
{
|
|
bool result = mSequence->AppendAlias(fName, start, len, channel,useOD);
|
|
if (result)
|
|
{
|
|
UpdateEnvelopeTrackLen();
|
|
MarkChanged();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool WaveClip::AppendCoded(wxString fName, sampleCount start,
|
|
sampleCount len, int channel, int decodeType)
|
|
{
|
|
bool result = mSequence->AppendCoded(fName, start, len, channel, decodeType);
|
|
if (result)
|
|
{
|
|
UpdateEnvelopeTrackLen();
|
|
MarkChanged();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool WaveClip::Flush()
|
|
{
|
|
//wxLogDebug(wxT("WaveClip::Flush"));
|
|
//wxLogDebug(wxT(" mAppendBufferLen=%lli"), (long long) mAppendBufferLen);
|
|
//wxLogDebug(wxT(" previous sample count %lli"), (long long) mSequence->GetNumSamples());
|
|
|
|
bool success = true;
|
|
if (mAppendBufferLen > 0) {
|
|
success = mSequence->Append(mAppendBuffer, mSequence->GetSampleFormat(), mAppendBufferLen);
|
|
if (success) {
|
|
mAppendBufferLen = 0;
|
|
UpdateEnvelopeTrackLen();
|
|
MarkChanged();
|
|
}
|
|
}
|
|
|
|
//wxLogDebug(wxT("now sample count %lli"), (long long) mSequence->GetNumSamples());
|
|
|
|
return success;
|
|
}
|
|
|
|
bool WaveClip::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
|
|
{
|
|
if (!wxStrcmp(tag, wxT("waveclip")))
|
|
{
|
|
double dblValue;
|
|
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);
|
|
}
|
|
}
|
|
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;
|
|
else if (!wxStrcmp(tag, wxT("envelope")))
|
|
return mEnvelope;
|
|
else if (!wxStrcmp(tag, wxT("waveclip")))
|
|
{
|
|
// Nested wave clips are cut lines
|
|
WaveClip *newCutLine = new WaveClip(mSequence->GetDirManager(),
|
|
mSequence->GetSampleFormat(), mRate);
|
|
mCutLines.Append(newCutLine);
|
|
return newCutLine;
|
|
} else
|
|
return NULL;
|
|
}
|
|
|
|
void WaveClip::WriteXML(XMLWriter &xmlFile)
|
|
{
|
|
xmlFile.StartTag(wxT("waveclip"));
|
|
xmlFile.WriteAttr(wxT("offset"), mOffset, 8);
|
|
|
|
mSequence->WriteXML(xmlFile);
|
|
mEnvelope->WriteXML(xmlFile);
|
|
|
|
for (WaveClipList::compatibility_iterator it=mCutLines.GetFirst(); it; it=it->GetNext())
|
|
it->GetData()->WriteXML(xmlFile);
|
|
|
|
xmlFile.EndTag(wxT("waveclip"));
|
|
}
|
|
|
|
bool WaveClip::CreateFromCopy(double t0, double t1, WaveClip* other)
|
|
{
|
|
sampleCount s0, s1;
|
|
|
|
other->TimeToSamplesClip(t0, &s0);
|
|
other->TimeToSamplesClip(t1, &s1);
|
|
|
|
Sequence* oldSequence = mSequence;
|
|
mSequence = NULL;
|
|
if (!other->mSequence->Copy(s0, s1, &mSequence))
|
|
{
|
|
mSequence = oldSequence;
|
|
return false;
|
|
}
|
|
|
|
delete oldSequence;
|
|
delete mEnvelope;
|
|
mEnvelope = new Envelope();
|
|
mEnvelope->CopyFrom(other->mEnvelope, (double)s0/mRate, (double)s1/mRate);
|
|
|
|
MarkChanged();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool WaveClip::Paste(double t0, const WaveClip* other)
|
|
{
|
|
const bool clipNeedsResampling = other->mRate != mRate;
|
|
const bool clipNeedsNewFormat =
|
|
other->mSequence->GetSampleFormat() != mSequence->GetSampleFormat();
|
|
std::auto_ptr<WaveClip> newClip;
|
|
const WaveClip* pastedClip;
|
|
|
|
if (clipNeedsResampling || clipNeedsNewFormat)
|
|
{
|
|
newClip.reset(new WaveClip(*other, mSequence->GetDirManager()));
|
|
if (clipNeedsResampling)
|
|
// The other clip's rate is different from ours, so resample
|
|
if (!newClip->Resample(mRate))
|
|
return false;
|
|
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;
|
|
}
|
|
|
|
sampleCount s0;
|
|
TimeToSamplesClip(t0, &s0);
|
|
|
|
bool result = false;
|
|
if (mSequence->Paste(s0, pastedClip->mSequence))
|
|
{
|
|
MarkChanged();
|
|
mEnvelope->Paste((double)s0/mRate + mOffset, pastedClip->mEnvelope);
|
|
mEnvelope->RemoveUnneededPoints();
|
|
OffsetCutLines(t0, pastedClip->GetEndTime() - pastedClip->GetStartTime());
|
|
|
|
// Paste cut lines contained in pasted clip
|
|
for (WaveClipList::compatibility_iterator it = pastedClip->mCutLines.GetFirst(); it; it=it->GetNext())
|
|
{
|
|
WaveClip* cutline = it->GetData();
|
|
WaveClip* newCutLine = new WaveClip(*cutline,
|
|
mSequence->GetDirManager());
|
|
newCutLine->Offset(t0 - mOffset);
|
|
mCutLines.Append(newCutLine);
|
|
}
|
|
|
|
result = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool WaveClip::InsertSilence(double t, double len)
|
|
{
|
|
sampleCount s0;
|
|
TimeToSamplesClip(t, &s0);
|
|
sampleCount slen = (sampleCount)floor(len * mRate + 0.5);
|
|
|
|
if (!GetSequence()->InsertSilence(s0, slen))
|
|
{
|
|
wxASSERT(false);
|
|
return false;
|
|
}
|
|
OffsetCutLines(t, len);
|
|
GetEnvelope()->InsertSpace(t, len);
|
|
MarkChanged();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool WaveClip::Clear(double t0, double t1)
|
|
{
|
|
sampleCount s0, s1;
|
|
|
|
TimeToSamplesClip(t0, &s0);
|
|
TimeToSamplesClip(t1, &s1);
|
|
|
|
if (GetSequence()->Delete(s0, s1-s0))
|
|
{
|
|
// 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();
|
|
|
|
WaveClipList::compatibility_iterator nextIt;
|
|
|
|
for (WaveClipList::compatibility_iterator it = mCutLines.GetFirst(); it; it=nextIt)
|
|
{
|
|
nextIt = it->GetNext();
|
|
WaveClip* clip = it->GetData();
|
|
double cutlinePosition = mOffset + clip->GetOffset();
|
|
if (cutlinePosition >= t0 && cutlinePosition <= t1)
|
|
{
|
|
// This cutline is within the area, delete it
|
|
delete clip;
|
|
mCutLines.DeleteNode(it);
|
|
} else
|
|
if (cutlinePosition >= t1)
|
|
{
|
|
clip->Offset(clip_t0-clip_t1);
|
|
}
|
|
}
|
|
|
|
// Collapse envelope
|
|
GetEnvelope()->CollapseRegion(t0, t1);
|
|
if (t0 < GetStartTime())
|
|
Offset(-(GetStartTime() - t0));
|
|
|
|
MarkChanged();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool WaveClip::ClearAndAddCutLine(double t0, double t1)
|
|
{
|
|
if (t0 > GetEndTime() || t1 < GetStartTime())
|
|
return true; // time out of bounds
|
|
|
|
WaveClip *newClip = new WaveClip(mSequence->GetDirManager(),
|
|
mSequence->GetSampleFormat(),
|
|
mRate);
|
|
double clip_t0 = t0;
|
|
double clip_t1 = t1;
|
|
if (clip_t0 < GetStartTime())
|
|
clip_t0 = GetStartTime();
|
|
if (clip_t1 > GetEndTime())
|
|
clip_t1 = GetEndTime();
|
|
|
|
if (!newClip->CreateFromCopy(clip_t0, clip_t1, this))
|
|
return false;
|
|
newClip->SetOffset(clip_t0-mOffset);
|
|
|
|
// Sort out cutlines that belong to the new cutline
|
|
WaveClipList::compatibility_iterator nextIt;
|
|
|
|
for (WaveClipList::compatibility_iterator it = mCutLines.GetFirst(); it; it=nextIt)
|
|
{
|
|
nextIt = it->GetNext();
|
|
WaveClip* clip = it->GetData();
|
|
double cutlinePosition = mOffset + clip->GetOffset();
|
|
if (cutlinePosition >= t0 && cutlinePosition <= t1)
|
|
{
|
|
clip->SetOffset(cutlinePosition - newClip->GetOffset() - mOffset);
|
|
newClip->mCutLines.Append(clip);
|
|
mCutLines.DeleteNode(it);
|
|
} else
|
|
if (cutlinePosition >= t1)
|
|
{
|
|
clip->Offset(clip_t0-clip_t1);
|
|
}
|
|
}
|
|
|
|
// Clear actual audio data
|
|
sampleCount s0, s1;
|
|
|
|
TimeToSamplesClip(t0, &s0);
|
|
TimeToSamplesClip(t1, &s1);
|
|
|
|
if (GetSequence()->Delete(s0, s1-s0))
|
|
{
|
|
// Collapse envelope
|
|
GetEnvelope()->CollapseRegion(t0, t1);
|
|
if (t0 < GetStartTime())
|
|
Offset(-(GetStartTime() - t0));
|
|
|
|
MarkChanged();
|
|
|
|
mCutLines.Append(newClip);
|
|
return true;
|
|
} else
|
|
{
|
|
delete newClip;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool WaveClip::FindCutLine(double cutLinePosition,
|
|
double* cutlineStart /* = NULL */,
|
|
double* cutlineEnd /* = NULL */)
|
|
{
|
|
for (WaveClipList::compatibility_iterator it = mCutLines.GetFirst(); it; it=it->GetNext())
|
|
{
|
|
WaveClip* cutline = it->GetData();
|
|
if (fabs(mOffset + cutline->GetOffset() - cutLinePosition) < 0.0001)
|
|
{
|
|
if (cutlineStart)
|
|
*cutlineStart = mOffset+cutline->GetStartTime();
|
|
if (cutlineEnd)
|
|
*cutlineEnd = mOffset+cutline->GetEndTime();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool WaveClip::ExpandCutLine(double cutLinePosition)
|
|
{
|
|
for (WaveClipList::compatibility_iterator it = mCutLines.GetFirst(); it; it=it->GetNext())
|
|
{
|
|
WaveClip* cutline = it->GetData();
|
|
if (fabs(mOffset + cutline->GetOffset() - cutLinePosition) < 0.0001)
|
|
{
|
|
if (!Paste(mOffset+cutline->GetOffset(), cutline))
|
|
return false;
|
|
delete cutline;
|
|
mCutLines.DeleteNode(it);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool WaveClip::RemoveCutLine(double cutLinePosition)
|
|
{
|
|
for (WaveClipList::compatibility_iterator it = mCutLines.GetFirst(); it; it=it->GetNext())
|
|
{
|
|
if (fabs(mOffset + it->GetData()->GetOffset() - cutLinePosition) < 0.0001)
|
|
{
|
|
delete it->GetData();
|
|
mCutLines.DeleteNode(it);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void WaveClip::RemoveAllCutLines()
|
|
{
|
|
while (!mCutLines.IsEmpty())
|
|
{
|
|
WaveClipList::compatibility_iterator head = mCutLines.GetFirst();
|
|
delete head->GetData();
|
|
mCutLines.DeleteNode(head);
|
|
}
|
|
}
|
|
|
|
void WaveClip::OffsetCutLines(double t0, double len)
|
|
{
|
|
for (WaveClipList::compatibility_iterator it = mCutLines.GetFirst(); it; it=it->GetNext())
|
|
{
|
|
WaveClip* cutLine = it->GetData();
|
|
if (mOffset + cutLine->GetOffset() >= t0)
|
|
cutLine->Offset(len);
|
|
}
|
|
}
|
|
|
|
void WaveClip::Lock()
|
|
{
|
|
GetSequence()->Lock();
|
|
for (WaveClipList::compatibility_iterator it = mCutLines.GetFirst(); it; it=it->GetNext())
|
|
it->GetData()->Lock();
|
|
}
|
|
|
|
void WaveClip::CloseLock()
|
|
{
|
|
GetSequence()->CloseLock();
|
|
for (WaveClipList::compatibility_iterator it = mCutLines.GetFirst(); it; it=it->GetNext())
|
|
it->GetData()->Lock();
|
|
}
|
|
|
|
void WaveClip::Unlock()
|
|
{
|
|
GetSequence()->Unlock();
|
|
for (WaveClipList::compatibility_iterator it = mCutLines.GetFirst(); it; it=it->GetNext())
|
|
it->GetData()->Unlock();
|
|
}
|
|
|
|
void WaveClip::SetRate(int rate)
|
|
{
|
|
mRate = rate;
|
|
UpdateEnvelopeTrackLen();
|
|
MarkChanged();
|
|
}
|
|
|
|
bool WaveClip::Resample(int rate, ProgressDialog *progress)
|
|
{
|
|
if (rate == mRate)
|
|
return true; // Nothing to do
|
|
|
|
double factor = (double)rate / (double)mRate;
|
|
::Resample* resample = new ::Resample(true, factor, factor); // constant rate resampling
|
|
|
|
int bufsize = 65536;
|
|
float* inBuffer = new float[bufsize];
|
|
float* outBuffer = new float[bufsize];
|
|
sampleCount pos = 0;
|
|
bool error = false;
|
|
int outGenerated = 0;
|
|
sampleCount numSamples = mSequence->GetNumSamples();
|
|
|
|
Sequence* newSequence =
|
|
new 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)
|
|
{
|
|
int inLen = numSamples - pos;
|
|
if (inLen > bufsize)
|
|
inLen = bufsize;
|
|
|
|
bool isLast = ((pos + inLen) == numSamples);
|
|
|
|
if (!mSequence->Get((samplePtr)inBuffer, floatSample, pos, inLen))
|
|
{
|
|
error = true;
|
|
break;
|
|
}
|
|
|
|
int inBufferUsed = 0;
|
|
outGenerated = resample->Process(factor, inBuffer, inLen, isLast,
|
|
&inBufferUsed, outBuffer, bufsize);
|
|
|
|
pos += inBufferUsed;
|
|
|
|
if (outGenerated < 0)
|
|
{
|
|
error = true;
|
|
break;
|
|
}
|
|
|
|
if (!newSequence->Append((samplePtr)outBuffer, floatSample,
|
|
outGenerated))
|
|
{
|
|
error = true;
|
|
break;
|
|
}
|
|
|
|
if (progress)
|
|
{
|
|
int updateResult = progress->Update(pos, numSamples);
|
|
error = (updateResult != eProgressSuccess);
|
|
if (error)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
delete[] inBuffer;
|
|
delete[] outBuffer;
|
|
delete resample;
|
|
|
|
if (error)
|
|
{
|
|
delete newSequence;
|
|
} else
|
|
{
|
|
delete mSequence;
|
|
mSequence = newSequence;
|
|
mRate = rate;
|
|
|
|
// Invalidate wave display cache
|
|
if (mWaveCache)
|
|
{
|
|
delete mWaveCache;
|
|
mWaveCache = NULL;
|
|
}
|
|
mWaveCache = new WaveCache();
|
|
// Invalidate the spectrum display cache
|
|
if (mSpecCache)
|
|
delete mSpecCache;
|
|
mSpecCache = new SpecCache();
|
|
}
|
|
|
|
return !error;
|
|
}
|