iPlug2 - C++ Audio Plug-in Framework
Loading...
Searching...
No Matches
IVBarGraphSpectrumAnalyzerControl.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
28{
29 float lowRangeDB;
30 float highRangeDB;
31 int nSegs;
32 IColor color;
33
34 SpectrumLEDRange(float low, float high, int segs, IColor c)
35 : lowRangeDB(low), highRangeDB(high), nSegs(segs), color(c) {}
36};
37
38// Default LED colors for spectrum analyzer
39const static IColor SPEC_LED1 = {255, 36, 157, 16}; // Green (quiet)
40const static IColor SPEC_LED2 = {255, 153, 191, 28}; // Yellow-green
41const static IColor SPEC_LED3 = {255, 215, 222, 37}; // Yellow
42const static IColor SPEC_LED4 = {255, 247, 153, 33}; // Orange
43const static IColor SPEC_LED5 = COLOR_RED; // Red (loud)
44
45// ISO standard 1/3 octave center frequencies (Hz)
46const static float kThirdOctaveCenterFreqs[] = {
47 20.f, 25.f, 31.5f, 40.f, 50.f, 63.f, 80.f, 100.f, 125.f, 160.f,
48 200.f, 250.f, 315.f, 400.f, 500.f, 630.f, 800.f, 1000.f, 1250.f, 1600.f,
49 2000.f, 2500.f, 3150.f, 4000.f, 5000.f, 6300.f, 8000.f, 10000.f, 12500.f, 16000.f, 20000.f
50};
51const static int kNumThirdOctaveBands = 31;
52
55template <int MAXNC = 2, int MAX_FFT_SIZE = 4096>
57 , public IVectorBase
58{
59public:
60 using TDataPacket = std::array<float, MAX_FFT_SIZE>;
61
64 {
65 kMsgTagSampleRate = 1,
66 kMsgTagFFTSize,
67 kMsgTagOverlap,
68 kMsgTagWindowType,
69 kMsgTagOctaveGain
70 };
71
72 enum class EFrequencyScale { Linear, Log, ThirdOctave };
73 enum class EColorMode { Segments, Smooth };
74 enum class EChannelMode { Left, Right, Sum, SideBySide, Split };
75
76 static constexpr float kPeakDecayRate = 0.95f;
77
96 const char* label = "",
97 const IVStyle& style = DEFAULT_STYLE,
98 int nBars = 32,
99 int nSegsPerBar = 16,
100 EFrequencyScale freqScale = EFrequencyScale::Log,
101 EColorMode colorMode = EColorMode::Segments,
102 EChannelMode channelMode = EChannelMode::Sum,
103 const IPattern& barPattern = IPattern(COLOR_GREEN),
104 const std::vector<SpectrumLEDRange>& ledRanges = {
105 {0.f, 6.f, 2, SPEC_LED5},
106 {-12.f, 0.f, 3, SPEC_LED4},
107 {-24.f, -12.f, 3, SPEC_LED3},
108 {-48.f, -24.f, 4, SPEC_LED2},
109 {-72.f, -48.f, 4, SPEC_LED1}
110 },
111 float gapRatio = 0.2f,
112 float segGapRatio = 0.1f,
113 float attackTimeMs = 5.0f,
114 float decayTimeMs = 50.0f,
115 float lowRangeDB = -72.f,
116 float highRangeDB = 6.f)
117 : IControl(bounds)
118 , IVectorBase(style)
119 , mNBars(nBars)
120 , mNSegsPerBar(nSegsPerBar)
121 , mFreqScale(freqScale)
122 , mColorMode(colorMode)
123 , mChannelMode(channelMode)
124 , mBarPattern(barPattern)
125 , mLEDRanges(ledRanges)
126 , mGapRatio(gapRatio)
127 , mSegGapRatio(segGapRatio)
128 , mAttackTimeMs(attackTimeMs)
129 , mDecayTimeMs(decayTimeMs)
130 , mLowRangeDB(lowRangeDB)
131 , mHighRangeDB(highRangeDB)
132 {
133 AttachIControl(this, label);
134 ResizeBarArrays();
135 SetFFTSize(1024);
136 CalculateFrequencyBands();
137 }
138
139 void OnResize() override
140 {
141 SetTargetRECT(MakeRects(mRECT));
142 SetDirty(false);
143 }
144
145 void OnMsgFromDelegate(int msgTag, int dataSize, const void* pData) override
146 {
147 IByteStream stream(pData, dataSize);
148
149 if (!IsDisabled() && msgTag == ISender<>::kUpdateMessage)
150 {
152 stream.Get(&d, 0);
153 ProcessSpectrumData(d);
154 }
155 else if (msgTag == kMsgTagSampleRate)
156 {
157 double sr;
158 stream.Get(&sr, 0);
159 SetSampleRate(sr);
160 }
161 else if (msgTag == kMsgTagFFTSize)
162 {
163 int fftSize;
164 stream.Get(&fftSize, 0);
165 SetFFTSize(fftSize);
166 }
167 else if (msgTag == kMsgTagOverlap)
168 {
169 int overlap;
170 stream.Get(&overlap, 0);
171 mOverlap = overlap;
172 }
173 else if (msgTag == kMsgTagWindowType)
174 {
175 int windowType;
176 stream.Get(&windowType, 0);
177 mWindowType = windowType;
178 }
179 else if (msgTag == kMsgTagOctaveGain)
180 {
181 double octaveGain;
182 stream.Get(&octaveGain, 0);
183 SetOctaveGain(static_cast<float>(octaveGain));
184 }
185 }
186
187 void OnMouseDown(float x, float y, const IMouseMod& mod) override
188 {
189 if (mod.R)
190 {
191 mMenu.Clear(true);
192 auto* pFftSizeMenu = mMenu.AddItem("FFT Size", new IPopupMenu("FFT Size", { "64", "128", "256", "512", "1024", "2048", "4096" }))->GetSubmenu();
193 auto* pChansMenu = mMenu.AddItem("Channels", new IPopupMenu("Channels", { "L", "R", "Sum", "L + R", "L | R" }))->GetSubmenu();
194 auto* pFreqScaleMenu = mMenu.AddItem("Freq Scaling", new IPopupMenu("Freq Scaling", { "Linear", "Log", "1/3 Octave" }))->GetSubmenu();
195 auto* pOverlapMenu = mMenu.AddItem("Overlap", new IPopupMenu("Overlap", { "1x", "2x", "4x", "8x" }))->GetSubmenu();
196 auto* pWindowMenu = mMenu.AddItem("Window", new IPopupMenu("Window", { "Hann", "Blackman Harris", "Hamming", "Flattop", "Rectangular" }))->GetSubmenu();
197
198 pFftSizeMenu->CheckItem(0, mFFTSize == 64);
199 pFftSizeMenu->CheckItem(1, mFFTSize == 128);
200 pFftSizeMenu->CheckItem(2, mFFTSize == 256);
201 pFftSizeMenu->CheckItem(3, mFFTSize == 512);
202 pFftSizeMenu->CheckItem(4, mFFTSize == 1024);
203 pFftSizeMenu->CheckItem(5, mFFTSize == 2048);
204 pFftSizeMenu->CheckItem(6, mFFTSize == 4096);
205
206 pChansMenu->CheckItem(0, mChannelMode == EChannelMode::Left);
207 pChansMenu->CheckItem(1, mChannelMode == EChannelMode::Right);
208 pChansMenu->CheckItem(2, mChannelMode == EChannelMode::Sum);
209 pChansMenu->CheckItem(3, mChannelMode == EChannelMode::SideBySide);
210 pChansMenu->CheckItem(4, mChannelMode == EChannelMode::Split);
211
212 pFreqScaleMenu->CheckItem(0, mFreqScale == EFrequencyScale::Linear);
213 pFreqScaleMenu->CheckItem(1, mFreqScale == EFrequencyScale::Log);
214 pFreqScaleMenu->CheckItem(2, mFreqScale == EFrequencyScale::ThirdOctave);
215
216 pOverlapMenu->CheckItem(0, mOverlap == 1);
217 pOverlapMenu->CheckItem(1, mOverlap == 2);
218 pOverlapMenu->CheckItem(2, mOverlap == 4);
219 pOverlapMenu->CheckItem(3, mOverlap == 8);
220
221 pWindowMenu->CheckItem(0, mWindowType == 0);
222 pWindowMenu->CheckItem(1, mWindowType == 1);
223 pWindowMenu->CheckItem(2, mWindowType == 2);
224 pWindowMenu->CheckItem(3, mWindowType == 3);
225 pWindowMenu->CheckItem(4, mWindowType == 4);
226
227 GetUI()->CreatePopupMenu(*this, mMenu, x, y);
228 }
229 }
230
231 void OnPopupMenuSelection(IPopupMenu* pSelectedMenu, int valIdx) override
232 {
233 if (pSelectedMenu)
234 {
235 const char* title = pSelectedMenu->GetRootTitle();
236
237 if (strcmp(title, "FFT Size") == 0)
238 {
239 int fftSize = atoi(pSelectedMenu->GetChosenItem()->GetText());
240 GetDelegate()->SendArbitraryMsgFromUI(kMsgTagFFTSize, kNoTag, sizeof(int), &fftSize);
241 SetFFTSize(fftSize);
242 }
243 else if (strcmp(title, "Channels") == 0)
244 {
245 int idx = pSelectedMenu->GetChosenItemIdx();
246 switch (idx)
247 {
248 case 0: SetChannelMode(EChannelMode::Left); break;
249 case 1: SetChannelMode(EChannelMode::Right); break;
250 case 2: SetChannelMode(EChannelMode::Sum); break;
251 case 3: SetChannelMode(EChannelMode::SideBySide); break;
252 case 4: SetChannelMode(EChannelMode::Split); break;
253 }
254 }
255 else if (strcmp(title, "Freq Scaling") == 0)
256 {
257 int idx = pSelectedMenu->GetChosenItemIdx();
258 EFrequencyScale scale = EFrequencyScale::Linear;
259 if (idx == 1) scale = EFrequencyScale::Log;
260 else if (idx == 2) scale = EFrequencyScale::ThirdOctave;
261 SetFrequencyScale(scale);
262 }
263 else if (strcmp(title, "Overlap") == 0)
264 {
265 const char* txt = pSelectedMenu->GetChosenItem()->GetText();
266 int overlap = atoi(txt);
267 if (overlap <= 0)
268 overlap = 1;
269 GetDelegate()->SendArbitraryMsgFromUI(kMsgTagOverlap, kNoTag, sizeof(int), &overlap);
270 mOverlap = overlap;
271 }
272 else if (strcmp(title, "Window") == 0)
273 {
274 int idx = pSelectedMenu->GetChosenItemIdx();
275 GetDelegate()->SendArbitraryMsgFromUI(kMsgTagWindowType, kNoTag, sizeof(int), &idx);
276 mWindowType = idx;
277 }
278 }
279 }
280
281 void Draw(IGraphics& g) override
282 {
283 DrawBackground(g, mRECT);
284 DrawWidget(g);
285 DrawLabel(g);
286
287 if (mStyle.drawFrame)
288 {
289 if (mChannelMode == EChannelMode::Split)
290 {
291 // Draw separate frames for left and right halves
292 const float halfWidth = mWidgetBounds.W() * 0.5f;
293 IRECT leftBounds(mWidgetBounds.L, mWidgetBounds.T, mWidgetBounds.L + halfWidth, mWidgetBounds.B);
294 IRECT rightBounds(mWidgetBounds.L + halfWidth, mWidgetBounds.T, mWidgetBounds.R, mWidgetBounds.B);
295 g.DrawRect(GetColor(kFR), leftBounds, &mBlend, mStyle.frameThickness);
296 g.DrawRect(GetColor(kFR), rightBounds, &mBlend, mStyle.frameThickness);
297 }
298 else
299 {
300 g.DrawRect(GetColor(kFR), mWidgetBounds, &mBlend, mStyle.frameThickness);
301 }
302 }
303 }
304
305 void SetNumBars(int nBars)
306 {
307 mNBars = nBars;
308 ResizeBarArrays();
309 CalculateFrequencyBands();
310 SetDirty(false);
311 }
312
313 void SetNumSegments(int nSegs)
314 {
315 mNSegsPerBar = nSegs;
316 SetDirty(false);
317 }
318
319 void SetColorMode(EColorMode mode)
320 {
321 mColorMode = mode;
322 SetDirty(false);
323 }
324
325 void SetChannelMode(EChannelMode mode)
326 {
327 mChannelMode = mode;
328 ResizeBarArrays();
329 SetDirty(false);
330 }
331
334 void SetBarPattern(const IPattern& pattern)
335 {
336 mBarPattern = pattern;
337 SetDirty(false);
338 }
339
340 void SetFrequencyScale(EFrequencyScale scale)
341 {
342 // Store current bar count when leaving non-ThirdOctave mode
343 if (mFreqScale != EFrequencyScale::ThirdOctave && scale == EFrequencyScale::ThirdOctave)
344 {
345 mStoredNBars = mNBars;
346 }
347 // Restore bar count when leaving ThirdOctave mode
348 else if (mFreqScale == EFrequencyScale::ThirdOctave && scale != EFrequencyScale::ThirdOctave)
349 {
350 mNBars = mStoredNBars;
351 ResizeBarArrays();
352 }
353
354 mFreqScale = scale;
355 CalculateFrequencyBands();
356 SetDirty(false);
357 }
358
359 void SetPeakHoldTime(int timeMs)
360 {
361 mPeakHoldTimeMs = timeMs;
362 }
363
364 void SetShowPeaks(bool show)
365 {
366 mShowPeaks = show;
367 SetDirty(false);
368 }
369
373 void SetClipIndicator(float thresholdDB, const IColor& clipColor = COLOR_RED)
374 {
375 mClipThresholdDB = thresholdDB;
376 mClipColor = clipColor;
377 mShowClipIndicator = true;
378 SetDirty(false);
379 }
380
381 void SetShowClipIndicator(bool show)
382 {
383 mShowClipIndicator = show;
384 SetDirty(false);
385 }
386
387 void SetFFTSize(int fftSize)
388 {
389 mFFTSize = fftSize;
390 SetSmoothing(mAttackTimeMs, mDecayTimeMs);
391 CalculateFrequencyBands();
392 SetDirty(false);
393 }
394
395 void SetSampleRate(double sr)
396 {
397 mSampleRate = sr;
398 SetSmoothing(mAttackTimeMs, mDecayTimeMs);
399 CalculateFrequencyBands();
400 SetDirty(false);
401 }
402
403 void SetOctaveGain(float octaveGain)
404 {
405 mOctaveGain = octaveGain;
406 SetDirty(false);
407 }
408
409private:
410 void ResizeBarArrays()
411 {
412 const int numBars = (mChannelMode == EChannelMode::SideBySide || mChannelMode == EChannelMode::Split) ? mNBars * 2 : mNBars;
413 mBarValues.resize(numBars, 0.f);
414 mPeakValues.resize(numBars, 0.f);
415 mPeakHoldCounters.resize(numBars, 0);
416 }
417
418 void DrawWidget(IGraphics& g) override
419 {
420 const float totalWidth = mWidgetBounds.W();
421 const float totalHeight = mWidgetBounds.H();
422 const float segHeightWithGap = totalHeight / mNSegsPerBar;
423 const float segHeight = segHeightWithGap * (1.f - mSegGapRatio);
424 const float segGap = segHeightWithGap * mSegGapRatio;
425
426 if (mChannelMode == EChannelMode::SideBySide)
427 {
428 // Draw L and R bars side by side for each frequency band
429 const float pairWidthWithGap = totalWidth / mNBars;
430 const float pairGap = pairWidthWithGap * mGapRatio;
431 const float pairWidth = pairWidthWithGap - pairGap;
432 const float singleBarWidth = pairWidth * 0.5f;
433
434 for (int bar = 0; bar < mNBars; bar++)
435 {
436 const float pairX = mWidgetBounds.L + bar * pairWidthWithGap + pairGap * 0.5f;
437
438 // Left channel bar
439 const float leftBarX = pairX;
440 const float leftValue = mBarValues[bar * 2];
441 DrawBar(g, leftBarX, singleBarWidth, segHeight, segGap, leftValue, 0);
442
443 if (mShowPeaks && mPeakValues[bar * 2] > 0.001f)
444 {
445 DrawPeakIndicator(g, leftBarX, singleBarWidth, segHeightWithGap, mPeakValues[bar * 2], 0);
446 }
447
448 // Right channel bar
449 const float rightBarX = pairX + singleBarWidth;
450 const float rightValue = mBarValues[bar * 2 + 1];
451 DrawBar(g, rightBarX, singleBarWidth, segHeight, segGap, rightValue, 1);
452
453 if (mShowPeaks && mPeakValues[bar * 2 + 1] > 0.001f)
454 {
455 DrawPeakIndicator(g, rightBarX, singleBarWidth, segHeightWithGap, mPeakValues[bar * 2 + 1], 1);
456 }
457 }
458 }
459 else if (mChannelMode == EChannelMode::Split)
460 {
461 // Draw all L bars on left half, all R bars on right half
462 const float halfWidth = totalWidth * 0.5f;
463 const float barWidthWithGap = halfWidth / mNBars;
464 const float barWidth = barWidthWithGap * (1.f - mGapRatio);
465 const float gapWidth = barWidthWithGap * mGapRatio;
466
467 // Left channel bars (left half)
468 for (int bar = 0; bar < mNBars; bar++)
469 {
470 const float barX = mWidgetBounds.L + bar * barWidthWithGap + gapWidth * 0.5f;
471 const float barValue = mBarValues[bar * 2];
472
473 DrawBar(g, barX, barWidth, segHeight, segGap, barValue, 0);
474
475 if (mShowPeaks && mPeakValues[bar * 2] > 0.001f)
476 {
477 DrawPeakIndicator(g, barX, barWidth, segHeightWithGap, mPeakValues[bar * 2], 0);
478 }
479 }
480
481 // Right channel bars (right half)
482 for (int bar = 0; bar < mNBars; bar++)
483 {
484 const float barX = mWidgetBounds.L + halfWidth + bar * barWidthWithGap + gapWidth * 0.5f;
485 const float barValue = mBarValues[bar * 2 + 1];
486
487 DrawBar(g, barX, barWidth, segHeight, segGap, barValue, 1);
488
489 if (mShowPeaks && mPeakValues[bar * 2 + 1] > 0.001f)
490 {
491 DrawPeakIndicator(g, barX, barWidth, segHeightWithGap, mPeakValues[bar * 2 + 1], 1);
492 }
493 }
494 }
495 else
496 {
497 // Single bar per frequency band (Left, Right, or Sum)
498 const float barWidthWithGap = totalWidth / mNBars;
499 const float barWidth = barWidthWithGap * (1.f - mGapRatio);
500 const float gapWidth = barWidthWithGap * mGapRatio;
501
502 for (int bar = 0; bar < mNBars; bar++)
503 {
504 const float barX = mWidgetBounds.L + bar * barWidthWithGap + gapWidth * 0.5f;
505 const float barValue = mBarValues[bar];
506
507 DrawBar(g, barX, barWidth, segHeight, segGap, barValue);
508
509 if (mShowPeaks && mPeakValues[bar] > 0.001f)
510 {
511 DrawPeakIndicator(g, barX, barWidth, segHeightWithGap, mPeakValues[bar]);
512 }
513 }
514 }
515 }
516
517 void DrawBar(IGraphics& g, float barX, float barWidth, float segHeight, float segGap, float value, int channelIdx = -1)
518 {
519 if (mColorMode == EColorMode::Smooth)
520 {
521 // Smooth mode - continuous fill using IPattern (solid or gradient)
522 if (value > 0.001f)
523 {
524 // Calculate clip threshold as normalized value
525 const float clipThresholdNorm = mShowClipIndicator ?
526 (mClipThresholdDB - mLowRangeDB) / (mHighRangeDB - mLowRangeDB) : 1.0f;
527
528 const bool isClipping = mShowClipIndicator && value > clipThresholdNorm;
529
530 // Draw gradient portion - if clipping, only draw up to threshold; otherwise draw full value
531 const float gradientValue = isClipping ? clipThresholdNorm : value;
532 if (gradientValue > 0.001f)
533 {
534 const float barHeight = gradientValue * mWidgetBounds.H();
535 const float barY = mWidgetBounds.B - barHeight;
536 IRECT barRect(barX, barY, barX + barWidth, mWidgetBounds.B);
537
538 // Create a vertical gradient for this bar based on the stored pattern
539 IPattern pattern = IPattern::CreateLinearGradient(barX, mWidgetBounds.B, barX, mWidgetBounds.T);
540
541 // Copy stops from the stored pattern
542 for (int i = 0; i < mBarPattern.NStops(); i++)
543 {
544 const IColorStop& stop = mBarPattern.GetStop(i);
545 pattern.AddStop(stop.mColor, stop.mOffset);
546 }
547
548 g.PathRect(barRect);
549 g.PathFill(pattern);
550 }
551
552 // Draw clip indicator on top if value exceeds threshold
553 if (isClipping)
554 {
555 const float clipStartY = mWidgetBounds.B - (clipThresholdNorm * mWidgetBounds.H());
556 const float clipEndY = mWidgetBounds.T;
557 IRECT clipRect(barX, clipEndY, barX + barWidth, clipStartY);
558
559 IColor clipColor = mClipColor;
560 if (channelIdx == 1 && mChannelMode != EChannelMode::Split)
561 {
562 clipColor = clipColor.WithContrast(0.2f);
563 }
564 g.FillRect(clipColor, clipRect);
565 }
566 }
567 }
568 else
569 {
570 // Segments mode - LED-style segments with color ranges
571 int segIdx = 0;
572
573 for (const auto& range : mLEDRanges)
574 {
575 for (int i = 0; i < range.nSegs; i++)
576 {
577 const int totalSeg = mNSegsPerBar - 1 - segIdx;
578 const float segNorm = static_cast<float>(totalSeg + 1) / mNSegsPerBar;
579
580 if (value >= segNorm - 0.001f)
581 {
582 const float segY = mWidgetBounds.B - (totalSeg + 1) * (segHeight + segGap) + segGap * 0.5f;
583 IRECT segRect(barX, segY, barX + barWidth, segY + segHeight);
584 IColor segColor = range.color;
585 // In SideBySide mode, slightly adjust color for R channel (not in Split mode)
586 if (channelIdx == 1 && mChannelMode != EChannelMode::Split)
587 {
588 segColor = segColor.WithContrast(0.2f);
589 }
590 g.FillRect(segColor, segRect);
591 }
592
593 segIdx++;
594 if (segIdx >= mNSegsPerBar) break;
595 }
596 if (segIdx >= mNSegsPerBar) break;
597 }
598 }
599 }
600
601 void DrawPeakIndicator(IGraphics& g, float barX, float barWidth, float segHeightWithGap, float peakValue, int channelIdx = -1)
602 {
603 if (peakValue <= 0.001f) return;
604
605 // In Smooth mode with clip indicator, don't draw peak if it's in the clip region
606 if (mColorMode == EColorMode::Smooth && mShowClipIndicator)
607 {
608 const float clipThresholdNorm = (mClipThresholdDB - mLowRangeDB) / (mHighRangeDB - mLowRangeDB);
609 if (peakValue >= clipThresholdNorm)
610 return; // Peak is in clip region, don't draw
611 }
612
613 float peakY;
614 if (mColorMode == EColorMode::Smooth)
615 {
616 // Continuous positioning for smooth mode
617 peakY = mWidgetBounds.B - (peakValue * mWidgetBounds.H());
618 }
619 else
620 {
621 // Segment-based positioning for LED mode
622 const int peakSeg = static_cast<int>(peakValue * mNSegsPerBar);
623 if (peakSeg <= 0) return;
624 peakY = mWidgetBounds.B - peakSeg * segHeightWithGap;
625 }
626
627 IRECT peakRect(barX, peakY, barX + barWidth, peakY + 2.f);
628
629 IColor peakColor;
630 if (mColorMode == EColorMode::Segments)
631 {
632 peakColor = GetColorForLevel(peakValue);
633 }
634 else
635 {
636 peakColor = GetPatternColorAtPosition(peakValue);
637 }
638 if (channelIdx == 1 && mChannelMode != EChannelMode::Split)
639 {
640 peakColor = peakColor.WithContrast(0.2f);
641 }
642
643 g.FillRect(peakColor, peakRect);
644 }
645
646 IColor GetColorForLevel(float normalizedLevel)
647 {
648 const float dB = mLowRangeDB + normalizedLevel * (mHighRangeDB - mLowRangeDB);
649
650 for (const auto& range : mLEDRanges)
651 {
652 if (dB >= range.lowRangeDB && dB <= range.highRangeDB)
653 {
654 return range.color;
655 }
656 }
657
658 return mLEDRanges.empty() ? COLOR_GREEN : mLEDRanges.back().color;
659 }
660
662 IColor GetPatternColorAtPosition(float pos)
663 {
664 pos = Clip(pos, 0.f, 1.f);
665 const int nStops = mBarPattern.NStops();
666
667 if (nStops == 0)
668 return COLOR_GREEN;
669 if (nStops == 1)
670 return mBarPattern.GetStop(0).mColor;
671
672 // Find the two stops to interpolate between
673 const IColorStop* prevStop = &mBarPattern.GetStop(0);
674 const IColorStop* nextStop = &mBarPattern.GetStop(nStops - 1);
675
676 for (int i = 0; i < nStops - 1; i++)
677 {
678 const IColorStop& s1 = mBarPattern.GetStop(i);
679 const IColorStop& s2 = mBarPattern.GetStop(i + 1);
680 if (pos >= s1.mOffset && pos <= s2.mOffset)
681 {
682 prevStop = &s1;
683 nextStop = &s2;
684 break;
685 }
686 }
687
688 // Interpolate
689 const float range = nextStop->mOffset - prevStop->mOffset;
690 const float t = (range > 0.f) ? (pos - prevStop->mOffset) / range : 0.f;
691
692 return IColor::LinearInterpolateBetween(prevStop->mColor, nextStop->mColor, t);
693 }
694
695 void ProcessSpectrumData(const ISenderData<MAXNC, TDataPacket>& d)
696 {
697 const float rangeDB = mHighRangeDB - mLowRangeDB;
698 const float lowPointAbs = std::fabs(mLowRangeDB);
699 const int numBins = mFFTSize / 2;
700
701 if (mChannelMode == EChannelMode::SideBySide || mChannelMode == EChannelMode::Split)
702 {
703 // Process L and R channels separately
704 for (int bar = 0; bar < mNBars; bar++)
705 {
706 const int startBin = mBandStartBins[bar];
707 const int endBin = mBandEndBins[bar];
708 const float freqNorm = static_cast<float>(startBin + endBin) / (2.f * numBins);
709
710 for (int ch = 0; ch < 2; ch++)
711 {
712 if (d.nChans <= 0) break; // Protect against invalid channel count
713 const int channelIdx = d.chanOffset + std::min(ch, d.nChans - 1);
714 const int barIdx = bar * 2 + ch;
715
716 float maxMag = 0.f;
717 for (int bin = startBin; bin < endBin; bin++)
718 {
719 float mag = d.vals[channelIdx][bin];
720 if (mag > maxMag) maxMag = mag;
721 }
722
723 UpdateBarValue(barIdx, maxMag, lowPointAbs, rangeDB, freqNorm);
724 }
725 }
726 }
727 else
728 {
729 // Left, Right, or Sum mode
730 for (int bar = 0; bar < mNBars; bar++)
731 {
732 const int startBin = mBandStartBins[bar];
733 const int endBin = mBandEndBins[bar];
734 const float freqNorm = static_cast<float>(startBin + endBin) / (2.f * numBins);
735
736 float maxMag = 0.f;
737 for (int bin = startBin; bin < endBin; bin++)
738 {
739 float mag = 0.f;
740
741 if (mChannelMode == EChannelMode::Left)
742 {
743 mag = d.vals[d.chanOffset][bin];
744 }
745 else if (mChannelMode == EChannelMode::Right)
746 {
747 const int rightCh = d.chanOffset + std::min(1, d.nChans - 1);
748 mag = d.vals[rightCh][bin];
749 }
750 else // Sum
751 {
752 for (int ch = d.chanOffset; ch < d.chanOffset + d.nChans; ch++)
753 {
754 mag += d.vals[ch][bin];
755 }
756 mag /= d.nChans;
757 }
758
759 if (mag > maxMag) maxMag = mag;
760 }
761
762 UpdateBarValue(bar, maxMag, lowPointAbs, rangeDB, freqNorm);
763 }
764 }
765
766 SetDirty(false);
767 }
768
769 void UpdateBarValue(int barIdx, float maxMag, float lowPointAbs, float rangeDB, float freqNorm = 0.5f)
770 {
771 // Apply octave gain compensation (boost higher frequencies)
772 if (mOctaveGain > 0.f)
773 {
774 const float centerFreqNorm = 500.f / (mSampleRate * 0.5f);
775 if (centerFreqNorm > 1e-6f) // Protect against division by zero
776 {
777 maxMag *= (freqNorm / centerFreqNorm) * mOctaveGain;
778 }
779 }
780
781 // Convert to dB and normalize
782 float dB = AmpToDB(maxMag + 1e-30f);
783 float normalizedValue = (dB + lowPointAbs) / rangeDB;
784 normalizedValue = Clip(normalizedValue, 0.f, 1.f);
785
786 // Apply smoothing
787 float prevValue = mBarValues[barIdx];
788 float newValue;
789 if (normalizedValue > prevValue)
790 newValue = mAttackCoeff * prevValue + (1.f - mAttackCoeff) * normalizedValue;
791 else
792 newValue = mReleaseCoeff * prevValue + (1.f - mReleaseCoeff) * normalizedValue;
793
794 mBarValues[barIdx] = newValue;
795
796 // Peak hold
797 if (mShowPeaks)
798 {
799 if (newValue > mPeakValues[barIdx])
800 {
801 mPeakValues[barIdx] = newValue;
802 mPeakHoldCounters[barIdx] = mPeakHoldTimeMs;
803 }
804 else if (mPeakHoldCounters[barIdx] > 0)
805 {
806 const int hopSize = mFFTSize / std::max(1, mOverlap);
807 mPeakHoldCounters[barIdx] -= static_cast<int>(1000.f * hopSize / mSampleRate);
808 }
809 else
810 {
811 mPeakValues[barIdx] *= kPeakDecayRate;
812 }
813 }
814 }
815
816 void CalculateFrequencyBands()
817 {
818 const int numBins = mFFTSize / 2;
819 const float nyquist = static_cast<float>(mSampleRate) * 0.5f;
820
821 if (mFreqScale == EFrequencyScale::ThirdOctave)
822 {
823 // 1/3 octave bandwidth factor: 2^(1/6) ≈ 1.1225
824 const float bandwidthFactor = std::pow(2.f, 1.f / 6.f);
825
826 // Find valid bands: must fit within nyquist AND have at least 1 unique bin
827 // We track the previous end bin to avoid duplicate bin ranges
828 std::vector<int> validIndices;
829 std::vector<int> startBins;
830 std::vector<int> endBins;
831 int prevEndBin = -1;
832
833 for (int i = 0; i < kNumThirdOctaveBands; i++)
834 {
835 const float centerFreq = kThirdOctaveCenterFreqs[i];
836 if (centerFreq >= nyquist)
837 break;
838
839 const float lowFreq = centerFreq / bandwidthFactor;
840 const float highFreq = std::min(centerFreq * bandwidthFactor, nyquist);
841
842 int startBin = std::max(1, static_cast<int>(lowFreq / nyquist * numBins));
843 int endBin = static_cast<int>(highFreq / nyquist * numBins);
844
845 // Skip bands that don't have at least one bin beyond the previous band
846 // This ensures each displayed band represents unique frequency content
847 if (endBin > prevEndBin && endBin > startBin)
848 {
849 // Adjust startBin to avoid overlap with previous band
850 if (startBin <= prevEndBin)
851 startBin = prevEndBin + 1;
852
853 if (endBin > startBin)
854 {
855 validIndices.push_back(i);
856 startBins.push_back(startBin);
857 endBins.push_back(endBin);
858 prevEndBin = endBin;
859 }
860 }
861 }
862
863 mNBars = static_cast<int>(validIndices.size());
864 ResizeBarArrays();
865 mBandStartBins.resize(mNBars);
866 mBandEndBins.resize(mNBars);
867 mThirdOctaveIndices = validIndices;
868
869 for (int bar = 0; bar < mNBars; bar++)
870 {
871 mBandStartBins[bar] = startBins[bar];
872 mBandEndBins[bar] = endBins[bar];
873 }
874 }
875 else if (mFreqScale == EFrequencyScale::Log)
876 {
877 mBandStartBins.resize(mNBars);
878 mBandEndBins.resize(mNBars);
879
880 const float minFreq = 20.f;
881 const float maxFreq = nyquist;
882 const float logMin = std::log(minFreq);
883 const float logMax = std::log(maxFreq);
884 const float logRange = logMax - logMin;
885
886 for (int bar = 0; bar < mNBars; bar++)
887 {
888 const float logFreqStart = logMin + (static_cast<float>(bar) / mNBars) * logRange;
889 const float logFreqEnd = logMin + (static_cast<float>(bar + 1) / mNBars) * logRange;
890 const float freqStart = std::exp(logFreqStart);
891 const float freqEnd = std::exp(logFreqEnd);
892
893 mBandStartBins[bar] = std::max(1, static_cast<int>(freqStart / nyquist * numBins));
894 mBandEndBins[bar] = std::max(mBandStartBins[bar] + 1, static_cast<int>(freqEnd / nyquist * numBins));
895 }
896 }
897 else // Linear
898 {
899 mBandStartBins.resize(mNBars);
900 mBandEndBins.resize(mNBars);
901
902 const float binWidth = static_cast<float>(numBins) / mNBars;
903 for (int bar = 0; bar < mNBars; bar++)
904 {
905 mBandStartBins[bar] = static_cast<int>(bar * binWidth);
906 mBandEndBins[bar] = static_cast<int>((bar + 1) * binWidth);
907 }
908 }
909 }
910
911 void SetSmoothing(float attackTimeMs, float releaseTimeMs)
912 {
913 mAttackTimeMs = attackTimeMs;
914 mDecayTimeMs = releaseTimeMs;
915 float attackTimeSec = attackTimeMs * 0.001f;
916 float releaseTimeSec = releaseTimeMs * 0.001f;
917 float updatePeriod = static_cast<float>(mFFTSize) / static_cast<float>(mSampleRate);
918 mAttackCoeff = std::exp(-updatePeriod / attackTimeSec);
919 mReleaseCoeff = std::exp(-updatePeriod / releaseTimeSec);
920 }
921
922 int mNBars = 32;
923 int mStoredNBars = 32; // Stores bar count when switching to ThirdOctave mode
924 int mNSegsPerBar = 16;
925 int mFFTSize = 1024;
926 int mOverlap = 1;
927 int mWindowType = 0;
928 double mSampleRate = 44100.0;
929 float mOctaveGain = 0.f;
930 EFrequencyScale mFreqScale = EFrequencyScale::Log;
931 EColorMode mColorMode = EColorMode::Segments;
932 EChannelMode mChannelMode = EChannelMode::Sum;
933 IPattern mBarPattern = IPattern(COLOR_GREEN);
934 std::vector<SpectrumLEDRange> mLEDRanges;
935
936 float mGapRatio = 0.2f;
937 float mSegGapRatio = 0.1f;
938 float mLowRangeDB = -72.f;
939 float mHighRangeDB = 6.f;
940 float mAttackTimeMs = 5.f;
941 float mDecayTimeMs = 50.f;
942 float mAttackCoeff = 0.9f;
943 float mReleaseCoeff = 0.99f;
944
945 bool mShowPeaks = true;
946 int mPeakHoldTimeMs = 1000;
947
948 bool mShowClipIndicator = false;
949 float mClipThresholdDB = 0.f;
950 IColor mClipColor = COLOR_RED;
951
952 std::vector<float> mBarValues;
953 std::vector<float> mPeakValues;
954 std::vector<int> mPeakHoldCounters;
955 std::vector<int> mBandStartBins;
956 std::vector<int> mBandEndBins;
957 std::vector<int> mThirdOctaveIndices; // Maps displayed bars to kThirdOctaveCenterFreqs indices
958 IPopupMenu mMenu {"Options"};
959};
960
961END_IGRAPHICS_NAMESPACE
962END_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 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
void PathRect(const IRECT &bounds)
Add a rectangle to the current path.
Definition: IGraphics.cpp:2653
virtual void FillRect(const IColor &color, const IRECT &bounds, const IBlend *pBlend=0)
Fill a rectangular region of the graphics context with a color.
Definition: IGraphics.cpp:2587
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 bar graph spectrum analyzer control with segmented LEDs.
void SetBarPattern(const IPattern &pattern)
Set the bar pattern for Smooth color mode.
void OnResize() override
Called when IControl is constructed or resized using SetRect().
void SetClipIndicator(float thresholdDB, const IColor &clipColor=COLOR_RED)
Enable a clip indicator segment above a threshold.
void OnMsgFromDelegate(int msgTag, int dataSize, const void *pData) override
Implement to receive messages sent to the control, see IEditorDelegate:SendControlMsgFromDelegate()
void Draw(IGraphics &g) override
Draw the control to the graphics context.
MsgTags
Message tags for synchronization with ISpectrumSender.
void OnMouseDown(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse down event on this control.
IVBarGraphSpectrumAnalyzerControl(const IRECT &bounds, const char *label="", const IVStyle &style=DEFAULT_STYLE, int nBars=32, int nSegsPerBar=16, EFrequencyScale freqScale=EFrequencyScale::Log, EColorMode colorMode=EColorMode::Segments, EChannelMode channelMode=EChannelMode::Sum, const IPattern &barPattern=IPattern(COLOR_GREEN), const std::vector< SpectrumLEDRange > &ledRanges={ {0.f, 6.f, 2, SPEC_LED5}, {-12.f, 0.f, 3, SPEC_LED4}, {-24.f, -12.f, 3, SPEC_LED3}, {-48.f, -24.f, 4, SPEC_LED2}, {-72.f, -48.f, 4, SPEC_LED1} }, float gapRatio=0.2f, float segGapRatio=0.1f, float attackTimeMs=5.0f, float decayTimeMs=50.0f, float lowRangeDB=-72.f, float highRangeDB=6.f)
Create a bar graph spectrum analyzer.
void OnPopupMenuSelection(IPopupMenu *pSelectedMenu, int valIdx) override
Implement this method to handle popup menu selection after IGraphics::CreatePopupMenu/IControlPromptU...
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)
Used to manage color data, independent of draw class/platform.
IColor WithContrast(float c) const
Returns a new contrasted IColor based on this one.
static IColor LinearInterpolateBetween(const IColor &start, const IColor &dest, float progress)
Helper function to linear interpolate between two IColors.
Used to represent a point/stop in a gradient.
Used to manage mouse modifiers i.e.
Used to store pattern information for gradients.
const IColorStop & GetStop(int idx) const
Get the IColorStop at a particular index (will crash if out of bounds)
int NStops() const
void AddStop(IColor color, float offset)
Add an IColorStop to the IPattern.
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.
float W() const
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.
LED Range for spectrum analyzer bar segments.