21#include "IPlugStructs.h"
24BEGIN_IGRAPHICS_NAMESPACE
30template <
int MAXNC = 2,
int MAX_FFT_SIZE = 4096>
37 kMsgTagSampleRate = 1,
44 static constexpr auto numExtraPoints = 2;
45 using TDataPacket = std::array<float, MAX_FFT_SIZE>;
46 enum class EChannelType { Left = 0, Right, LeftAndRight };
47 enum class EFrequencyScale { Linear, Log };
48 enum class EAmplitudeScale { Linear, Decibel };
63 std::initializer_list<IColor> colors = {COLOR_RED, COLOR_GREEN},
64 EFrequencyScale freqScale = EFrequencyScale::Log,
65 EAmplitudeScale ampScale = EAmplitudeScale::Decibel,
66 float curveThickness = 2.0,
67 float gridThickness = 1.0,
68 float fillOpacity = 0.25,
69 float attackTimeMs = 3.0,
70 float decayTimeMs = 50.0)
73 , mChannelColors(colors)
74 , mFreqScale(freqScale)
76 , mCurveThickness(curveThickness)
77 , mGridThickness(gridThickness)
78 , mFillOpacity(fillOpacity)
79 , mAttackTimeMs(attackTimeMs)
80 , mDecayTimeMs(decayTimeMs)
82 assert(colors.size() >= MAXNC);
85 SetFreqRange(FirstBinFreq(), NyquistFreq());
86 SetAmpRange(
DBToAmp(-90.0f), 1.0f);
92 auto* pFftSizeMenu = mMenu.AddItem(
"FFT Size",
new IPopupMenu(
"FFT Size", {
"64",
"128",
"256",
"512",
"1024",
"2048",
"4096"}))->GetSubmenu();
93 auto* pChansMenu = mMenu.AddItem(
"Channels",
new IPopupMenu(
"Channels", {
"L",
"R",
"L + R"}))->GetSubmenu();
94 auto* pFreqScaleMenu = mMenu.AddItem(
"Freq Scaling",
new IPopupMenu(
"Freq Scaling", {
"Linear",
"Log"}))->GetSubmenu();
95 auto* pOverlapMenu = mMenu.AddItem(
"Overlap",
new IPopupMenu(
"Overlap", {
"1x",
"2x",
"4x",
"8x" }))->GetSubmenu();
96 auto* pWindowMenu = mMenu.AddItem(
"Window",
new IPopupMenu(
"Window", {
"Hann",
"Blackman Harris",
"Hamming",
"Flattop",
"Rectangular" }))->GetSubmenu();
98 pFftSizeMenu->CheckItem(0, mFFTSize == 64);
99 pFftSizeMenu->CheckItem(1, mFFTSize == 128);
100 pFftSizeMenu->CheckItem(2, mFFTSize == 256);
101 pFftSizeMenu->CheckItem(3, mFFTSize == 512);
102 pFftSizeMenu->CheckItem(4, mFFTSize == 1024);
103 pFftSizeMenu->CheckItem(5, mFFTSize == 2048);
104 pFftSizeMenu->CheckItem(6, mFFTSize == 4096);
106 pChansMenu->CheckItem(0, mChanType == EChannelType::Left);
107 pChansMenu->CheckItem(1, mChanType == EChannelType::Right);
108 pChansMenu->CheckItem(2, mChanType == EChannelType::LeftAndRight);
109 pFreqScaleMenu->CheckItem(0, mFreqScale == EFrequencyScale::Linear);
110 pFreqScaleMenu->CheckItem(1, mFreqScale == EFrequencyScale::Log);
113 pOverlapMenu->CheckItem(0, mOverlap == 1);
114 pOverlapMenu->CheckItem(1, mOverlap == 2);
115 pOverlapMenu->CheckItem(2, mOverlap == 4);
116 pOverlapMenu->CheckItem(3, mOverlap == 8);
119 pWindowMenu->CheckItem(0, mWindowType == 0);
120 pWindowMenu->CheckItem(1, mWindowType == 1);
121 pWindowMenu->CheckItem(2, mWindowType == 2);
122 pWindowMenu->CheckItem(3, mWindowType == 3);
123 pWindowMenu->CheckItem(4, mWindowType == 4);
131 mCursorAmp = CalcYNorm(1.0 - y/mWidgetBounds.
H(), mAmpScale,
true);
132 mCursorFreq = CalcXNorm(x/mWidgetBounds.
W(), mFreqScale,
true) * NyquistFreq();
139 const char* title = pSelectedMenu->GetRootTitle();
141 if (strcmp(title,
"FFT Size") == 0)
143 int fftSize = atoi(pSelectedMenu->GetChosenItem()->GetText());
147 else if (strcmp(title,
"Channels") == 0)
149 const char* chanStr = pSelectedMenu->GetChosenItem()->GetText();
150 if (strcmp(chanStr,
"L") == 0) mChanType = EChannelType::Left;
151 else if (strcmp(chanStr,
"R") == 0) mChanType = EChannelType::Right;
152 else if (strcmp(chanStr,
"L + R") == 0) mChanType = EChannelType::LeftAndRight;
154 else if (strcmp(title,
"Freq Scaling") == 0)
156 auto index = pSelectedMenu->GetChosenItemIdx();
157 SetFrequencyScale(index == 0 ? EFrequencyScale::Linear : EFrequencyScale::Log);
159 else if (strcmp(title,
"Overlap") == 0)
161 const char* txt = pSelectedMenu->GetChosenItem()->GetText();
162 int overlap = atoi(txt);
168 else if (strcmp(title,
"Window") == 0)
170 int idx = pSelectedMenu->GetChosenItemIdx();
192 for (
auto c = d.chanOffset; c < (d.chanOffset + d.nChans); c++)
194 CalculateYPoints(c, d.vals[c]);
197 else if (msgTag == kMsgTagSampleRate)
203 else if (msgTag == kMsgTagFFTSize)
206 stream.
Get(&fftSize, 0);
209 else if (msgTag == kMsgTagOverlap)
212 stream.
Get(&overlap, 0);
215 else if (msgTag == kMsgTagWindowType)
218 stream.
Get(&windowType, 0);
219 mWindowType = windowType;
221 else if (msgTag == kMsgTagOctaveGain)
224 stream.
Get(&octaveGain, 0);
225 SetOctaveGain(octaveGain);
236 if (mStyle.drawFrame)
246 while (freq <= mFreqHi)
248 auto t = CalcXNorm(freq, mFreqScale);
249 auto x0 = t * mWidgetBounds.
W();
250 auto y0 = mWidgetBounds.B;
252 auto y1 = mWidgetBounds.T;
258 else if (freq < 100.0)
260 else if (freq < 1000.0)
262 else if (freq < 10000.0)
269 if (mAmpScale == EAmplitudeScale::Decibel)
272 const auto dBYHi =
AmpToDB(mAmpHi);
274 while (ampDB <= dBYHi)
276 auto t =
Clip(CalcYNorm(ampDB, EAmplitudeScale::Decibel), 0.0f, 1.0f);
278 auto x0 = mWidgetBounds.L;
279 auto y0 = t * mWidgetBounds.
H();
280 auto x1 = mWidgetBounds.R;
294 for (
auto c = 0; c < MAXNC; c++)
296 if ((c == 0) && (mChanType == EChannelType::Right))
298 if ((c == 1) && (mChanType == EChannelType::Left))
301 const int nBins = NumBins();
302 const IColor baseColor = mChannelColors[c];
306 float x0 = mWidgetBounds.L + mXPoints[0] * mWidgetBounds.
W();
307 float y0 = mWidgetBounds.B - mYPoints[c][0] * mWidgetBounds.
H();
310 if (mCurveSmoothing > 0.f && nBins > 3)
313 const float s = mCurveSmoothing;
314 for (
int i = 0; i < nBins - 1; ++i)
316 const int i0 = std::max(i - 1, 0);
318 const int i2 = i + 1;
319 const int i3 = std::min(i + 2, nBins - 1);
321 const float x1 = mWidgetBounds.L + mXPoints[i1] * mWidgetBounds.
W();
322 const float y1 = mWidgetBounds.B - mYPoints[c][i1] * mWidgetBounds.
H();
323 const float x2 = mWidgetBounds.L + mXPoints[i2] * mWidgetBounds.
W();
324 const float y2 = mWidgetBounds.B - mYPoints[c][i2] * mWidgetBounds.
H();
326 const float x0s = mWidgetBounds.L + mXPoints[i0] * mWidgetBounds.
W();
327 const float y0s = mWidgetBounds.B - mYPoints[c][i0] * mWidgetBounds.
H();
328 const float x3 = mWidgetBounds.L + mXPoints[i3] * mWidgetBounds.
W();
329 const float y3 = mWidgetBounds.B - mYPoints[c][i3] * mWidgetBounds.
H();
331 const float c1x = x1 + (x2 - x0s) * (s / 6.f);
332 const float c1y = y1 + (y2 - y0s) * (s / 6.f);
333 const float c2x = x2 - (x3 - x1) * (s / 6.f);
334 const float c2y = y2 - (y3 - y1) * (s / 6.f);
341 for (
int i = 1; i < nBins; ++i)
343 float xi = mWidgetBounds.L + mXPoints[i] * mWidgetBounds.
W();
344 float yi = mWidgetBounds.B - mYPoints[c][i] * mWidgetBounds.
H();
353 float xLast = mWidgetBounds.L + mXPoints[nBins-1] * mWidgetBounds.
W();
366 if (mCurveSmoothing > 0.f && nBins > 3)
368 const float s = mCurveSmoothing;
369 for (
int i = 0; i < nBins - 1; ++i)
371 const int i0 = std::max(i - 1, 0);
373 const int i2 = i + 1;
374 const int i3 = std::min(i + 2, nBins - 1);
376 const float x1 = mWidgetBounds.L + mXPoints[i1] * mWidgetBounds.
W();
377 const float y1 = mWidgetBounds.B - mYPoints[c][i1] * mWidgetBounds.
H();
378 const float x2 = mWidgetBounds.L + mXPoints[i2] * mWidgetBounds.
W();
379 const float y2 = mWidgetBounds.B - mYPoints[c][i2] * mWidgetBounds.
H();
381 const float x0s = mWidgetBounds.L + mXPoints[i0] * mWidgetBounds.
W();
382 const float y0s = mWidgetBounds.B - mYPoints[c][i0] * mWidgetBounds.
H();
383 const float x3 = mWidgetBounds.L + mXPoints[i3] * mWidgetBounds.
W();
384 const float y3 = mWidgetBounds.B - mYPoints[c][i3] * mWidgetBounds.
H();
386 const float c1x = x1 + (x2 - x0s) * (s / 6.f);
387 const float c1y = y1 + (y2 - y0s) * (s / 6.f);
388 const float c2x = x2 - (x3 - x1) * (s / 6.f);
389 const float c2y = y2 - (y3 - y1) * (s / 6.f);
396 for (
int i = 1; i < nBins; ++i)
398 float xi = mWidgetBounds.L + mXPoints[i] * mWidgetBounds.
W();
399 float yi = mWidgetBounds.B - mYPoints[c][i] * mWidgetBounds.
H();
414 if (mCursorFreq >= 0.0)
416 label.SetFormatted(64,
"%.1fHz", mCursorFreq);
420 if (mAmpScale == EAmplitudeScale::Linear)
421 label.SetFormatted(64,
"%.3fs", mCursorAmp);
423 label.SetFormatted(64,
"%ddB", (
int) mCursorAmp);
430 void SetFFTSize(
int fftSize)
433 assert(fftSize <= MAX_FFT_SIZE);
438 SetFreqRange(FirstBinFreq(), NyquistFreq());
439 SetSmoothing(mAttackTimeMs, mDecayTimeMs);
443 void SetSampleRate(
double sampleRate)
445 mSampleRate = sampleRate;
446 SetFreqRange(FirstBinFreq(), NyquistFreq());
447 SetSmoothing(mAttackTimeMs, mDecayTimeMs);
451 void SetFreqRange(
float freqLo,
float freqHi)
458 void SetFrequencyScale(EFrequencyScale scale)
465 void SetAmpRange(
float ampLo,
float ampHi)
472 void SetOctaveGain(
float octaveGain)
474 mOctaveGain = octaveGain;
478 void SetCurveSmoothing(
float amount)
480 mCurveSmoothing =
Clip(amount, 0.f, 1.f);
484 void SetSmoothing(
float attackTimeMs,
float releaseTimeMs)
486 auto attackTimeSec = attackTimeMs * 0.001f;
487 auto releaseTimeSec = releaseTimeMs * 0.001f;
488 auto updatePeriod = (float) mFFTSize / (
float) mSampleRate;
489 mAttackCoeff = exp(-updatePeriod / attackTimeSec);
490 mReleaseCoeff = exp(-updatePeriod / releaseTimeSec);
494 float ApplyOctaveGain(
float amp,
float freqNorm)
497 const float centerFreq = 500.0f;
498 float centerFreqNorm = (centerFreq - mFreqLo)/(mFreqHi - mFreqLo);
500 if (mOctaveGain > 0.0)
502 amp *= freqNorm/centerFreqNorm;
510 mXPoints.resize(NumPoints());
512 for (
auto c = 0; c < MAXNC; c++)
514 mYPoints[c].assign(NumPoints(), 0.0f);
515 mEnvelopeValues[c].assign(NumPoints(), 0.0f);
519 void CalculateXPoints()
521 const auto numBins = NumBins();
522 const auto xIncr = (1.0f /
static_cast<float>(numBins-1)) * NyquistFreq();
524 for (
auto i = 1; i < numBins; i++)
526 auto xVal = CalcXNorm(
float(i) * xIncr, mFreqScale);
529 mXPoints[numBins] = mXPoints[numBins-1];
530 mXPoints[numBins+1] = mXPoints[0];
533 void CalculateYPoints(
int ch,
const TDataPacket& powerSpectrum)
535 const auto numBins = NumBins();
537 for (
auto i = 0; i < numBins; i++)
539 const auto adjustedAmp = ApplyOctaveGain(powerSpectrum[i],
static_cast<float>(numBins-1));
540 float rawVal = (mAmpScale == EAmplitudeScale::Decibel)
541 ?
AmpToDB(adjustedAmp + 1e-30f)
543 rawVal =
Clip(CalcYNorm(rawVal, mAmpScale), 0.0f, 1.0f);
545 float prevVal = mEnvelopeValues[ch][i];
547 if (rawVal > prevVal)
548 newVal = mAttackCoeff * prevVal + (1.0f - mAttackCoeff) * rawVal;
550 newVal = mReleaseCoeff * prevVal + (1.0f - mReleaseCoeff) * rawVal;
552 mEnvelopeValues[ch][i] = newVal;
553 mYPoints[ch][i] = newVal;
559 auto offset = mCurveThickness/mWidgetBounds.
H();
561 mYPoints[ch][numBins] = -offset;
562 mYPoints[ch][numBins+1] = -offset;
568 float CalcXNorm(
float x, EFrequencyScale scale,
bool inverted =
false)
570 const auto nyquist = NyquistFreq();
574 case EFrequencyScale::Linear:
577 return (x - mFreqLo) / (mFreqHi - mFreqLo);
579 return (mFreqLo + x * (mFreqHi - mFreqLo)) / nyquist;
581 case EFrequencyScale::Log:
583 const auto logXLo = std::log(mFreqLo / nyquist);
584 const auto logXHi = std::log(mFreqHi / nyquist);
587 return (std::log(x / nyquist) - logXLo) / (logXHi - logXLo);
589 return std::exp(logXLo + x * (logXHi - logXLo));
595 float CalcYNorm(
float y, EAmplitudeScale scale,
bool inverted =
false)
const
599 case EAmplitudeScale::Linear:
602 return (y - mAmpLo) / (mAmpHi - mAmpLo);
604 return mAmpLo + y * (mAmpHi - mAmpLo);
606 case EAmplitudeScale::Decibel:
608 const auto dBYLo =
AmpToDB(mAmpLo);
609 const auto dBYHi =
AmpToDB(mAmpHi);
612 return (y - dBYLo) / (dBYHi - dBYLo);
614 return dBYLo + y * (dBYHi - dBYLo);
619 int NumPoints()
const {
return FillCurves() ? NumBins() + numExtraPoints : NumBins(); }
620 int NumBins()
const {
return mFFTSize / 2; }
621 double FirstBinFreq()
const {
return NyquistFreq()/mFFTSize; }
622 double NyquistFreq()
const {
return mSampleRate * 0.5; }
623 bool FillCurves()
const {
return mFillOpacity > 0.0f; }
626 std::vector<IColor> mChannelColors;
627 EFrequencyScale mFreqScale;
628 EAmplitudeScale mAmpScale;
630 double mSampleRate = 44100.0;
632 float mOctaveGain = 0.0;
633 float mFreqLo = 20.0;
634 float mFreqHi = 22050.0;
637 float mAttackTimeMs = 3.0;
638 float mDecayTimeMs = 50.0;
639 EChannelType mChanType = EChannelType::LeftAndRight;
641 float mCurveThickness = 1.0f;
642 float mGridThickness = 1.0f;
643 float mFillOpacity = 0.5f;
644 float mCursorAmp = 0.0;
645 float mCursorFreq = -1.0;
648 std::vector<float> mXPoints;
649 std::array<std::vector<float>, MAXNC> mYPoints;
650 std::array<std::vector<float>, MAXNC> mEnvelopeValues;
651 float mAttackCoeff = 0.2f;
652 float mReleaseCoeff = 0.99f;
655 float mCurveSmoothing = 0.6f;
658END_IGRAPHICS_NAMESPACE
This file contains the base IControl implementation, along with some base classes for specific types ...
Manages a non-owned block of memory, for receiving arbitrary message byte streams.
int Get(T *pDst, int startPos) const
Get arbitary typed data from the stream.
The lowest level base class of an IGraphics control.
void SetTargetRECT(const IRECT &bounds)
Set the rectangular mouse tracking target area, within the graphics context for this control.
IGEditorDelegate * GetDelegate()
Gets a pointer to the class implementing the IEditorDelegate interface that handles parameter changes...
virtual void SetDirty(bool triggerAction=true, int valIdx=kNoValIdx)
Mark the control as dirty, i.e.
virtual void SendArbitraryMsgFromUI(int msgTag, int ctrlTag=kNoTag, int dataSize=0, const void *pData=nullptr)
SendArbitraryMsgFromUI (Abbreviation: SAMFUI)
The lowest level base class of an IGraphics context.
virtual void DrawRect(const IColor &color, const IRECT &bounds, const IBlend *pBlend=0, float thickness=1.f)
Draw a rectangle to the graphics context.
virtual void PathFill(const IPattern &pattern, const IFillOptions &options=IFillOptions(), const IBlend *pBlend=0)=0
Fill the current current path.
void DrawText(const IText &text, const char *str, const IRECT &bounds, const IBlend *pBlend=0)
Draw some text to the graphics context in a specific rectangle.
void CreatePopupMenu(IControl &control, IPopupMenu &menu, const IRECT &bounds, int valIdx=0)
Shows a pop up/contextual menu in relation to a rectangular region of the graphics context.
virtual void PathClear()=0
Clear the stack of path drawing commands.
virtual void PathClose()=0
Close the path that is being specified.
virtual void PathStroke(const IPattern &pattern, float thickness, const IStrokeOptions &options=IStrokeOptions(), const IBlend *pBlend=0)=0
Stroke the current current path.
virtual void DrawLine(const IColor &color, float x1, float y1, float x2, float y2, const IBlend *pBlend=0, float thickness=1.f)
Draw a line to the graphics context.
virtual void PathMoveTo(float x, float y)=0
Move the current point in the current path.
virtual void PathLineTo(float x, float y)=0
Add a line to the current path from the current point to the specified location.
virtual void PathCubicBezierTo(float c1x, float c1y, float c2x, float c2y, float x2, float y2)=0
Add a cubic bezier to the current path from the current point to the specified location.
ISender is a utility class which can be used to defer data from the realtime audio processing and sen...
Vectorial multi-channel capable spectrum analyzer controlDerived from work by Alex Harker and Matthew...
void OnMsgFromDelegate(int msgTag, int dataSize, const void *pData) override
Implement to receive messages sent to the control, see IEditorDelegate:SendControlMsgFromDelegate()
IVSpectrumAnalyzerControl(const IRECT &bounds, const char *label="", const IVStyle &style=DEFAULT_STYLE, std::initializer_list< IColor > colors={COLOR_RED, COLOR_GREEN}, EFrequencyScale freqScale=EFrequencyScale::Log, EAmplitudeScale ampScale=EAmplitudeScale::Decibel, float curveThickness=2.0, float gridThickness=1.0, float fillOpacity=0.25, float attackTimeMs=3.0, float decayTimeMs=50.0)
Create a IVSpectrumAnalyzerControl.
void OnPopupMenuSelection(IPopupMenu *pSelectedMenu, int valIdx) override
Implement this method to handle popup menu selection after IGraphics::CreatePopupMenu/IControlPromptU...
void OnResize() override
Called when IControl is constructed or resized using SetRect().
void Draw(IGraphics &g) override
Draw the control to the graphics context.
void OnMouseOver(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouseover event on this control.
void OnMouseDown(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse down event on this control.
A base interface to be combined with IControl for vectorial controls "IVControls",...
IRECT MakeRects(const IRECT &parent, bool hasHandle=false)
Calculate the rectangles for the various areas, depending on the style.
virtual void DrawBackground(IGraphics &g, const IRECT &rect)
Draw the IVControl background (usually transparent)
void AttachIControl(IControl *pControl, const char *label)
Call in the constructor of your IVControl to link the IVectorBase and IControl.
virtual void DrawLabel(IGraphics &g)
Draw the IVControl label text.
const IColor & GetColor(EVColor color) const
Get value of a specific EVColor in the IVControl.
BEGIN_IPLUG_NAMESPACE T Clip(T x, T lo, T hi)
Clips the value x between lo and hi.
static double AmpToDB(double amp)
static double DBToAmp(double dB)
Calculates gain from a given dB value.
Used to manage color data, independent of draw class/platform.
IColor WithOpacity(float alpha) const
Returns a new IColor with a different opacity.
Used to represent a point/stop in a gradient.
Used to manage mouse modifiers i.e.
Used to store pattern information for gradients.
static IPattern CreateLinearGradient(float x1, float y1, float x2, float y2, const std::initializer_list< IColorStop > &stops={})
Create a linear gradient IPattern.
Used to manage a rectangular area, independent of draw class/platform.
IRECT GetFromTRHC(float w, float h) const
Get a subrect of this IRECT expanding from the top-right corner.
IRECT FracRectVertical(float frac, bool fromTop=false) const
Returns a new IRECT with a height that is multiplied by frac.
void Constrain(float &x, float &y) const
Ensure the point (x,y) is inside this IRECT.
ISenderData is used to represent a typed data packet, that may contain values for multiple channels.
A struct encapsulating a set of properties used to configure IVControls.