Skip to content

Commit 8905969

Browse files
committed
More work
1 parent 316287a commit 8905969

File tree

5 files changed

+105
-177
lines changed

5 files changed

+105
-177
lines changed

examples/graphics/source/examples/SpectrumAnalyzer.h

Lines changed: 17 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ class SpectrumAnalyzerDemo
285285
// Update FFT info display
286286
if (fftInfoLabel)
287287
{
288-
yup::String fftText = "FFT: " + yup::String (currentFFTSize) + ", Overlap: " + yup::String (currentOverlapPercent) + "%";
288+
yup::String fftText = "FFT: " + yup::String (currentFFTSize);
289289
fftInfoLabel->setText (fftText, yup::dontSendNotification);
290290
}
291291
}
@@ -302,10 +302,7 @@ class SpectrumAnalyzerDemo
302302
for (int sample = 0; sample < numSamples; ++sample)
303303
{
304304
// Generate audio sample using signal generator
305-
float audioSample = signalGenerator.getNextSample();
306-
307-
// Scale final output
308-
audioSample *= masterVolume;
305+
const float audioSample = signalGenerator.getNextSample();
309306

310307
// Output to all channels
311308
for (int channel = 0; channel < numOutputChannels; ++channel)
@@ -316,7 +313,7 @@ class SpectrumAnalyzerDemo
316313
}
317314
}
318315

319-
void audioDeviceAboutToStart (yup::AudioIODevice* device) override
316+
void audioDeviceAboutToStart (yup::AudioIODevice* device) override
320317
{
321318
double sampleRate = device->getCurrentSampleRate();
322319

@@ -349,7 +346,7 @@ class SpectrumAnalyzerDemo
349346
// Signal type selector
350347
signalTypeCombo = std::make_unique<yup::ComboBox> ("SignalType");
351348
signalTypeCombo->addItem ("Single Tone", 1);
352-
signalTypeCombo->addItem ("Frequency Sweep", 2);
349+
signalTypeCombo->addItem ("Sweep", 2);
353350
signalTypeCombo->addItem ("White Noise", 3);
354351
signalTypeCombo->addItem ("Pink Noise", 4);
355352
signalTypeCombo->addItem ("Brown Noise", 5);
@@ -398,43 +395,28 @@ class SpectrumAnalyzerDemo
398395
fftSizeCombo = std::make_unique<yup::ComboBox> ("FFTSize");
399396
int fftSizeId = 1;
400397
for (int size = 32; size <= 16384; size *= 2)
401-
{
402398
fftSizeCombo->addItem (yup::String (size), fftSizeId++);
403-
}
404-
fftSizeCombo->setSelectedId (9); // 2048 (default)
399+
fftSizeCombo->setSelectedId (8);
405400
fftSizeCombo->onSelectedItemChanged = [this]
406401
{
407402
updateFFTSize();
408403
};
409404
addAndMakeVisible (*fftSizeCombo);
410405

411-
// Overlap selector
412-
overlapCombo = std::make_unique<yup::ComboBox> ("Overlap");
413-
overlapCombo->addItem ("0%", 1);
414-
overlapCombo->addItem ("25%", 2);
415-
overlapCombo->addItem ("50%", 3);
416-
overlapCombo->addItem ("75%", 4);
417-
overlapCombo->setSelectedId (4); // 75% default
418-
overlapCombo->onSelectedItemChanged = [this]
419-
{
420-
updateOverlap();
421-
};
422-
addAndMakeVisible (*overlapCombo);
423-
424406
// Window type selector
425407
windowTypeCombo = std::make_unique<yup::ComboBox> ("WindowType");
426408
windowTypeCombo->addItem ("Rectangular", 1);
427409
windowTypeCombo->addItem ("Hann", 2);
428410
windowTypeCombo->addItem ("Hamming", 3);
429411
windowTypeCombo->addItem ("Blackman", 4);
430-
windowTypeCombo->addItem ("Blackman-Harris", 5);
412+
windowTypeCombo->addItem ("B-Harris", 5);
431413
windowTypeCombo->addItem ("Kaiser", 6);
432414
windowTypeCombo->addItem ("Gaussian", 7);
433415
windowTypeCombo->addItem ("Tukey", 8);
434416
windowTypeCombo->addItem ("Bartlett", 9);
435417
windowTypeCombo->addItem ("Welch", 10);
436418
windowTypeCombo->addItem ("Flat-top", 11);
437-
windowTypeCombo->setSelectedId (2); // Hann
419+
windowTypeCombo->setSelectedId (4);
438420
windowTypeCombo->onSelectedItemChanged = [this]
439421
{
440422
updateWindowType();
@@ -478,7 +460,7 @@ class SpectrumAnalyzerDemo
478460
addAndMakeVisible (*amplitudeLabel);
479461

480462
fftInfoLabel = std::make_unique<yup::Label> ("FFTInfoLabel");
481-
fftInfoLabel->setText ("FFT: 2048, Overlap: 75%");
463+
fftInfoLabel->setText ("FFT: 2048");
482464
fftInfoLabel->setColor (yup::Label::Style::textFillColorId, yup::Colors::lightgray);
483465
fftInfoLabel->setFont (statusFont);
484466
addAndMakeVisible (*fftInfoLabel);
@@ -494,23 +476,15 @@ class SpectrumAnalyzerDemo
494476
// Create parameter labels with proper font sizing
495477
auto labelFont = font.withHeight (12.0f);
496478

497-
for (const auto& labelText : { "Signal Type:", "Frequency:", "Amplitude:", "Sweep Duration:",
498-
"FFT Size:", "Overlap:", "Window:", "Display:", "Release:" })
479+
for (const auto& labelText : { "Signal Type:", "Frequency:", "Amplitude:", "Sweep Duration:",
480+
"FFT Size:", "Window:", "Display:", "Release:" })
499481
{
500482
auto label = parameterLabels.add (std::make_unique<yup::Label> (labelText));
501483
label->setText (labelText);
502484
label->setColor (yup::Label::Style::textFillColorId, yup::Colors::lightgray);
503485
label->setFont (labelFont);
504486
addAndMakeVisible (*label);
505487
}
506-
507-
// Initialize parameters
508-
currentFrequency = 440.0;
509-
currentAmplitude = 0.5f;
510-
sweepDurationSeconds = 10.0;
511-
masterVolume = 0.3f;
512-
currentFFTSize = 2048;
513-
currentOverlapPercent = 75;
514488
}
515489

516490
void setupAudio()
@@ -547,26 +521,23 @@ class SpectrumAnalyzerDemo
547521
parameterLabels[3]->setBounds (sweepSection.removeFromTop (labelHeight));
548522
sweepDurationSlider->setBounds (sweepSection.removeFromTop (controlHeight));
549523

550-
parameterLabels[8]->setBounds (smoothingSection.removeFromTop (labelHeight));
524+
parameterLabels[7]->setBounds (smoothingSection.removeFromTop (labelHeight));
551525
releaseSlider->setBounds (smoothingSection.removeFromTop (controlHeight));
552526

553527
// Second row: FFT controls
554528
auto row2 = bounds.removeFromTop (rowHeight);
555529
auto fftSizeSection = row2.removeFromLeft (colWidth);
556-
auto overlapSection = row2.removeFromLeft (colWidth);
557530
auto windowSection = row2.removeFromLeft (colWidth);
558531
auto displaySection = row2.removeFromLeft (colWidth);
532+
row2.removeFromLeft (colWidth); // Skip unused column
559533

560534
parameterLabels[4]->setBounds (fftSizeSection.removeFromTop (labelHeight));
561535
fftSizeCombo->setBounds (fftSizeSection.removeFromTop (controlHeight));
562536

563-
parameterLabels[5]->setBounds (overlapSection.removeFromTop (labelHeight));
564-
overlapCombo->setBounds (overlapSection.removeFromTop (controlHeight));
565-
566-
parameterLabels[6]->setBounds (windowSection.removeFromTop (labelHeight));
537+
parameterLabels[5]->setBounds (windowSection.removeFromTop (labelHeight));
567538
windowTypeCombo->setBounds (windowSection.removeFromTop (controlHeight));
568539

569-
parameterLabels[7]->setBounds (displaySection.removeFromTop (labelHeight));
540+
parameterLabels[6]->setBounds (displaySection.removeFromTop (labelHeight));
570541
displayTypeCombo->setBounds (displaySection.removeFromTop (controlHeight));
571542

572543
// Third row: Status labels
@@ -607,23 +578,8 @@ class SpectrumAnalyzerDemo
607578
int selectedId = fftSizeCombo->getSelectedId();
608579
currentFFTSize = 32 << (selectedId - 1); // 32, 64, 128, 256, ..., 16384
609580

610-
// Update the analyzer component with the new FFT size
611-
analyzerComponent.setFFTSize (currentFFTSize);
612-
}
613-
614-
void updateOverlap()
615-
{
616-
switch (overlapCombo->getSelectedId())
617-
{
618-
case 1: currentOverlapPercent = 0; break;
619-
case 2: currentOverlapPercent = 25; break;
620-
case 3: currentOverlapPercent = 50; break;
621-
case 4: currentOverlapPercent = 75; break;
622-
}
623-
624-
// Update the analyzer component with the new overlap factor
625-
float overlapFactor = float (currentOverlapPercent) / 100.0f;
626-
analyzerComponent.setOverlapFactor (overlapFactor);
581+
// Update the analyzer state with the new FFT size
582+
analyzerState.setFftSize (currentFFTSize);
627583
}
628584

629585
void updateWindowType()
@@ -680,7 +636,6 @@ class SpectrumAnalyzerDemo
680636

681637
// FFT controls
682638
std::unique_ptr<yup::ComboBox> fftSizeCombo;
683-
std::unique_ptr<yup::ComboBox> overlapCombo;
684639
std::unique_ptr<yup::ComboBox> windowTypeCombo;
685640
std::unique_ptr<yup::ComboBox> displayTypeCombo;
686641
std::unique_ptr<yup::Slider> releaseSlider;
@@ -696,7 +651,5 @@ class SpectrumAnalyzerDemo
696651
double currentFrequency = 440.0;
697652
float currentAmplitude = 0.5f;
698653
double sweepDurationSeconds = 10.0;
699-
float masterVolume = 0.3f;
700-
int currentFFTSize = 2048;
701-
int currentOverlapPercent = 75;
654+
int currentFFTSize = 4096;
702655
};

modules/yup_audio_gui/displays/yup_SpectrumAnalyzerComponent.cpp

Lines changed: 31 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,14 @@ SpectrumAnalyzerComponent::~SpectrumAnalyzerComponent()
4343
//==============================================================================
4444
void SpectrumAnalyzerComponent::initializeFFTBuffers()
4545
{
46-
hopSize = fftSize / 2; // 50% overlap
47-
4846
fftProcessor = std::make_unique<FFTProcessor> (fftSize);
4947
fftInputBuffer.resize (fftSize, 0.0f);
5048
fftOutputBuffer.resize (fftSize * 2, 0.0f); // Complex output needs 2x space
5149
windowBuffer.resize (fftSize, 0.0f);
52-
overlapBuffer.resize (fftSize, 0.0f);
53-
50+
5451
// Pre-allocate magnitude buffer to avoid allocations during processing
5552
const int numBins = fftSize / 2 + 1;
5653
magnitudeBuffer.resize (numBins, 0.0f);
57-
accumulatedSpectrum.resize (numBins, 0.0f);
58-
59-
overlapBufferPos = 0;
60-
spectrumAccumCount = 0;
6154
}
6255

6356
//==============================================================================
@@ -66,7 +59,7 @@ void SpectrumAnalyzerComponent::timerCallback()
6659
bool hasNewData = false;
6760

6861
// Process FFT frames with proper overlap
69-
while (analyzerState.getNumAvailableSamples() >= hopSize && analyzerState.isFFTDataReady())
62+
while (analyzerState.getNumAvailableSamples() >= fftSize && analyzerState.isFFTDataReady())
7063
{
7164
processFFT();
7265
hasNewData = true;
@@ -97,50 +90,23 @@ void SpectrumAnalyzerComponent::processFFT()
9790
// Perform FFT
9891
fftProcessor->performRealFFTForward (fftInputBuffer.data(), fftOutputBuffer.data());
9992

100-
// Pre-compute magnitudes and accumulate spectrum
93+
// Pre-compute magnitudes with window gain compensation
10194
const int numBins = fftSize / 2 + 1;
10295

103-
// If this is the first spectrum or we want to reset accumulation
104-
if (spectrumAccumCount == 0)
96+
for (int binIndex = 0; binIndex < numBins; ++binIndex)
10597
{
106-
// Start fresh accumulation
107-
for (int binIndex = 0; binIndex < numBins; ++binIndex)
108-
{
109-
const float real = fftOutputBuffer[static_cast<size_t> (binIndex * 2)];
110-
const float imag = fftOutputBuffer[static_cast<size_t> (binIndex * 2 + 1)];
111-
const float magnitude = std::sqrt (real * real + imag * imag);
112-
accumulatedSpectrum[static_cast<size_t> (binIndex)] = magnitude;
113-
magnitudeBuffer[static_cast<size_t> (binIndex)] = magnitude;
114-
}
115-
spectrumAccumCount = 1;
116-
}
117-
else
118-
{
119-
// Accumulate with existing spectrum using exponential moving average
120-
const float overlapFactor = getOverlapFactor();
121-
const float alpha = overlapFactor > 0.0f ? 0.7f : 0.3f; // More averaging with higher overlap
122-
123-
for (int binIndex = 0; binIndex < numBins; ++binIndex)
124-
{
125-
const float real = fftOutputBuffer[static_cast<size_t> (binIndex * 2)];
126-
const float imag = fftOutputBuffer[static_cast<size_t> (binIndex * 2 + 1)];
127-
const float magnitude = std::sqrt (real * real + imag * imag);
128-
129-
// Update exponential moving average - this reduces pulsation
130-
accumulatedSpectrum[static_cast<size_t> (binIndex)] =
131-
alpha * accumulatedSpectrum[static_cast<size_t> (binIndex)] + (1.0f - alpha) * magnitude;
132-
magnitudeBuffer[static_cast<size_t> (binIndex)] = accumulatedSpectrum[static_cast<size_t> (binIndex)];
133-
}
134-
spectrumAccumCount = jmin (spectrumAccumCount + 1, 20); // Limit accumulation count
98+
const float real = fftOutputBuffer[static_cast<size_t> (binIndex * 2)];
99+
const float imag = fftOutputBuffer[static_cast<size_t> (binIndex * 2 + 1)];
100+
const float magnitude = std::sqrt (real * real + imag * imag) * windowGain;
101+
102+
magnitudeBuffer[static_cast<size_t> (binIndex)] = magnitude;
135103
}
136104
}
137105

138106
void SpectrumAnalyzerComponent::updateDisplay(bool hasNewFFTData)
139107
{
140108
// Always apply consistent smoothing to prevent pulsating
141109
const int numBins = fftSize / 2 + 1;
142-
const float logMin = std::log10 (minFrequency);
143-
const float logMax = std::log10 (maxFrequency);
144110

145111
// Process display bins
146112
for (int i = 0; i < scopeSize; ++i)
@@ -151,29 +117,29 @@ void SpectrumAnalyzerComponent::updateDisplay(bool hasNewFFTData)
151117
{
152118
// Calculate frequency range for this display bin
153119
const float proportion = float (i) / float (scopeSize - 1);
154-
const float logFreq = logMin + proportion * (logMax - logMin);
120+
const float logFreq = logMinFrequency + proportion * (logMaxFrequency - logMinFrequency);
155121
const float centerFreq = std::pow (10.0f, logFreq);
156122

157123
// Calculate the frequency range that this display bin represents
158124
float freqRangeStart, freqRangeEnd;
159125
if (i == 0)
160126
{
161127
freqRangeStart = minFrequency;
162-
const float nextLogFreq = logMin + (float (i + 1) / float (scopeSize - 1)) * (logMax - logMin);
128+
const float nextLogFreq = logMinFrequency + (float (i + 1) / float (scopeSize - 1)) * (logMaxFrequency - logMinFrequency);
163129
const float nextFreq = std::pow (10.0f, nextLogFreq);
164130
freqRangeEnd = (centerFreq + nextFreq) * 0.5f;
165131
}
166132
else if (i == scopeSize - 1)
167133
{
168-
const float prevLogFreq = logMin + (float (i - 1) / float (scopeSize - 1)) * (logMax - logMin);
134+
const float prevLogFreq = logMinFrequency + (float (i - 1) / float (scopeSize - 1)) * (logMaxFrequency - logMinFrequency);
169135
const float prevFreq = std::pow (10.0f, prevLogFreq);
170136
freqRangeStart = (prevFreq + centerFreq) * 0.5f;
171137
freqRangeEnd = maxFrequency;
172138
}
173139
else
174140
{
175-
const float prevLogFreq = logMin + (float (i - 1) / float (scopeSize - 1)) * (logMax - logMin);
176-
const float nextLogFreq = logMin + (float (i + 1) / float (scopeSize - 1)) * (logMax - logMin);
141+
const float prevLogFreq = logMinFrequency + (float (i - 1) / float (scopeSize - 1)) * (logMaxFrequency - logMinFrequency);
142+
const float nextLogFreq = logMinFrequency + (float (i + 1) / float (scopeSize - 1)) * (logMaxFrequency - logMinFrequency);
177143
const float prevFreq = std::pow (10.0f, prevLogFreq);
178144
const float nextFreq = std::pow (10.0f, nextLogFreq);
179145
freqRangeStart = (prevFreq + centerFreq) * 0.5f;
@@ -208,14 +174,15 @@ void SpectrumAnalyzerComponent::updateDisplay(bool hasNewFFTData)
208174
const int binEnd = jlimit (0, numBins - 1, static_cast<int> (endBin + 0.5f));
209175

210176
for (int binIndex = binStart; binIndex <= binEnd; ++binIndex)
211-
{
212177
magnitude = jmax (magnitude, magnitudeBuffer[static_cast<size_t> (binIndex)]);
213-
}
214178
}
215179

216-
// Convert to decibels with proper normalization
180+
// Convert to decibels with proper calibration
181+
// Account for window function coherent gain losses
182+
const float windowCompensation = WindowFunctions<float>::getCompensationGain (currentWindowType);
183+
217184
const float magnitudeDb = magnitude > 0.0f
218-
? 20.0f * std::log10 (magnitude / float (fftSize))
185+
? 20.0f * std::log10 ((magnitude * windowCompensation) / float (fftSize))
219186
: minDecibels;
220187

221188
// Map to display range [0.0, 1.0]
@@ -227,12 +194,12 @@ void SpectrumAnalyzerComponent::updateDisplay(bool hasNewFFTData)
227194

228195
if (hasNewFFTData && targetLevel > currentValue)
229196
{
230-
// INSTANT ATTACK: Immediately use new peak values for zero latency
197+
// Immediately use new peak values for zero latency
231198
currentValue = targetLevel;
232199
}
233200
else
234201
{
235-
// TIME-BASED RELEASE: Calculate release rate based on time
202+
// Calculate release rate based on time
236203
if (releaseTimeSeconds <= 0.0f)
237204
{
238205
// Immediate falloff - use target directly or fast decay
@@ -267,6 +234,14 @@ void SpectrumAnalyzerComponent::updateDisplay(bool hasNewFFTData)
267234
void SpectrumAnalyzerComponent::generateWindow()
268235
{
269236
WindowFunctions<float>::generate (currentWindowType, windowBuffer.data(), windowBuffer.size());
237+
238+
// Calculate window gain compensation
239+
float windowSum = 0.0f;
240+
for (int i = 0; i < fftSize; ++i)
241+
windowSum += windowBuffer[static_cast<size_t> (i)];
242+
243+
// Gain compensation factor to restore energy after windowing
244+
windowGain = windowSum > 0.0f ? float (fftSize) / windowSum : 1.0f;
270245
}
271246

272247
//==============================================================================
@@ -589,25 +564,12 @@ void SpectrumAnalyzerComponent::setFFTSize (int size)
589564
if (fftSize != size)
590565
{
591566
fftSize = size;
567+
592568
analyzerState.setFftSize (size); // Update the state as well
569+
593570
initializeFFTBuffers();
594571
generateWindow();
595-
spectrumAccumCount = 0; // Reset accumulation
596-
repaint();
597-
}
598-
}
599572

600-
void SpectrumAnalyzerComponent::setOverlapFactor (float factor)
601-
{
602-
jassert (factor >= 0.0f && factor < 1.0f);
603-
604-
const int newHopSize = roundToInt (float (fftSize) * (1.0f - factor));
605-
606-
if (hopSize != newHopSize)
607-
{
608-
hopSize = jmax (1, newHopSize);
609-
initializeFFTBuffers();
610-
spectrumAccumCount = 0; // Reset accumulation
611573
repaint();
612574
}
613575
}

0 commit comments

Comments
 (0)