48#include "../Core/DspMath.h"
49#include "../Core/FFT.h"
50#include "../Core/WindowFunctions.h"
105 assert(fftSize >= 256 && fftSize <= 16384 && (fftSize & (fftSize - 1)) == 0);
107 if (!std::isfinite(sampleRate) || sampleRate <= 0.0)
112 fftSize = std::clamp(fftSize, 256, 16384);
114 while (pow2 < fftSize) pow2 <<= 1;
119 sampleRate_ = sampleRate;
121 numBins_ = fftSize / 2 + 1;
122 hopSize_ = fftSize / 2;
123 windowType_ = windowType;
125 window_.resize(
static_cast<size_t>(fftSize));
126 generateWindow(windowType);
129 if (windowGain_ < T(0.001)) windowGain_ = T(1);
130 invGain_ = T(2) / (
static_cast<T
>(fftSize_) * windowGain_);
132 inputRing_.assign(
static_cast<size_t>(fftSize), T(0));
133 fftBuffer_.resize(
static_cast<size_t>(fftSize));
134 freqBuffer_.resize(
static_cast<size_t>(fftSize + 2));
137 const T floorDb = floorDb_.load(std::memory_order_relaxed);
138 magnitudesState_.assign(
static_cast<size_t>(numBins_), T(0));
139 peakState_.assign(
static_cast<size_t>(numBins_), floorDb);
142 for (
auto& slot : outSlots_)
144 slot.magnitudesDb.assign(
static_cast<size_t>(numBins_), floorDb);
145 slot.peakDb.assign(
static_cast<size_t>(numBins_), floorDb);
149 pendingSlot_.store(2, std::memory_order_relaxed);
152 ringMask_ = fftSize_ - 1;
153 samplesUntilFFT_ = hopSize_;
154 newDataReady_.store(
false, std::memory_order_relaxed);
156 fft_ = std::make_unique<FFTReal<T>>(
static_cast<size_t>(fftSize));
162 const T floorDb = floorDb_.load(std::memory_order_relaxed);
163 std::fill(inputRing_.begin(), inputRing_.end(), T(0));
164 std::fill(magnitudesState_.begin(), magnitudesState_.end(), T(0));
165 std::fill(peakState_.begin(), peakState_.end(), floorDb);
167 for (
auto& slot : outSlots_)
169 std::fill(slot.magnitudesDb.begin(), slot.magnitudesDb.end(), floorDb);
170 std::fill(slot.peakDb.begin(), slot.peakDb.end(), floorDb);
174 samplesUntilFFT_ = hopSize_;
175 newDataReady_.store(
false, std::memory_order_relaxed);
186 if (!std::isfinite(factor))
return;
187 smoothing_.store(std::clamp(factor, T(0), T(0.99)), std::memory_order_relaxed);
197 if (!std::isfinite(decayDbPerSecond))
return;
198 peakDecayRate_.store(std::max(T(0), decayDbPerSecond), std::memory_order_relaxed);
204 peakHoldEnabled_.store(enabled, std::memory_order_relaxed);
214 if (!std::isfinite(floorDb))
return;
215 floorDb_.store(floorDb, std::memory_order_relaxed);
219 [[nodiscard]] T
getSmoothing() const noexcept {
return smoothing_.load(std::memory_order_relaxed); }
222 [[nodiscard]] T
getPeakDecay() const noexcept {
return peakDecayRate_.load(std::memory_order_relaxed); }
225 [[nodiscard]]
bool isPeakHoldEnabled() const noexcept {
return peakHoldEnabled_.load(std::memory_order_relaxed); }
228 [[nodiscard]] T
getFloorDb() const noexcept {
return floorDb_.load(std::memory_order_relaxed); }
244 if (fft_ ==
nullptr || samples ==
nullptr || numSamples <= 0)
247 for (
int i = 0; i < numSamples; ++i)
249 inputRing_[
static_cast<size_t>(ringWritePos_)] = samples[i];
250 ringWritePos_ = (ringWritePos_ + 1) & ringMask_;
252 if (--samplesUntilFFT_ <= 0)
255 samplesUntilFFT_ = hopSize_;
267 return outSlots_[
static_cast<size_t>(readSlot_)].magnitudesDb.data();
277 return outSlots_[
static_cast<size_t>(readSlot_)].peakDb.data();
283 return newDataReady_.exchange(
false, std::memory_order_relaxed);
287 [[nodiscard]]
int getNumBins() const noexcept {
return numBins_; }
290 [[nodiscard]]
int getFFTSize() const noexcept {
return fftSize_; }
295 if (fftSize_ <= 0)
return T(0);
296 return static_cast<T
>(bin) *
static_cast<T
>(sampleRate_) /
static_cast<T
>(fftSize_);
317 void computeSpectrum() noexcept
320 const T smoothing = smoothing_.load(std::memory_order_relaxed);
321 const T floorDb = floorDb_.load(std::memory_order_relaxed);
322 const T decayRate = peakDecayRate_.load(std::memory_order_relaxed);
323 const bool peakHold = peakHoldEnabled_.load(std::memory_order_relaxed);
326 for (
int i = 0; i < fftSize_; ++i)
328 int ringIdx = (ringWritePos_ + i) & ringMask_;
329 fftBuffer_[
static_cast<size_t>(i)] = inputRing_[
static_cast<size_t>(ringIdx)] * window_[
static_cast<size_t>(i)];
333 fft_->forward(fftBuffer_.data(), freqBuffer_.data());
336 const T oneMinusSmooth = T(1) - smoothing;
338 for (
int k = 0; k < numBins_; ++k)
340 T re = freqBuffer_[
static_cast<size_t>(2 * k)];
341 T im = freqBuffer_[
static_cast<size_t>(2 * k + 1)];
342 T mag = std::sqrt(re * re + im * im) * invGain_;
348 if (k == 0 || k == numBins_ - 1) mag *= T(0.5);
350 magnitudesState_[
static_cast<size_t>(k)] =
351 smoothing * magnitudesState_[
static_cast<size_t>(k)] + oneMinusSmooth * mag;
355 const T peakDecayDb = decayRate *
static_cast<T
>(hopSize_) /
static_cast<T
>(sampleRate_);
358 auto& slot = outSlots_[
static_cast<size_t>(writeSlot_)];
361 for (
int k = 0; k < numBins_; ++k)
363 T dB =
gainToDecibels(magnitudesState_[
static_cast<size_t>(k)], floorDb);
364 slot.magnitudesDb[
static_cast<size_t>(k)] = dB;
372 T& peak = peakState_[
static_cast<size_t>(k)];
373 peak = std::max(dB, std::max(floorDb, peak - peakDecayDb));
374 slot.peakDb[
static_cast<size_t>(k)] = peak;
381 const int old = pendingSlot_.exchange(writeSlot_ | kFreshBit, std::memory_order_acq_rel);
382 writeSlot_ = old & kSlotMask;
383 newDataReady_.store(
true, std::memory_order_release);
386 double sampleRate_ = 48000.0;
391 std::unique_ptr<FFTReal<T>> fft_;
392 std::vector<T> window_;
394 T windowGain_ = T(1);
397 std::vector<T> inputRing_;
398 int ringWritePos_ = 0;
399 int samplesUntilFFT_ = 0;
401 std::vector<T> fftBuffer_;
402 std::vector<T> freqBuffer_;
405 std::vector<T> magnitudesState_;
406 std::vector<T> peakState_;
411 static constexpr int kFreshBit = 4;
412 static constexpr int kSlotMask = 3;
416 std::vector<T> magnitudesDb;
417 std::vector<T> peakDb;
419 std::array<OutSlot, 3> outSlots_;
421 mutable int readSlot_ = 1;
422 mutable std::atomic<int> pendingSlot_{ 2 };
425 void acquireLatestSlot() const noexcept
427 int expected = pendingSlot_.load(std::memory_order_acquire);
428 while (expected & kFreshBit)
430 if (pendingSlot_.compare_exchange_weak(expected, readSlot_,
431 std::memory_order_acq_rel,
432 std::memory_order_acquire))
434 readSlot_ = expected & kSlotMask;
440 int ringMask_ = 2047;
442 std::atomic<T> smoothing_ { T(0.8) };
443 std::atomic<T> peakDecayRate_ { T(10) };
444 std::atomic<T> floorDb_ { T(-100) };
445 std::atomic<bool> peakHoldEnabled_ {
false };
447 std::atomic<bool> newDataReady_{
false };
Real-time FFT spectrum analyser with per-bin smoothing and peak hold.
void setFloorDb(T floorDb) noexcept
Sets the readout floor in decibels.
T getFloorDb() const noexcept
Returns the readout floor in decibels.
void prepare(double sampleRate, int fftSize=2048, WindowType windowType=WindowType::Hann)
Prepares the analyser and allocates all necessary buffers.
int getFFTSize() const noexcept
FFT size in samples; 0 before prepare().
void setPeakHoldEnabled(bool enabled) noexcept
Enables or disables peak-hold tracking.
WindowType getWindowType() const noexcept
Returns the window type in use (set at prepare time).
int getNumBins() const noexcept
Number of spectrum bins (fftSize/2 + 1); 0 before prepare().
T getPeakDecay() const noexcept
Returns the peak-hold decay rate in dB per second.
T getSmoothing() const noexcept
Returns the per-frame magnitude smoothing factor.
void pushSamples(const T *samples, int numSamples) noexcept
Pushes audio samples into the analyser's internal ring buffer.
bool isPeakHoldEnabled() const noexcept
Returns true if peak-hold tracking is enabled.
void setPeakDecay(T decayDbPerSecond) noexcept
Sets the peak-hold decay rate.
const T * getPeakHoldDb() const noexcept
Returns the peak-hold spectrum in decibels.
void reset() noexcept
Resets all internal buffers and state to zero/floor values.
void setSmoothing(T factor) noexcept
Sets the per-frame magnitude smoothing factor.
const T * getMagnitudesDb() const noexcept
Returns the current magnitude spectrum in decibels.
T binToFrequency(int bin) const noexcept
Centre frequency of the given bin in Hz (0 before prepare()).
WindowType
Available window types for the FFT analysis.
@ Hann
Default. Good general-purpose choice.
@ Hamming
Slightly better side lobe rejection.
@ Rectangular
No windowing (transient analysis).
@ BlackmanHarris
Highest side lobe rejection.
@ Blackman
High dynamic range.
@ FlatTop
Amplitude-accurate measurement.
bool isNewDataReady() noexcept
Consumes and returns the new data flag. True if updated since last call.
Main namespace for the DSPark framework.
T gainToDecibels(T gain, T minusInfinityDb=T(-100)) noexcept
Converts a linear gain value to decibels.
static void hann(T *output, int size, bool periodic=true) noexcept
Hann (raised cosine) window.
static void blackman(T *output, int size, bool periodic=true) noexcept
Blackman window.
static void blackmanHarris(T *output, int size, bool periodic=true) noexcept
Blackman-Harris window (4-term, -92 dB side lobes).
static void hamming(T *output, int size, bool periodic=true) noexcept
Hamming window.
static T coherentGain(const T *window, int size) noexcept
Computes the coherent gain of a window (mean of its samples).
static void flatTop(T *output, int size, bool periodic=true) noexcept
Flat-top window (amplitude-accurate: scallop loss < 0.01 dB).
static void rectangular(T *output, int size) noexcept
Rectangular window (no windowing – all ones).