@@ -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()
117117void 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// ==============================================================================
555703float SpectrumAnalyzerComponent::getFrequencyForBin (int binIndex) const noexcept
556704{
0 commit comments