Actually - I can share it here with a few extras and people can do with it what they will. It could easily be turned into a control suitable to use with DrawData(). Some hacking will be required to disentangle from the underlying Plotter class, but probably not much.
I’m not sharing the plotter class, simply because it is pretty much is a close copy of DrawData() simply with a method for filling rather than stroking the data.I can post it if desired.
In header:
//////////////////////////////////////
// Spectral Plotter //
//////////////////////////////////////
class SpectralPlotter : protected Plotter
{
struct Point
{
Point(float xIn, float yIn) : x(xIn), y(yIn) {}
float x;
float y;
};
public:
SpectralPlotter(const IRECT& bounds);
void SetBounds(const IRECT& bounds) { Plotter::SetBounds(bounds); }
void SetFreqRange(float freqLo, float freqHi, float sampleRate);
void SetDBRange(float dbLo, float dbHi);
// N.B. size is the number of points, the last being the nyquist (and not the FFT size)
void Data(const float* powerSpectrum, unsigned long size);
void Plot(IGraphics& g, const Plotter::Style& style);
protected:
void AddPoint(const Point& p);
void ConvertAndAddPoints(Point& p1, Point& p2);
int NumPoints() { return static_cast<int>(mXPoints.size()); }
float CalcXNorm(float x) { return (logf(x) - mLogXLo) / (mLogXHi - mLogXLo); }
float CalcYNorm(float y) { return (logf(y) - mLogYLo) / (mLogYHi - mLogYLo); }
float mOptimiseX;
float mSmoothX;
float mLogXLo;
float mLogXHi;
float mLogYLo;
float mLogYHi;
std::vector<float> mXPoints;
std::vector<float> mYPoints;
};
In Source:
(this runs on fhe graphics thread and thus I don’t mind about allocating there, but if you wished the vector reserve stuff could be in a constructor (rather that Data() which is where you feed new data in). However, as vector never sizes down unless you explicitly make it happen this is going to be a one-off cost.
//////////////////////////////////////
// Spectral Plotter //
//////////////////////////////////////
SpectralPlotter::SpectralPlotter(const IRECT& bounds)
: Plotter(bounds), mOptimiseX(1.f), mSmoothX(1.f)
{
SetFreqRange(20.f, 20000.f, 44100.f);
SetDBRange(-100.f, 0.f);
}
void SpectralPlotter::SetFreqRange(float freqLo, float freqHi, float sampleRate)
{
mLogXLo = logf(freqLo / (sampleRate / 2.f));
mLogXHi = logf(freqHi / (sampleRate / 2.f));
}
void SpectralPlotter::SetDBRange(float dbLo, float dbHi)
{
mLogYLo = logf(powf(10.f, dbLo / 10.0));
mLogYHi = logf(powf(10.f, dbHi / 10.0));
}
// N.B. size is the number of points, the last being the nyquist (and not the FFT size)
void SpectralPlotter::Data(const float* powerSpectrum, unsigned long size)
{
mXPoints.reserve(size);
mYPoints.reserve(size);
mXPoints.clear();
mYPoints.clear();
float xRecip = 1.f / static_cast<float>(size);
float xAdvance = mOptimiseX / mBounds.W();
float xPrev = 0.f;
float ym2 = CalcYNorm(powerSpectrum[1]);
float ym1 = CalcYNorm(powerSpectrum[0]);
float yp0 = CalcYNorm(powerSpectrum[1]);
float yp1 = 0.0;
// Don't use the DC bin
unsigned long i = 1;
// Calculate Curve
for (; i < size; i++)
{
int N = NumPoints();
float x = CalcXNorm(i * xRecip);
// Add cubic smoothing if desired
if (i + 1 < size)
{
yp1 = CalcYNorm(powerSpectrum[i+1]);
if (mSmoothX)
{
float xInterp = 1.0 / (x - xPrev);
for (float xS = xPrev + xAdvance; xS < x - xAdvance; xS += xAdvance)
AddPoint(Point(xS, interpolateCubic((xS - xPrev) * xInterp, ym2, ym1, yp0, yp1)));
}
}
AddPoint(Point(x, yp0));
ym2 = ym1;
ym1 = yp0;
yp0 = yp1;
if (N && (XCalc(mXPoints[N]) - XCalc(mXPoints[N - 1])) < mOptimiseX)
break;
xPrev = x;
}
while (i < size)
{
Point minPoint(CalcXNorm(i * xRecip), powerSpectrum[i]);
Point maxPoint = minPoint;
float x = XCalc(minPoint.x);
float xNorm = CalcXNorm(++i * xRecip);
while (((XCalc(xNorm) - x) < mOptimiseX) && i < size)
{
if (powerSpectrum[i] < minPoint.y)
minPoint = Point(xNorm, powerSpectrum[i]);
if (powerSpectrum[i] > maxPoint.y)
maxPoint = Point(xNorm, powerSpectrum[i]);
xNorm = CalcXNorm(++i * xRecip);
}
if (minPoint.x < maxPoint.x)
ConvertAndAddPoints(minPoint, maxPoint);
else
ConvertAndAddPoints(maxPoint, minPoint);
}
}
void SpectralPlotter::Plot(IGraphics& g, const Plotter::Style& style)
{
Plotter::Plot(g, mXPoints.data(), mYPoints.data(), NumPoints(), style);
}
void SpectralPlotter::AddPoint(const Point& p)
{
mXPoints.push_back(p.x);
mYPoints.push_back(p.y);
}
void SpectralPlotter::ConvertAndAddPoints(Point& p1, Point& p2)
{
p1.y = CalcYNorm(p1.y);
p2.y = CalcYNorm(p2.y);
AddPoint(p1);
AddPoint(p2);
}
You’d also need the code for cubic interpolation which makes the low frequencies look nice…
This is a template because we use it for a mix of scalar and SIMD types, but it doesn’t need to be one for this usage (that’s also why all the inputs are passed by const reference - if I knew that the inputs were double/float I wouldn’t bother with that).
template <typename T>
inline T interpolateCubic(const T& x, const T& y0, const T& y1, const T& y2, const T& y3)
{
// N.B. - this is currently a high-quality cubic hermite
T c0 = y1;
T c1 = 0.5 * (y2 - y0);
T c2 = y0 - 2.5 * y1 + y2 + y2 - 0.5 * y3;
T c3 = 0.5 * (y3 - y0) + 1.5 * (y1 - y2);
return (((c3 * x + c2) * x + c1) * x + c0);
}