iPlug2 - C++ Audio Plug-in Framework
Loading...
Searching...
No Matches
IVSpectrumAnalyzerControl.h
Go to the documentation of this file.
1/*
2 ==============================================================================
3
4 This file is part of the iPlug 2 library. Copyright (C) the iPlug 2 developers.
5
6 See LICENSE.txt for more info.
7
8 ==============================================================================
9 */
10
11#pragma once
12
19#include "IControl.h"
20#include "ISender.h"
21#include "IPlugStructs.h"
22
23BEGIN_IPLUG_NAMESPACE
24BEGIN_IGRAPHICS_NAMESPACE
25
30template <int MAXNC = 2, int MAX_FFT_SIZE = 4096>
32 , public IVectorBase
33{
34public:
35 enum MsgTags
36 {
37 kMsgTagSampleRate = 1,
38 kMsgTagFFTSize,
39 kMsgTagOverlap,
40 kMsgTagWindowType,
41 kMsgTagOctaveGain
42 };
43
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 };
49
62 IVSpectrumAnalyzerControl(const IRECT& bounds, const char* label = "", const IVStyle& style = DEFAULT_STYLE,
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)
71 : IControl(bounds)
72 , IVectorBase(style)
73 , mChannelColors(colors)
74 , mFreqScale(freqScale)
75 , mAmpScale(ampScale)
76 , mCurveThickness(curveThickness)
77 , mGridThickness(gridThickness)
78 , mFillOpacity(fillOpacity)
79 , mAttackTimeMs(attackTimeMs)
80 , mDecayTimeMs(decayTimeMs)
81 {
82 assert(colors.size() >= MAXNC);
83 AttachIControl(this, label);
84 SetFFTSize(1024);
85 SetFreqRange(FirstBinFreq(), NyquistFreq());
86 SetAmpRange(DBToAmp(-90.0f), 1.0f);
87 }
88
89 void OnMouseDown(float x, float y, const IMouseMod& mod) override
90 {
91 mMenu.Clear(true);
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
96 pFftSizeMenu->CheckItem(0, mFFTSize == 64);
97 pFftSizeMenu->CheckItem(1, mFFTSize == 128);
98 pFftSizeMenu->CheckItem(2, mFFTSize == 256);
99 pFftSizeMenu->CheckItem(3, mFFTSize == 512);
100 pFftSizeMenu->CheckItem(4, mFFTSize == 1024);
101 pFftSizeMenu->CheckItem(5, mFFTSize == 2048);
102 pFftSizeMenu->CheckItem(6, mFFTSize == 4096);
103
104 pChansMenu->CheckItem(0, mChanType == EChannelType::Left);
105 pChansMenu->CheckItem(1, mChanType == EChannelType::Right);
106 pChansMenu->CheckItem(2, mChanType == EChannelType::LeftAndRight);
107 pFreqScaleMenu->CheckItem(0, mFreqScale == EFrequencyScale::Linear);
108 pFreqScaleMenu->CheckItem(1, mFreqScale == EFrequencyScale::Log);
109
110 GetUI()->CreatePopupMenu(*this, mMenu, x, y);
111 }
112
113 void OnMouseOver(float x, float y, const IMouseMod& mod) override
114 {
115 mWidgetBounds.Constrain(x, y);
116 mCursorAmp = CalcYNorm(1.0 - y/mWidgetBounds.H(), mAmpScale, true);
117 mCursorFreq = CalcXNorm(x/mWidgetBounds.W(), mFreqScale, true) * NyquistFreq();
118 }
119
120 void OnPopupMenuSelection(IPopupMenu* pSelectedMenu, int valIdx) override
121 {
122 if (pSelectedMenu)
123 {
124 const char* title = pSelectedMenu->GetRootTitle();
125
126 if (strcmp(title, "FFT Size") == 0)
127 {
128 int fftSize = atoi(pSelectedMenu->GetChosenItem()->GetText());
129 GetDelegate()->SendArbitraryMsgFromUI(kMsgTagFFTSize, kNoTag, sizeof(int), &fftSize);
130 SetFFTSize(fftSize);
131 }
132 else if (strcmp(title, "Channels") == 0)
133 {
134 const char* chanStr = pSelectedMenu->GetChosenItem()->GetText();
135 if (strcmp(chanStr, "L") == 0) mChanType = EChannelType::Left;
136 else if (strcmp(chanStr, "R") == 0) mChanType = EChannelType::Right;
137 else if (strcmp(chanStr, "L + R") == 0) mChanType = EChannelType::LeftAndRight;
138 }
139 else if (strcmp(title, "Freq Scaling") == 0)
140 {
141 auto index = pSelectedMenu->GetChosenItemIdx();
142 SetFrequencyScale(index == 0 ? EFrequencyScale::Linear : EFrequencyScale::Log);
143 }
144 }
145 }
146
147 void OnResize() override
148 {
149 SetTargetRECT(MakeRects(mRECT));
150 SetDirty(false);
151 }
152
153 void OnMsgFromDelegate(int msgTag, int dataSize, const void* pData) override
154 {
155 IByteStream stream(pData, dataSize);
156
157 if (!IsDisabled() && msgTag == ISender<>::kUpdateMessage)
158 {
160 stream.Get(&d, 0);
161
162 for (auto c = d.chanOffset; c < (d.chanOffset + d.nChans); c++)
163 {
164 CalculateYPoints(c, d.vals[c]);
165 }
166 }
167 else if (msgTag == kMsgTagSampleRate)
168 {
169 double sr;
170 stream.Get(&sr, 0);
171 SetSampleRate(sr);
172 }
173 else if (msgTag == kMsgTagFFTSize)
174 {
175 int fftSize;
176 stream.Get(&fftSize, 0);
177 SetFFTSize(fftSize);
178 }
179 else if (msgTag == kMsgTagOctaveGain)
180 {
181 double octaveGain;
182 stream.Get(&octaveGain, 0);
183 SetOctaveGain(octaveGain);
184 }
185 }
186
187 void Draw(IGraphics& g) override
188 {
189 DrawBackground(g, mRECT);
190 DrawWidget(g);
191 DrawLabel(g);
192 DrawCursorValues(g);
193
194 if (mStyle.drawFrame)
195 g.DrawRect(GetColor(kFR), mWidgetBounds, &mBlend, mStyle.frameThickness);
196 }
197
198private:
199 void DrawGrids(IGraphics& g)
200 {
201 // Frequency Grid
202 auto freq = mFreqLo;
203
204 while (freq <= mFreqHi)
205 {
206 auto t = CalcXNorm(freq, mFreqScale);
207 auto x0 = t * mWidgetBounds.W();
208 auto y0 = mWidgetBounds.B;
209 auto x1 = x0;
210 auto y1 = mWidgetBounds.T;
211
212 g.DrawLine(GetColor(kFG), x0, y0, x1, y1, 0, mGridThickness);
213
214 if (freq < 10.0)
215 freq += 1.0;
216 else if (freq < 100.0)
217 freq += 10.0;
218 else if (freq < 1000.0)
219 freq += 100.0;
220 else if (freq < 10000.0)
221 freq += 1000.0;
222 else
223 freq += 10000.0;
224 }
225
226 // Amplitude Grid
227 if (mAmpScale == EAmplitudeScale::Decibel)
228 {
229 auto ampDB = AmpToDB(mAmpLo);
230 const auto dBYHi = AmpToDB(mAmpHi);
231
232 while (ampDB <= dBYHi)
233 {
234 auto t = Clip(CalcYNorm(ampDB, EAmplitudeScale::Decibel), 0.0f, 1.0f);
235
236 auto x0 = mWidgetBounds.L;
237 auto y0 = t * mWidgetBounds.H();
238 auto x1 = mWidgetBounds.R;
239 auto y1 = y0;
240
241 g.DrawLine(GetColor(kFG), x0, y0, x1, y1, 0, mGridThickness);
242
243 ampDB += 10.0;
244 }
245 }
246 }
247
248 void DrawWidget(IGraphics& g) override
249 {
250 DrawGrids(g);
251
252 for (auto c = 0; c < MAXNC; c++)
253 {
254 if ((c == 0) && (mChanType == EChannelType::Right))
255 continue;
256 if ((c == 1) && (mChanType == EChannelType::Left))
257 continue;
258
259 int nPoints = NumPoints();
260 IColor fillColor = mChannelColors[c].WithOpacity(mFillOpacity);
261 g.DrawData(mChannelColors[c], mWidgetBounds, mYPoints[c].data(), nPoints, mXPoints.data(), 0, mCurveThickness, &fillColor);
262 }
263 }
264
265 void DrawCursorValues(IGraphics& g)
266 {
267 WDL_String label;
268
269 if (mCursorFreq >= 0.0)
270 {
271 label.SetFormatted(64, "%.1fHz", mCursorFreq);
272 g.DrawText(mStyle.valueText, label.Get(), mWidgetBounds.GetFromTRHC(100, 50).FracRectVertical(0.5));
273 }
274
275 if (mAmpScale == EAmplitudeScale::Linear)
276 label.SetFormatted(64, "%.3fs", mCursorAmp);
277 else
278 label.SetFormatted(64, "%ddB", (int) mCursorAmp);
279
280 g.DrawText(mStyle.valueText, label.Get(), mWidgetBounds.GetFromTRHC(100, 50).FracRectVertical(0.5, true));
281 }
282
283#pragma mark -
284
285 void SetFFTSize(int fftSize)
286 {
287 assert(fftSize > 0);
288 assert(fftSize <= MAX_FFT_SIZE);
289 mFFTSize = fftSize;
290
291 ResizePoints();
292 CalculateXPoints();
293 SetFreqRange(FirstBinFreq(), NyquistFreq());
294 SetSmoothing(mAttackTimeMs, mDecayTimeMs);
295 SetDirty(false);
296 }
297
298 void SetSampleRate(double sampleRate)
299 {
300 mSampleRate = sampleRate;
301 SetFreqRange(FirstBinFreq(), NyquistFreq());
302 SetSmoothing(mAttackTimeMs, mDecayTimeMs);
303 SetDirty(false);
304 }
305
306 void SetFreqRange(float freqLo, float freqHi)
307 {
308 mFreqLo = freqLo;
309 mFreqHi = freqHi;
310 SetDirty(false);
311 }
312
313 void SetFrequencyScale(EFrequencyScale scale)
314 {
315 mFreqScale = scale;
316 CalculateXPoints();
317 SetDirty(false);
318 }
319
320 void SetAmpRange(float ampLo, float ampHi)
321 {
322 mAmpLo = ampLo;
323 mAmpHi = ampHi;
324 SetDirty(false);
325 }
326
327 void SetOctaveGain(float octaveGain)
328 {
329 mOctaveGain = octaveGain;
330 SetDirty(false);
331 }
332
333 void SetSmoothing(float attackTimeMs, float releaseTimeMs)
334 {
335 auto attackTimeSec = attackTimeMs * 0.001f;
336 auto releaseTimeSec = releaseTimeMs * 0.001f;
337 auto updatePeriod = (float) mFFTSize / (float) mSampleRate;
338 mAttackCoeff = exp(-updatePeriod / attackTimeSec);
339 mReleaseCoeff = exp(-updatePeriod / releaseTimeSec);
340 }
341
342protected:
343 float ApplyOctaveGain(float amp, float freqNorm)
344 {
345 // Center on 500Hz
346 const float centerFreq = 500.0f;
347 float centerFreqNorm = (centerFreq - mFreqLo)/(mFreqHi - mFreqLo);
348
349 if (mOctaveGain > 0.0)
350 {
351 amp *= freqNorm/centerFreqNorm;
352 }
353
354 return amp;
355 }
356
357 void ResizePoints()
358 {
359 mXPoints.resize(NumPoints());
360
361 for (auto c = 0; c < MAXNC; c++)
362 {
363 mYPoints[c].assign(NumPoints(), 0.0f);
364 mEnvelopeValues[c].assign(NumPoints(), 0.0f);
365 }
366 }
367
368 void CalculateXPoints()
369 {
370 const auto numBins = NumBins();
371 const auto xIncr = (1.0f / static_cast<float>(numBins-1)) * NyquistFreq();
372 mXPoints[0] = 0.0f;
373 for (auto i = 1; i < numBins; i++)
374 {
375 auto xVal = CalcXNorm(float(i) * xIncr, mFreqScale);
376 mXPoints[i] = xVal;
377 }
378 mXPoints[numBins] = mXPoints[numBins-1];
379 mXPoints[numBins+1] = mXPoints[0];
380 }
381
382 void CalculateYPoints(int ch, const TDataPacket& powerSpectrum)
383 {
384 const auto numBins = NumBins();
385
386 for (auto i = 0; i < numBins; i++)
387 {
388 const auto adjustedAmp = ApplyOctaveGain(powerSpectrum[i], static_cast<float>(numBins-1));
389 float rawVal = (mAmpScale == EAmplitudeScale::Decibel)
390 ? AmpToDB(adjustedAmp + 1e-30f)
391 : adjustedAmp;
392 rawVal = Clip(CalcYNorm(rawVal, mAmpScale), 0.0f, 1.0f);
393
394 float prevVal = mEnvelopeValues[ch][i];
395 float newVal;
396 if (rawVal > prevVal)
397 newVal = mAttackCoeff * prevVal + (1.0f - mAttackCoeff) * rawVal; // Attack phase
398 else
399 newVal = mReleaseCoeff * prevVal + (1.0f - mReleaseCoeff) * rawVal; // Release phase
400
401 mEnvelopeValues[ch][i] = newVal; // Store smoothed value
402 mYPoints[ch][i] = newVal; // Use smoothed value for drawing
403 }
404
405 if (FillCurves())
406 {
407 // Used to close the path outside the bounds of the control
408 auto offset = mCurveThickness/mWidgetBounds.H();
409
410 mYPoints[ch][numBins] = -offset;
411 mYPoints[ch][numBins+1] = -offset;
412 }
413
414 SetDirty(false);
415 }
416
417 float CalcXNorm(float x, EFrequencyScale scale, bool inverted = false)
418 {
419 const auto nyquist = NyquistFreq();
420
421 switch (scale)
422 {
423 case EFrequencyScale::Linear:
424 {
425 if (!inverted)
426 return (x - mFreqLo) / (mFreqHi - mFreqLo);
427 else
428 return (mFreqLo + x * (mFreqHi - mFreqLo)) / nyquist;
429 }
430 case EFrequencyScale::Log:
431 {
432 const auto logXLo = std::log(mFreqLo / nyquist);
433 const auto logXHi = std::log(mFreqHi / nyquist);
434
435 if (!inverted)
436 return (std::log(x / nyquist) - logXLo) / (logXHi - logXLo);
437 else
438 return std::exp(logXLo + x * (logXHi - logXLo));
439 }
440 }
441 }
442
443 // Amplitudes
444 float CalcYNorm(float y, EAmplitudeScale scale, bool inverted = false) const
445 {
446 switch (scale)
447 {
448 case EAmplitudeScale::Linear:
449 {
450 if (!inverted)
451 return (y - mAmpLo) / (mAmpHi - mAmpLo);
452 else
453 return mAmpLo + y * (mAmpHi - mAmpLo);
454 }
455 case EAmplitudeScale::Decibel:
456 {
457 const auto dBYLo = AmpToDB(mAmpLo);
458 const auto dBYHi = AmpToDB(mAmpHi);
459
460 if (!inverted)
461 return (y - dBYLo) / (dBYHi - dBYLo);
462 else
463 return dBYLo + y * (dBYHi - dBYLo);
464 }
465 }
466 }
467
468 int NumPoints() const { return FillCurves() ? NumBins() + numExtraPoints : NumBins(); }
469 int NumBins() const { return mFFTSize / 2; }
470 double FirstBinFreq() const { return NyquistFreq()/mFFTSize; }
471 double NyquistFreq() const { return mSampleRate * 0.5; }
472 bool FillCurves() const { return mFillOpacity > 0.0f; }
473
474private:
475 std::vector<IColor> mChannelColors;
476 EFrequencyScale mFreqScale;
477 EAmplitudeScale mAmpScale;
478
479 double mSampleRate = 44100.0;
480 int mFFTSize = 1024;
481 float mOctaveGain = 0.0;
482 float mFreqLo = 20.0;
483 float mFreqHi = 22050.0;
484 float mAmpLo = 0.0;
485 float mAmpHi = 1.0;
486 float mAttackTimeMs = 3.0;
487 float mDecayTimeMs = 50.0;
488 EChannelType mChanType = EChannelType::LeftAndRight;
489
490 float mCurveThickness = 1.0f;
491 float mGridThickness = 1.0f;
492 float mFillOpacity = 0.5f;
493 float mCursorAmp = 0.0;
494 float mCursorFreq = -1.0;
495 IPopupMenu mMenu {"Options"};
496
497 std::vector<float> mXPoints;
498 std::array<std::vector<float>, MAXNC> mYPoints;
499 std::array<std::vector<float>, MAXNC> mEnvelopeValues;
500 float mAttackCoeff = 0.2f;
501 float mReleaseCoeff = 0.99f;
502};
503
504END_IGRAPHICS_NAMESPACE
505END_IPLUG_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.
Definition: IPlugStructs.h:268
int Get(T *pDst, int startPos) const
Get arbitary typed data from the stream.
Definition: IPlugStructs.h:289
The lowest level base class of an IGraphics control.
Definition: IControl.h:49
IGraphics * GetUI()
Definition: IControl.h:472
bool IsDisabled() const
Definition: IControl.h:367
void SetTargetRECT(const IRECT &bounds)
Set the rectangular mouse tracking target area, within the graphics context for this control.
Definition: IControl.h:328
IGEditorDelegate * GetDelegate()
Gets a pointer to the class implementing the IEditorDelegate interface that handles parameter changes...
Definition: IControl.h:454
virtual void SetDirty(bool triggerAction=true, int valIdx=kNoValIdx)
Mark the control as dirty, i.e.
Definition: IControl.cpp:198
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.
Definition: IGraphics.h:86
virtual void DrawRect(const IColor &color, const IRECT &bounds, const IBlend *pBlend=0, float thickness=1.f)
Draw a rectangle to the graphics context.
Definition: IGraphics.cpp:2508
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.
Definition: IGraphics.cpp:678
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.
Definition: IGraphics.cpp:1971
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.
Definition: IGraphics.cpp:2427
virtual void DrawData(const IColor &color, const IRECT &bounds, float *normYPoints, int nPoints, float *normXPoints=nullptr, const IBlend *pBlend=0, float thickness=1.f, const IColor *pFillColor=nullptr)
Draw a line between a collection of normalized points.
Definition: IGraphics.cpp:2461
A class for setting the contents of a pop up menu.
ISender is a utility class which can be used to defer data from the realtime audio processing and sen...
Definition: ISender.h:66
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",...
Definition: IControl.h:762
IRECT MakeRects(const IRECT &parent, bool hasHandle=false)
Calculate the rectangles for the various areas, depending on the style.
Definition: IControl.h:1163
virtual void DrawBackground(IGraphics &g, const IRECT &rect)
Draw the IVControl background (usually transparent)
Definition: IControl.h:881
void AttachIControl(IControl *pControl, const char *label)
Call in the constructor of your IVControl to link the IVectorBase and IControl.
Definition: IControl.h:780
virtual void DrawLabel(IGraphics &g)
Draw the IVControl label text.
Definition: IControl.h:894
const IColor & GetColor(EVColor color) const
Get value of a specific EVColor in the IVControl.
Definition: IControl.h:806
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.
Used to manage mouse modifiers i.e.
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.
float W() const
void Constrain(float &x, float &y) const
Ensure the point (x,y) is inside this IRECT.
float H() const
ISenderData is used to represent a typed data packet, that may contain values for multiple channels.
Definition: ISender.h:35
A struct encapsulating a set of properties used to configure IVControls.