Skip to content

Commit 4e04a79

Browse files
committed
More tweaks
1 parent 8a05f10 commit 4e04a79

4 files changed

Lines changed: 321 additions & 41 deletions

File tree

examples/graphics/source/examples/SpectrumAnalyzer.h

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,19 @@ class SpectrumAnalyzerDemo
715715
};
716716
addAndMakeVisible (*displayTypeCombo);
717717

718+
// Level mode selector
719+
levelModeCombo = std::make_unique<yup::ComboBox> ("LevelMode");
720+
levelModeCombo->addItem ("Peak dBFS", 1);
721+
levelModeCombo->addItem ("RMS dBFS", 2);
722+
levelModeCombo->addItem ("Power dBFS", 3);
723+
levelModeCombo->addItem ("PSD dBFS/Hz", 4);
724+
levelModeCombo->setSelectedId (1);
725+
levelModeCombo->onSelectedItemChanged = [this]
726+
{
727+
updateLevelMode();
728+
};
729+
addAndMakeVisible (*levelModeCombo);
730+
718731
// Release control
719732
releaseSlider = std::make_unique<yup::Slider> (yup::Slider::LinearHorizontal, "Release");
720733
releaseSlider->setRange ({ 0.0, 5.0 });
@@ -815,7 +828,7 @@ class SpectrumAnalyzerDemo
815828
// Create parameter labels with proper font sizing
816829
auto labelFont = font.withHeight (12.0f);
817830

818-
for (const auto& labelText : { "Signal Type:", "Frequency:", "Amplitude:", "Sweep Duration:", "FFT Size:", "Window:", "Display:", "View Mode:", "Color Map:", "Release:", "Overlap:", "Smoothing:" })
831+
for (const auto& labelText : { "Signal Type:", "Frequency:", "Amplitude:", "Sweep Duration:", "FFT Size:", "Window:", "Display:", "View Mode:", "Color Map:", "Release:", "Overlap:", "Smoothing:", "Level Mode:" })
819832
{
820833
auto label = parameterLabels.add (std::make_unique<yup::Label> (labelText));
821834
label->setText (labelText);
@@ -898,13 +911,17 @@ class SpectrumAnalyzerDemo
898911
auto row3 = bounds.removeFromTop (rowHeight);
899912
auto releaseSection = row3.removeFromLeft (colWidth);
900913
auto overlapSection = row3.removeFromLeft (colWidth);
914+
auto levelModeSection = row3.removeFromLeft (colWidth);
901915

902916
parameterLabels[9]->setBounds (releaseSection.removeFromTop (labelHeight));
903917
releaseSlider->setBounds (releaseSection.removeFromTop (controlHeight));
904918

905919
parameterLabels[10]->setBounds (overlapSection.removeFromTop (labelHeight));
906920
overlapSlider->setBounds (overlapSection.removeFromTop (controlHeight));
907921

922+
parameterLabels[12]->setBounds (levelModeSection.removeFromTop (labelHeight));
923+
levelModeCombo->setBounds (levelModeSection.removeFromTop (controlHeight));
924+
908925
// Fourth row: Status labels
909926
auto row4 = bounds.removeFromTop (30);
910927
auto freqStatus = row4.removeFromLeft (bounds.getWidth() / 3);
@@ -1038,6 +1055,29 @@ class SpectrumAnalyzerDemo
10381055
analyzerComponent.setDisplayType (displayType);
10391056
}
10401057

1058+
void updateLevelMode()
1059+
{
1060+
auto levelMode = yup::SpectrumAnalyzerComponent::LevelMode::peakDecibels;
1061+
1062+
switch (levelModeCombo->getSelectedId())
1063+
{
1064+
case 1:
1065+
levelMode = yup::SpectrumAnalyzerComponent::LevelMode::peakDecibels;
1066+
break;
1067+
case 2:
1068+
levelMode = yup::SpectrumAnalyzerComponent::LevelMode::rmsDecibels;
1069+
break;
1070+
case 3:
1071+
levelMode = yup::SpectrumAnalyzerComponent::LevelMode::powerDecibels;
1072+
break;
1073+
case 4:
1074+
levelMode = yup::SpectrumAnalyzerComponent::LevelMode::powerSpectralDensity;
1075+
break;
1076+
}
1077+
1078+
analyzerComponent.setLevelMode (levelMode);
1079+
}
1080+
10411081
void updateViewMode()
10421082
{
10431083
switch (viewModeCombo->getSelectedId())
@@ -1114,6 +1154,7 @@ class SpectrumAnalyzerDemo
11141154
std::unique_ptr<yup::ComboBox> fftSizeCombo;
11151155
std::unique_ptr<yup::ComboBox> windowTypeCombo;
11161156
std::unique_ptr<yup::ComboBox> displayTypeCombo;
1157+
std::unique_ptr<yup::ComboBox> levelModeCombo;
11171158
std::unique_ptr<yup::Slider> releaseSlider;
11181159
std::unique_ptr<yup::Slider> overlapSlider;
11191160
std::unique_ptr<yup::Slider> smoothingSlider;

modules/yup_audio_gui/displays/yup_SpectrumAnalyzerComponent.cpp

Lines changed: 187 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,14 @@ void SpectrumAnalyzerComponent::processFFT()
101101
// Perform FFT
102102
fftProcessor->performRealFFTForward (fftInputBuffer.data(), fftOutputBuffer.data());
103103

104-
// Pre-compute magnitudes with window gain compensation
104+
// Pre-compute raw FFT magnitudes. Calibration is applied when bins are mapped to display levels.
105105
const int numBins = fftSize / 2 + 1;
106106

107107
for (int binIndex = 0; binIndex < numBins; ++binIndex)
108108
{
109109
const float real = fftOutputBuffer[static_cast<size_t> (binIndex * 2)];
110110
const float imag = fftOutputBuffer[static_cast<size_t> (binIndex * 2 + 1)];
111-
const float magnitude = std::sqrt (real * real + imag * imag) * windowGain;
111+
const float magnitude = std::sqrt (real * real + imag * imag);
112112

113113
magnitudeBuffer[static_cast<size_t> (binIndex)] = magnitude;
114114
}
@@ -117,8 +117,6 @@ void SpectrumAnalyzerComponent::processFFT()
117117
void SpectrumAnalyzerComponent::updateDisplay (bool hasNewFFTData)
118118
{
119119
// Always apply consistent smoothing to prevent pulsating
120-
const int numBins = fftSize / 2 + 1;
121-
122120
// Process display bins
123121
for (int i = 0; i < scopeSize; ++i)
124122
{
@@ -162,36 +160,8 @@ void SpectrumAnalyzerComponent::updateDisplay (bool hasNewFFTData)
162160
const float endBin = (freqRangeEnd * float (fftSize)) / float (sampleRate);
163161
const float binSpan = endBin - startBin;
164162

165-
float magnitude = 0.0f;
166-
167-
if (binSpan <= 1.5f)
168-
{
169-
// Low frequencies: Use interpolation for smooth transitions
170-
const float exactBin = (centerFreq * float (fftSize)) / float (sampleRate);
171-
const int bin1 = jlimit (0, numBins - 1, static_cast<int> (exactBin));
172-
const int bin2 = jlimit (0, numBins - 1, bin1 + 1);
173-
const float fraction = exactBin - float (bin1);
174-
175-
const float mag1 = magnitudeBuffer[static_cast<size_t> (bin1)];
176-
const float mag2 = magnitudeBuffer[static_cast<size_t> (bin2)];
177-
178-
// Linear interpolation for smooth low-frequency response
179-
magnitude = mag1 + fraction * (mag2 - mag1);
180-
}
181-
else
182-
{
183-
// High frequencies: Aggregate multiple bins using peak-hold
184-
const int binStart = jlimit (0, numBins - 1, static_cast<int> (startBin));
185-
const int binEnd = jlimit (0, numBins - 1, static_cast<int> (endBin + 0.5f));
186-
187-
for (int binIndex = binStart; binIndex <= binEnd; ++binIndex)
188-
magnitude = jmax (magnitude, magnitudeBuffer[static_cast<size_t> (binIndex)]);
189-
}
190-
191-
// Convert to decibels with proper calibration
192-
const float magnitudeDb = magnitude > 0.0f
193-
? 20.0f * std::log10 (magnitude / float (fftSize))
194-
: minDecibels;
163+
const float exactBin = (centerFreq * float (fftSize)) / float (sampleRate);
164+
const float magnitudeDb = getDisplayDecibelsForBinRange (startBin, endBin, exactBin);
195165

196166
// Map to display range [0.0, 1.0]
197167
targetLevel = jmap (jlimit (minDecibels, maxDecibels, magnitudeDb), minDecibels, maxDecibels, 0.0f, 1.0f);
@@ -243,13 +213,175 @@ void SpectrumAnalyzerComponent::generateWindow()
243213
{
244214
WindowFunctions<float>::generate (currentWindowType, windowBuffer.data(), windowBuffer.size());
245215

246-
// Calculate window gain compensation
247216
float windowSum = 0.0f;
217+
float windowPowerSum = 0.0f;
248218
for (int i = 0; i < fftSize; ++i)
249-
windowSum += windowBuffer[static_cast<size_t> (i)];
219+
{
220+
const float windowValue = windowBuffer[static_cast<size_t> (i)];
221+
windowSum += windowValue;
222+
windowPowerSum += windowValue * windowValue;
223+
}
224+
225+
windowCoherentGain = windowSum > 0.0f ? windowSum / float (fftSize) : 1.0f;
226+
227+
equivalentNoiseBandwidthBins = (windowSum > 0.0f && windowPowerSum > 0.0f)
228+
? (float (fftSize) * windowPowerSum) / (windowSum * windowSum)
229+
: 1.0f;
230+
}
231+
232+
float SpectrumAnalyzerComponent::getBinPeakAmplitude (int binIndex) const noexcept
233+
{
234+
const int lastBin = fftSize / 2;
235+
236+
if (! isPositiveAndBelow (binIndex, lastBin + 1))
237+
return 0.0f;
238+
239+
const float coherentGain = windowCoherentGain > 0.0f ? windowCoherentGain : 1.0f;
240+
const float oneSidedScale = (binIndex > 0 && binIndex < lastBin) ? 2.0f : 1.0f;
241+
const float rawMagnitude = magnitudeBuffer[static_cast<size_t> (binIndex)];
242+
243+
return (oneSidedScale * rawMagnitude) / (coherentGain * float (fftSize));
244+
}
245+
246+
float SpectrumAnalyzerComponent::getBinRMSAmplitude (int binIndex) const noexcept
247+
{
248+
const int lastBin = fftSize / 2;
249+
const float peakAmplitude = getBinPeakAmplitude (binIndex);
250+
251+
if (binIndex <= 0 || binIndex >= lastBin)
252+
return peakAmplitude;
253+
254+
return peakAmplitude * 0.7071067811865475f;
255+
}
256+
257+
float SpectrumAnalyzerComponent::getBinPower (int binIndex) const noexcept
258+
{
259+
const float rmsAmplitude = getBinRMSAmplitude (binIndex);
260+
return rmsAmplitude * rmsAmplitude;
261+
}
262+
263+
float SpectrumAnalyzerComponent::getBinPowerSpectralDensity (int binIndex) const noexcept
264+
{
265+
const float enbwHz = getEquivalentNoiseBandwidthHz();
266+
return enbwHz > 0.0f ? getBinPower (binIndex) / enbwHz : 0.0f;
267+
}
268+
269+
float SpectrumAnalyzerComponent::getBinLinearLevel (int binIndex) const noexcept
270+
{
271+
switch (levelMode)
272+
{
273+
case LevelMode::peakDecibels:
274+
return getBinPeakAmplitude (binIndex);
275+
276+
case LevelMode::rmsDecibels:
277+
return getBinRMSAmplitude (binIndex);
278+
279+
case LevelMode::powerDecibels:
280+
return getBinPower (binIndex);
281+
282+
case LevelMode::powerSpectralDensity:
283+
return getBinPowerSpectralDensity (binIndex);
284+
}
285+
286+
return getBinPeakAmplitude (binIndex);
287+
}
288+
289+
float SpectrumAnalyzerComponent::linearLevelToDecibels (float level) const noexcept
290+
{
291+
if (level <= 0.0f)
292+
return minDecibels;
293+
294+
return (isPowerMode() ? 10.0f : 20.0f) * std::log10 (level);
295+
}
296+
297+
float SpectrumAnalyzerComponent::getInterpolatedPeakDecibels (float exactBin) const noexcept
298+
{
299+
const int numBins = fftSize / 2 + 1;
300+
const int lastBin = numBins - 1;
301+
const int nearestBin = jlimit (0, lastBin, roundToInt (exactBin));
302+
303+
int peakBin = nearestBin;
304+
float peakLevel = getBinLinearLevel (nearestBin);
305+
306+
const int searchStart = jmax (0, nearestBin - 1);
307+
const int searchEnd = jmin (lastBin, nearestBin + 1);
308+
309+
for (int binIndex = searchStart; binIndex <= searchEnd; ++binIndex)
310+
{
311+
const float binLevel = getBinLinearLevel (binIndex);
312+
313+
if (binLevel > peakLevel)
314+
{
315+
peakLevel = binLevel;
316+
peakBin = binIndex;
317+
}
318+
}
319+
320+
const float peakDecibels = linearLevelToDecibels (peakLevel);
321+
322+
if (peakBin <= 0 || peakBin >= lastBin)
323+
return peakDecibels;
324+
325+
const float y0 = linearLevelToDecibels (getBinLinearLevel (peakBin - 1));
326+
const float y1 = peakDecibels;
327+
const float y2 = linearLevelToDecibels (getBinLinearLevel (peakBin + 1));
328+
const float denominator = y0 - 2.0f * y1 + y2;
329+
330+
if (std::abs (denominator) < 1.0e-6f || denominator >= 0.0f)
331+
return y1;
250332

251-
// Gain compensation factor to restore energy after windowing
252-
windowGain = windowSum > 0.0f ? float (fftSize) / windowSum : 1.0f;
333+
const float offset = jlimit (-1.0f, 1.0f, 0.5f * (y0 - y2) / denominator);
334+
return y1 - 0.25f * (y0 - y2) * offset;
335+
}
336+
337+
float SpectrumAnalyzerComponent::getDisplayDecibelsForBinRange (float startBin, float endBin, float centerBin) const noexcept
338+
{
339+
const int numBins = fftSize / 2 + 1;
340+
const int lastBin = numBins - 1;
341+
const float binSpan = endBin - startBin;
342+
343+
if (binSpan <= 1.5f && ! isPowerMode())
344+
return getInterpolatedPeakDecibels (centerBin);
345+
346+
const int binStart = jlimit (0, lastBin, static_cast<int> (std::floor (startBin)));
347+
const int binEnd = jlimit (0, lastBin, static_cast<int> (std::ceil (endBin)));
348+
349+
if (levelMode == LevelMode::powerDecibels)
350+
{
351+
float bandPower = 0.0f;
352+
353+
for (int binIndex = binStart; binIndex <= binEnd; ++binIndex)
354+
bandPower += getBinPower (binIndex);
355+
356+
return linearLevelToDecibels (bandPower);
357+
}
358+
359+
if (levelMode == LevelMode::powerSpectralDensity)
360+
{
361+
float densitySum = 0.0f;
362+
int densityCount = 0;
363+
364+
for (int binIndex = binStart; binIndex <= binEnd; ++binIndex)
365+
{
366+
densitySum += getBinPowerSpectralDensity (binIndex);
367+
++densityCount;
368+
}
369+
370+
return linearLevelToDecibels (densityCount > 0 ? densitySum / float (densityCount) : 0.0f);
371+
}
372+
373+
float peakLevel = 0.0f;
374+
375+
for (int binIndex = binStart; binIndex <= binEnd; ++binIndex)
376+
peakLevel = jmax (peakLevel, getBinLinearLevel (binIndex));
377+
378+
return linearLevelToDecibels (peakLevel);
379+
}
380+
381+
bool SpectrumAnalyzerComponent::isPowerMode() const noexcept
382+
{
383+
return levelMode == LevelMode::powerDecibels
384+
|| levelMode == LevelMode::powerSpectralDensity;
253385
}
254386

255387
//==============================================================================
@@ -485,8 +617,10 @@ void SpectrumAnalyzerComponent::setWindowType (WindowType type)
485617
if (currentWindowType != type)
486618
{
487619
currentWindowType = type;
620+
generateWindow();
621+
needsWindowUpdate = false;
488622

489-
needsWindowUpdate = true;
623+
repaint();
490624
}
491625
}
492626

@@ -551,6 +685,20 @@ void SpectrumAnalyzerComponent::setDisplayType (DisplayType type)
551685
}
552686
}
553687

688+
void SpectrumAnalyzerComponent::setLevelMode (LevelMode mode)
689+
{
690+
if (levelMode != mode)
691+
{
692+
levelMode = mode;
693+
repaint();
694+
}
695+
}
696+
697+
float SpectrumAnalyzerComponent::getEquivalentNoiseBandwidthHz() const noexcept
698+
{
699+
return equivalentNoiseBandwidthBins * (float (sampleRate) / float (fftSize));
700+
}
701+
554702
//==============================================================================
555703
float SpectrumAnalyzerComponent::getFrequencyForBin (int binIndex) const noexcept
556704
{

0 commit comments

Comments
 (0)