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 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();
97
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);
105
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);
111
112 // Overlap checks
113 pOverlapMenu->CheckItem(0, mOverlap == 1);
114 pOverlapMenu->CheckItem(1, mOverlap == 2);
115 pOverlapMenu->CheckItem(2, mOverlap == 4);
116 pOverlapMenu->CheckItem(3, mOverlap == 8);
117
118 // Window checks (indices match enum order)
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);
124
125 GetUI()->CreatePopupMenu(*this, mMenu, x, y);
126 }
127
128 void OnMouseOver(float x, float y, const IMouseMod& mod) override
129 {
130 if (mPlotBounds.W() <= 0.f || mPlotBounds.H() <= 0.f)
131 return;
132
133 mPlotBounds.Constrain(x, y);
134
135 const float normalizedX = (x - mPlotBounds.L) / mPlotBounds.W();
136 const float normalizedY = 1.f - ((y - mPlotBounds.T) / mPlotBounds.H());
137
138 mCursorAmp = CalcYNorm(normalizedY, mAmpScale, true);
139 mCursorFreq = CalcXNorm(normalizedX, mFreqScale, true) * NyquistFreq();
140 }
141
142 void OnPopupMenuSelection(IPopupMenu* pSelectedMenu, int valIdx) override
143 {
144 if (pSelectedMenu)
145 {
146 const char* title = pSelectedMenu->GetRootTitle();
147
148 if (strcmp(title, "FFT Size") == 0)
149 {
150 int fftSize = atoi(pSelectedMenu->GetChosenItem()->GetText());
151 GetDelegate()->SendArbitraryMsgFromUI(kMsgTagFFTSize, kNoTag, sizeof(int), &fftSize);
152 SetFFTSize(fftSize);
153 }
154 else if (strcmp(title, "Channels") == 0)
155 {
156 const char* chanStr = pSelectedMenu->GetChosenItem()->GetText();
157 if (strcmp(chanStr, "L") == 0) mChanType = EChannelType::Left;
158 else if (strcmp(chanStr, "R") == 0) mChanType = EChannelType::Right;
159 else if (strcmp(chanStr, "L + R") == 0) mChanType = EChannelType::LeftAndRight;
160 }
161 else if (strcmp(title, "Freq Scaling") == 0)
162 {
163 auto index = pSelectedMenu->GetChosenItemIdx();
164 SetFrequencyScale(index == 0 ? EFrequencyScale::Linear : EFrequencyScale::Log);
165 }
166 else if (strcmp(title, "Overlap") == 0)
167 {
168 const char* txt = pSelectedMenu->GetChosenItem()->GetText();
169 int overlap = atoi(txt); // works for strings like "1x", "2x"
170 if (overlap <= 0)
171 overlap = 1;
172 GetDelegate()->SendArbitraryMsgFromUI(kMsgTagOverlap, kNoTag, sizeof(int), &overlap);
173 mOverlap = overlap;
174 }
175 else if (strcmp(title, "Window") == 0)
176 {
177 int idx = pSelectedMenu->GetChosenItemIdx();
178 GetDelegate()->SendArbitraryMsgFromUI(kMsgTagWindowType, kNoTag, sizeof(int), &idx);
179 mWindowType = idx;
180 }
181 }
182 }
183
184 void OnResize() override
185 {
186 SetTargetRECT(MakeRects(mRECT));
187 UpdatePlotBounds();
188 SetDirty(false);
189 }
190
191 void OnMsgFromDelegate(int msgTag, int dataSize, const void* pData) override
192 {
193 IByteStream stream(pData, dataSize);
194
195 if (!IsDisabled() && msgTag == ISender<>::kUpdateMessage)
196 {
198 stream.Get(&d, 0);
199
200 for (auto c = d.chanOffset; c < (d.chanOffset + d.nChans); c++)
201 {
202 CalculateYPoints(c, d.vals[c]);
203 }
204 }
205 else if (msgTag == kMsgTagSampleRate)
206 {
207 double sr;
208 stream.Get(&sr, 0);
209 SetSampleRate(sr);
210 }
211 else if (msgTag == kMsgTagFFTSize)
212 {
213 int fftSize;
214 stream.Get(&fftSize, 0);
215 SetFFTSize(fftSize);
216 }
217 else if (msgTag == kMsgTagOverlap)
218 {
219 int overlap;
220 stream.Get(&overlap, 0);
221 mOverlap = overlap;
222 }
223 else if (msgTag == kMsgTagWindowType)
224 {
225 int windowType;
226 stream.Get(&windowType, 0);
227 mWindowType = windowType;
228 }
229 else if (msgTag == kMsgTagOctaveGain)
230 {
231 double octaveGain;
232 stream.Get(&octaveGain, 0);
233 SetOctaveGain(octaveGain);
234 }
235 }
236
237 void Draw(IGraphics& g) override
238 {
239 DrawBackground(g, mRECT);
240 DrawWidget(g);
241 DrawAxisLabels(g);
242 DrawLabel(g);
243 DrawCursorValues(g);
244
245 if (mStyle.drawFrame)
246 g.DrawRect(GetColor(kFR), mWidgetBounds, &mBlend, mStyle.frameThickness);
247 }
248
249private:
250 IText GetAxisLabelText() const
251 {
252 return mStyle.valueText.WithFGColor(GetColor(kFG))
253 .WithSize(std::max(11.f, mStyle.valueText.mSize * 0.95f));
254 }
255
256 void UpdatePlotBounds()
257 {
258 mPlotBounds = mWidgetBounds;
259
260 const float topPadding = 8.f;
261 const float rightPadding = 10.f;
262 float leftPadding = 48.f;
263 float bottomPadding = 22.f;
264
265 if (auto* pUI = GetUI())
266 {
267 const IText axisText = GetAxisLabelText();
268 IRECT textBounds;
269 WDL_String ampLabel;
270
271 ampLabel.SetFormatted(64, "%ddB", static_cast<int>(std::floor(AmpToDB(mAmpLo))));
272 pUI->MeasureText(axisText, ampLabel.Get(), textBounds);
273 leftPadding = textBounds.W() + 14.f;
274
275 pUI->MeasureText(axisText, "20kHz", textBounds);
276 bottomPadding = textBounds.H() + 12.f;
277 }
278
279 mPlotBounds.L += leftPadding;
280 mPlotBounds.T += topPadding;
281 mPlotBounds.R -= rightPadding;
282 mPlotBounds.B -= bottomPadding;
283
284 if (mPlotBounds.W() <= 0.f || mPlotBounds.H() <= 0.f)
285 mPlotBounds = mWidgetBounds;
286 }
287
288 void DrawAxisLabels(IGraphics& g)
289 {
290 if (mPlotBounds.W() <= 0.f || mPlotBounds.H() <= 0.f)
291 return;
292
293 const IText axisText = GetAxisLabelText();
294 const IText freqText = axisText.WithAlign(EAlign::Center).WithVAlign(EVAlign::Top);
295 const IText ampText = axisText.WithAlign(EAlign::Far).WithVAlign(EVAlign::Middle);
296 IRECT textBounds;
297
298 g.MeasureText(axisText, "20kHz", textBounds);
299 const float freqLabelHalfWidth = textBounds.W() * 0.5f;
300 const float freqLabelHeight = textBounds.H();
301
302 float previousRight = mPlotBounds.L - 6.f;
303 constexpr float kFrequencyTicks[] = {50.f, 100.f, 200.f, 500.f, 1000.f, 2000.f, 5000.f, 10000.f};
304
305 for (const float freq : kFrequencyTicks)
306 {
307 if (freq < mFreqLo || freq > mFreqHi)
308 continue;
309
310 WDL_String label;
311 if (freq >= 1000.f)
312 label.SetFormatted(32, "%.0fkHz", freq / 1000.f);
313 else
314 label.SetFormatted(32, "%.0fHz", freq);
315
316 g.MeasureText(freqText, label.Get(), textBounds);
317
318 const float x = mPlotBounds.L + CalcXNorm(freq, mFreqScale) * mPlotBounds.W();
319 IRECT labelRect(x - std::max(freqLabelHalfWidth, textBounds.W() * 0.5f),
320 mPlotBounds.B + 4.f,
321 x + std::max(freqLabelHalfWidth, textBounds.W() * 0.5f),
322 mPlotBounds.B + 4.f + freqLabelHeight);
323
324 if (labelRect.L <= previousRight + 4.f)
325 continue;
326
327 labelRect.L = std::max(labelRect.L, mPlotBounds.L);
328 labelRect.R = std::min(labelRect.R, mWidgetBounds.R);
329 g.DrawText(freqText, label.Get(), labelRect, &mBlend);
330 previousRight = labelRect.R;
331 }
332
333 const float dBLo = AmpToDB(mAmpLo);
334 const float dBHi = AmpToDB(mAmpHi);
335 float previousBottom = mWidgetBounds.T - 1.f;
336
337 for (float ampDB = dBHi; ampDB >= dBLo; ampDB -= 10.f)
338 {
339 WDL_String label;
340 label.SetFormatted(32, "%ddB", static_cast<int>(ampDB));
341 g.MeasureText(ampText, label.Get(), textBounds);
342
343 const float t = Clip(CalcYNorm(ampDB, EAmplitudeScale::Decibel), 0.0f, 1.0f);
344 const float y = mPlotBounds.B - t * mPlotBounds.H();
345 IRECT labelRect(mWidgetBounds.L + 2.f,
346 y - textBounds.H() * 0.5f,
347 mPlotBounds.L - 6.f,
348 y + textBounds.H() * 0.5f);
349
350 if (labelRect.T <= previousBottom + 2.f)
351 continue;
352
353 g.DrawText(ampText, label.Get(), labelRect, &mBlend);
354 previousBottom = labelRect.B;
355 }
356 }
357
358 void DrawGrids(IGraphics& g)
359 {
360 // Frequency Grid
361 auto freq = mFreqLo;
362
363 while (freq <= mFreqHi)
364 {
365 auto t = CalcXNorm(freq, mFreqScale);
366 auto x0 = mPlotBounds.L + t * mPlotBounds.W();
367 auto y0 = mPlotBounds.B;
368 auto x1 = x0;
369 auto y1 = mPlotBounds.T;
370
371 g.DrawLine(GetColor(kFG), x0, y0, x1, y1, 0, mGridThickness);
372
373 if (freq < 10.0)
374 freq += 1.0;
375 else if (freq < 100.0)
376 freq += 10.0;
377 else if (freq < 1000.0)
378 freq += 100.0;
379 else if (freq < 10000.0)
380 freq += 1000.0;
381 else
382 freq += 10000.0;
383 }
384
385 // Amplitude Grid
386 if (mAmpScale == EAmplitudeScale::Decibel)
387 {
388 auto ampDB = AmpToDB(mAmpLo);
389 const auto dBYHi = AmpToDB(mAmpHi);
390
391 while (ampDB <= dBYHi)
392 {
393 auto t = Clip(CalcYNorm(ampDB, EAmplitudeScale::Decibel), 0.0f, 1.0f);
394
395 auto x0 = mPlotBounds.L;
396 auto y0 = mPlotBounds.B - t * mPlotBounds.H();
397 auto x1 = mPlotBounds.R;
398 auto y1 = y0;
399
400 g.DrawLine(GetColor(kFG), x0, y0, x1, y1, 0, mGridThickness);
401
402 ampDB += 10.0;
403 }
404 }
405 }
406
407 void DrawWidget(IGraphics& g) override
408 {
409 DrawGrids(g);
410
411 for (auto c = 0; c < MAXNC; c++)
412 {
413 if ((c == 0) && (mChanType == EChannelType::Right))
414 continue;
415 if ((c == 1) && (mChanType == EChannelType::Left))
416 continue;
417
418 const int nBins = NumBins();
419 const IColor baseColor = mChannelColors[c];
420
421 // Build the spectrum path (optionally smoothed using cubic Beziers)
422 g.PathClear();
423 float x0 = mPlotBounds.L + mXPoints[0] * mPlotBounds.W();
424 float y0 = mPlotBounds.B - mYPoints[c][0] * mPlotBounds.H();
425 g.PathMoveTo(x0, y0);
426
427 if (mCurveSmoothing > 0.f && nBins > 3)
428 {
429 // Catmull-Rom to Bezier conversion with adjustable tension (mCurveSmoothing in [0,1])
430 const float s = mCurveSmoothing; // 0 -> straight lines, 1 -> classic Catmull-Rom
431 for (int i = 0; i < nBins - 1; ++i)
432 {
433 const int i0 = std::max(i - 1, 0);
434 const int i1 = i;
435 const int i2 = i + 1;
436 const int i3 = std::min(i + 2, nBins - 1);
437
438 const float x1 = mPlotBounds.L + mXPoints[i1] * mPlotBounds.W();
439 const float y1 = mPlotBounds.B - mYPoints[c][i1] * mPlotBounds.H();
440 const float x2 = mPlotBounds.L + mXPoints[i2] * mPlotBounds.W();
441 const float y2 = mPlotBounds.B - mYPoints[c][i2] * mPlotBounds.H();
442
443 const float x0s = mPlotBounds.L + mXPoints[i0] * mPlotBounds.W();
444 const float y0s = mPlotBounds.B - mYPoints[c][i0] * mPlotBounds.H();
445 const float x3 = mPlotBounds.L + mXPoints[i3] * mPlotBounds.W();
446 const float y3 = mPlotBounds.B - mYPoints[c][i3] * mPlotBounds.H();
447
448 const float c1x = x1 + (x2 - x0s) * (s / 6.f);
449 const float c1y = y1 + (y2 - y0s) * (s / 6.f);
450 const float c2x = x2 - (x3 - x1) * (s / 6.f);
451 const float c2y = y2 - (y3 - y1) * (s / 6.f);
452
453 g.PathCubicBezierTo(c1x, c1y, c2x, c2y, x2, y2);
454 }
455 }
456 else
457 {
458 for (int i = 1; i < nBins; ++i)
459 {
460 float xi = mPlotBounds.L + mXPoints[i] * mPlotBounds.W();
461 float yi = mPlotBounds.B - mYPoints[c][i] * mPlotBounds.H();
462 g.PathLineTo(xi, yi);
463 }
464 }
465
466 // Fill under the curve with a vertical gradient to transparent
467 if (FillCurves())
468 {
469 // Close the path down to the baseline and back to the start
470 float xLast = mPlotBounds.L + mXPoints[nBins-1] * mPlotBounds.W();
471 g.PathLineTo(xLast, mPlotBounds.B);
472 g.PathLineTo(x0, mPlotBounds.B);
473 g.PathClose();
474
475 const IColor topCol = baseColor.WithOpacity(mFillOpacity);
476 const IColor botCol = baseColor.WithOpacity(0.f);
477 IPattern fill = IPattern::CreateLinearGradient(mPlotBounds, EDirection::Vertical, { IColorStop(topCol, 0.f), IColorStop(botCol, 1.f) });
478 g.PathFill(fill);
479
480 // Recreate the path for stroking (PathFill may consume it on some backends)
481 g.PathClear();
482 g.PathMoveTo(x0, y0);
483 if (mCurveSmoothing > 0.f && nBins > 3)
484 {
485 const float s = mCurveSmoothing;
486 for (int i = 0; i < nBins - 1; ++i)
487 {
488 const int i0 = std::max(i - 1, 0);
489 const int i1 = i;
490 const int i2 = i + 1;
491 const int i3 = std::min(i + 2, nBins - 1);
492
493 const float x1 = mPlotBounds.L + mXPoints[i1] * mPlotBounds.W();
494 const float y1 = mPlotBounds.B - mYPoints[c][i1] * mPlotBounds.H();
495 const float x2 = mPlotBounds.L + mXPoints[i2] * mPlotBounds.W();
496 const float y2 = mPlotBounds.B - mYPoints[c][i2] * mPlotBounds.H();
497
498 const float x0s = mPlotBounds.L + mXPoints[i0] * mPlotBounds.W();
499 const float y0s = mPlotBounds.B - mYPoints[c][i0] * mPlotBounds.H();
500 const float x3 = mPlotBounds.L + mXPoints[i3] * mPlotBounds.W();
501 const float y3 = mPlotBounds.B - mYPoints[c][i3] * mPlotBounds.H();
502
503 const float c1x = x1 + (x2 - x0s) * (s / 6.f);
504 const float c1y = y1 + (y2 - y0s) * (s / 6.f);
505 const float c2x = x2 - (x3 - x1) * (s / 6.f);
506 const float c2y = y2 - (y3 - y1) * (s / 6.f);
507
508 g.PathCubicBezierTo(c1x, c1y, c2x, c2y, x2, y2);
509 }
510 }
511 else
512 {
513 for (int i = 1; i < nBins; ++i)
514 {
515 float xi = mPlotBounds.L + mXPoints[i] * mPlotBounds.W();
516 float yi = mPlotBounds.B - mYPoints[c][i] * mPlotBounds.H();
517 g.PathLineTo(xi, yi);
518 }
519 }
520 }
521
522 // Stroke the curve for crispness
523 g.PathStroke(IPattern(baseColor), mCurveThickness);
524 }
525 }
526
527 void DrawCursorValues(IGraphics& g)
528 {
529 WDL_String label;
530
531 if (mCursorFreq >= 0.0)
532 {
533 label.SetFormatted(64, "%.1fHz", mCursorFreq);
534 g.DrawText(mStyle.valueText, label.Get(), mPlotBounds.GetFromTRHC(100, 50).FracRectVertical(0.5));
535 }
536
537 if (mAmpScale == EAmplitudeScale::Linear)
538 label.SetFormatted(64, "%.3fs", mCursorAmp);
539 else
540 label.SetFormatted(64, "%ddB", (int) mCursorAmp);
541
542 g.DrawText(mStyle.valueText, label.Get(), mPlotBounds.GetFromTRHC(100, 50).FracRectVertical(0.5, true));
543 }
544
545#pragma mark -
546
547 void SetFFTSize(int fftSize)
548 {
549 assert(fftSize > 0);
550 assert(fftSize <= MAX_FFT_SIZE);
551 mFFTSize = fftSize;
552
553 ResizePoints();
554 CalculateXPoints();
555 SetFreqRange(FirstBinFreq(), NyquistFreq());
556 SetSmoothing(mAttackTimeMs, mDecayTimeMs);
557 SetDirty(false);
558 }
559
560 void SetSampleRate(double sampleRate)
561 {
562 mSampleRate = sampleRate;
563 SetFreqRange(FirstBinFreq(), NyquistFreq());
564 SetSmoothing(mAttackTimeMs, mDecayTimeMs);
565 SetDirty(false);
566 }
567
568 void SetFreqRange(float freqLo, float freqHi)
569 {
570 mFreqLo = freqLo;
571 mFreqHi = freqHi;
572 UpdatePlotBounds();
573 SetDirty(false);
574 }
575
576 void SetFrequencyScale(EFrequencyScale scale)
577 {
578 mFreqScale = scale;
579 CalculateXPoints();
580 SetDirty(false);
581 }
582
583 void SetAmpRange(float ampLo, float ampHi)
584 {
585 mAmpLo = ampLo;
586 mAmpHi = ampHi;
587 UpdatePlotBounds();
588 SetDirty(false);
589 }
590
591 void SetOctaveGain(float octaveGain)
592 {
593 mOctaveGain = octaveGain;
594 SetDirty(false);
595 }
596
597 void SetCurveSmoothing(float amount)
598 {
599 mCurveSmoothing = Clip(amount, 0.f, 1.f);
600 SetDirty(false);
601 }
602
603 void SetSmoothing(float attackTimeMs, float releaseTimeMs)
604 {
605 auto attackTimeSec = attackTimeMs * 0.001f;
606 auto releaseTimeSec = releaseTimeMs * 0.001f;
607 auto updatePeriod = (float) mFFTSize / (float) mSampleRate;
608 mAttackCoeff = exp(-updatePeriod / attackTimeSec);
609 mReleaseCoeff = exp(-updatePeriod / releaseTimeSec);
610 }
611
612protected:
613 float ApplyOctaveGain(float amp, float freqNorm)
614 {
615 // Center on 500Hz
616 const float centerFreq = 500.0f;
617 float centerFreqNorm = (centerFreq - mFreqLo)/(mFreqHi - mFreqLo);
618
619 if (mOctaveGain > 0.0)
620 {
621 amp *= freqNorm/centerFreqNorm;
622 }
623
624 return amp;
625 }
626
627 void ResizePoints()
628 {
629 mXPoints.resize(NumPoints());
630
631 for (auto c = 0; c < MAXNC; c++)
632 {
633 mYPoints[c].assign(NumPoints(), 0.0f);
634 mEnvelopeValues[c].assign(NumPoints(), 0.0f);
635 }
636 }
637
638 void CalculateXPoints()
639 {
640 const auto numBins = NumBins();
641 const auto xIncr = (1.0f / static_cast<float>(numBins-1)) * NyquistFreq();
642 mXPoints[0] = 0.0f;
643 for (auto i = 1; i < numBins; i++)
644 {
645 auto xVal = CalcXNorm(float(i) * xIncr, mFreqScale);
646 mXPoints[i] = xVal;
647 }
648 mXPoints[numBins] = mXPoints[numBins-1];
649 mXPoints[numBins+1] = mXPoints[0];
650 }
651
652 void CalculateYPoints(int ch, const TDataPacket& powerSpectrum)
653 {
654 const auto numBins = NumBins();
655
656 for (auto i = 0; i < numBins; i++)
657 {
658 const auto binNorm = numBins > 1 ? static_cast<float>(i) / static_cast<float>(numBins - 1) : 0.f;
659 const auto adjustedAmp = ApplyOctaveGain(powerSpectrum[i], binNorm);
660 float rawVal = (mAmpScale == EAmplitudeScale::Decibel)
661 ? AmpToDB(adjustedAmp + 1e-30f)
662 : adjustedAmp;
663 rawVal = Clip(CalcYNorm(rawVal, mAmpScale), 0.0f, 1.0f);
664
665 float prevVal = mEnvelopeValues[ch][i];
666 float newVal;
667 if (rawVal > prevVal)
668 newVal = mAttackCoeff * prevVal + (1.0f - mAttackCoeff) * rawVal; // Attack phase
669 else
670 newVal = mReleaseCoeff * prevVal + (1.0f - mReleaseCoeff) * rawVal; // Release phase
671
672 mEnvelopeValues[ch][i] = newVal; // Store smoothed value
673 mYPoints[ch][i] = newVal; // Use smoothed value for drawing
674 }
675
676 if (FillCurves())
677 {
678 // Used to close the path outside the bounds of the control
679 auto offset = mCurveThickness / std::max(mPlotBounds.H(), 1.f);
680
681 mYPoints[ch][numBins] = -offset;
682 mYPoints[ch][numBins+1] = -offset;
683 }
684
685 SetDirty(false);
686 }
687
688 float CalcXNorm(float x, EFrequencyScale scale, bool inverted = false)
689 {
690 const auto nyquist = NyquistFreq();
691
692 switch (scale)
693 {
694 case EFrequencyScale::Linear:
695 {
696 if (!inverted)
697 return (x - mFreqLo) / (mFreqHi - mFreqLo);
698 else
699 return (mFreqLo + x * (mFreqHi - mFreqLo)) / nyquist;
700 }
701 case EFrequencyScale::Log:
702 {
703 const auto logXLo = std::log(mFreqLo / nyquist);
704 const auto logXHi = std::log(mFreqHi / nyquist);
705
706 if (!inverted)
707 return (std::log(x / nyquist) - logXLo) / (logXHi - logXLo);
708 else
709 return std::exp(logXLo + x * (logXHi - logXLo));
710 }
711 }
712 }
713
714 // Amplitudes
715 float CalcYNorm(float y, EAmplitudeScale scale, bool inverted = false) const
716 {
717 switch (scale)
718 {
719 case EAmplitudeScale::Linear:
720 {
721 if (!inverted)
722 return (y - mAmpLo) / (mAmpHi - mAmpLo);
723 else
724 return mAmpLo + y * (mAmpHi - mAmpLo);
725 }
726 case EAmplitudeScale::Decibel:
727 {
728 const auto dBYLo = AmpToDB(mAmpLo);
729 const auto dBYHi = AmpToDB(mAmpHi);
730
731 if (!inverted)
732 return (y - dBYLo) / (dBYHi - dBYLo);
733 else
734 return dBYLo + y * (dBYHi - dBYLo);
735 }
736 }
737 }
738
739 int NumPoints() const { return FillCurves() ? NumBins() + numExtraPoints : NumBins(); }
740 int NumBins() const { return mFFTSize / 2; }
741 double FirstBinFreq() const { return NyquistFreq()/mFFTSize; }
742 double NyquistFreq() const { return mSampleRate * 0.5; }
743 bool FillCurves() const { return mFillOpacity > 0.0f; }
744
745private:
746 std::vector<IColor> mChannelColors;
747 EFrequencyScale mFreqScale;
748 EAmplitudeScale mAmpScale;
749
750 double mSampleRate = 44100.0;
751 int mFFTSize = 1024;
752 float mOctaveGain = 0.0;
753 float mFreqLo = 20.0;
754 float mFreqHi = 22050.0;
755 float mAmpLo = 0.0;
756 float mAmpHi = 1.0;
757 float mAttackTimeMs = 3.0;
758 float mDecayTimeMs = 50.0;
759 EChannelType mChanType = EChannelType::LeftAndRight;
760
761 float mCurveThickness = 1.0f;
762 float mGridThickness = 1.0f;
763 float mFillOpacity = 0.5f;
764 float mCursorAmp = 0.0;
765 float mCursorFreq = -1.0;
766 IPopupMenu mMenu {"Options"};
767 IRECT mPlotBounds;
768
769 std::vector<float> mXPoints;
770 std::array<std::vector<float>, MAXNC> mYPoints;
771 std::array<std::vector<float>, MAXNC> mEnvelopeValues;
772 float mAttackCoeff = 0.2f;
773 float mReleaseCoeff = 0.99f;
774 int mOverlap = 1;
775 int mWindowType = 0; // matches ISpectrumSender<>::EWindowType::Hann
776 float mCurveSmoothing = 0.6f; // 0 = straight lines, 1 = full Catmull-Rom
777};
778
779END_IGRAPHICS_NAMESPACE
780END_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:2515
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.
Definition: IGraphics.cpp:683
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:1976
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.
Definition: IGraphics.cpp:2434
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.
virtual float MeasureText(const IText &text, const char *str, IRECT &bounds) const
Measure the rectangular region that some text will occupy.
Definition: IGraphics.cpp:691
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.
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.
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
IText is used to manage font and text/text entry style for a piece of text on the UI,...
A struct encapsulating a set of properties used to configure IVControls.