168 assert(fftSize <= 0 ||
169 (fftSize >= 256 && fftSize <= 16384 && (fftSize & (fftSize - 1)) == 0));
171 if (!std::isfinite(sampleRate) || sampleRate <= 0.0)
180 const double target = sampleRate * (kAutoSpanRef / kAutoSpanRate);
182 while (n < kAutoMaxFft &&
static_cast<double>(n) < target) n <<= 1;
188 fftSize = std::clamp(fftSize, 256, 16384);
190 while (pow2 < fftSize) pow2 <<= 1;
196 const int numBins = fftSize / 2 + 1;
197 const T floorDb = floorDb_.load(std::memory_order_relaxed);
204 std::vector<T> window(
static_cast<size_t>(fftSize));
205 const WindowType effectiveWindow = generateWindow(window.data(), fftSize, windowType);
208 if (windowGain < T(0.001)) windowGain = T(1);
209 const T invGain = T(2) / (
static_cast<T
>(fftSize) * windowGain);
211 std::vector<T> inputRing(
static_cast<size_t>(fftSize), T(0));
212 std::vector<T> fftBuffer(
static_cast<size_t>(fftSize));
213 std::vector<T> freqBuffer(
static_cast<size_t>(fftSize + 2));
216 std::vector<T> magnitudesState(
static_cast<size_t>(numBins), T(0));
217 std::vector<T> peakState(
static_cast<size_t>(numBins), floorDb);
222 std::array<OutSlot, 3> slots;
223 for (
auto& slot : slots)
225 slot.magnitudesDb = std::make_unique<std::atomic<T>[]>(
static_cast<size_t>(numBins));
226 slot.peakDb = std::make_unique<std::atomic<T>[]>(
static_cast<size_t>(numBins));
227 slot.seq = std::make_unique<std::atomic<unsigned>>(0u);
228 for (
int k = 0; k < numBins; ++k)
230 slot.magnitudesDb[
static_cast<size_t>(k)].store(floorDb, std::memory_order_relaxed);
231 slot.peakDb[
static_cast<size_t>(k)].store(floorDb, std::memory_order_relaxed);
238 std::vector<T> magSnapshot(
static_cast<size_t>(2 * numBins), floorDb);
239 std::vector<T> peakSnapshot(
static_cast<size_t>(2 * numBins), floorDb);
241 auto fft = std::make_unique<FFTReal<T>>(
static_cast<size_t>(fftSize));
250 static_assert(std::is_nothrow_move_assignable_v<std::vector<T>> &&
251 std::is_nothrow_move_assignable_v<std::array<OutSlot, 3>> &&
252 std::is_nothrow_move_assignable_v<std::unique_ptr<FFTReal<T>>>,
253 "prepare()'s commit phase must be non-throwing");
255 sampleRate_ = sampleRate;
258 hopSize_ = fftSize / 2;
259 windowType_ = effectiveWindow;
260 window_ = std::move(window);
261 windowGain_ = windowGain;
264 inputRing_ = std::move(inputRing);
265 fftBuffer_ = std::move(fftBuffer);
266 freqBuffer_ = std::move(freqBuffer);
267 magnitudesState_ = std::move(magnitudesState);
268 peakState_ = std::move(peakState);
270 outSlots_ = std::move(slots);
271 magSnapshot_ = std::move(magSnapshot);
272 peakSnapshot_ = std::move(peakSnapshot);
273 magSnapshotHalf_ = 0;
274 peakSnapshotHalf_ = 0;
278 pendingSlot_.store(2, std::memory_order_relaxed);
281 ringMask_ = fftSize_ - 1;
282 samplesUntilFFT_ = hopSize_;
283 newDataReady_.store(
false, std::memory_order_relaxed);
285 fft_ = std::move(fft);
297 const T floorDb = floorDb_.load(std::memory_order_relaxed);
298 std::fill(inputRing_.begin(), inputRing_.end(), T(0));
299 std::fill(magnitudesState_.begin(), magnitudesState_.end(), T(0));
300 std::fill(peakState_.begin(), peakState_.end(), floorDb);
302 for (
auto& slot : outSlots_)
307 for (
int k = 0; k < numBins_; ++k)
309 slot.magnitudesDb[
static_cast<size_t>(k)].store(floorDb, std::memory_order_relaxed);
310 slot.peakDb[
static_cast<size_t>(k)].store(floorDb, std::memory_order_relaxed);
313 std::fill(magSnapshot_.begin(), magSnapshot_.end(), floorDb);
314 std::fill(peakSnapshot_.begin(), peakSnapshot_.end(), floorDb);
317 samplesUntilFFT_ = hopSize_;
318 newDataReady_.store(
false, std::memory_order_relaxed);
329 if (!std::isfinite(factor))
return;
330 smoothing_.store(std::clamp(factor, T(0), T(0.99)), std::memory_order_relaxed);
340 if (!std::isfinite(decayDbPerSecond))
return;
341 peakDecayRate_.store(std::max(T(0), decayDbPerSecond), std::memory_order_relaxed);
347 peakHoldEnabled_.store(enabled, std::memory_order_relaxed);
357 if (!std::isfinite(floorDb))
return;
358 floorDb_.store(floorDb, std::memory_order_relaxed);
362 [[nodiscard]] T
getSmoothing() const noexcept {
return smoothing_.load(std::memory_order_relaxed); }
365 [[nodiscard]] T
getPeakDecay() const noexcept {
return peakDecayRate_.load(std::memory_order_relaxed); }
368 [[nodiscard]]
bool isPeakHoldEnabled() const noexcept {
return peakHoldEnabled_.load(std::memory_order_relaxed); }
371 [[nodiscard]] T
getFloorDb() const noexcept {
return floorDb_.load(std::memory_order_relaxed); }
387 if (fft_ ==
nullptr || samples ==
nullptr || numSamples <= 0)
390 for (
int i = 0; i < numSamples; ++i)
392 inputRing_[
static_cast<size_t>(ringWritePos_)] = samples[i];
393 ringWritePos_ = (ringWritePos_ + 1) & ringMask_;
395 if (--samplesUntilFFT_ <= 0)
398 samplesUntilFFT_ = hopSize_;
426 return snapshotFrame(
false);
441 return snapshotFrame(
true);
460 return newDataReady_.exchange(
false, std::memory_order_relaxed);
466 [[nodiscard]]
int getNumBins() const noexcept {
return numBins_; }
469 [[nodiscard]]
int getFFTSize() const noexcept {
return fftSize_; }
475 if (fftSize_ <= 0)
return T(0);
476 return static_cast<T
>(bin) *
static_cast<T
>(sampleRate_) /
static_cast<T
>(fftSize_);
501 void computeSpectrum() noexcept
504 const T smoothing = smoothing_.load(std::memory_order_relaxed);
505 const T floorDb = floorDb_.load(std::memory_order_relaxed);
506 const T decayRate = peakDecayRate_.load(std::memory_order_relaxed);
507 const bool peakHold = peakHoldEnabled_.load(std::memory_order_relaxed);
510 for (
int i = 0; i < fftSize_; ++i)
512 int ringIdx = (ringWritePos_ + i) & ringMask_;
513 fftBuffer_[
static_cast<size_t>(i)] = inputRing_[
static_cast<size_t>(ringIdx)] * window_[
static_cast<size_t>(i)];
517 fft_->forward(fftBuffer_.data(), freqBuffer_.data());
520 const T oneMinusSmooth = T(1) - smoothing;
522 for (
int k = 0; k < numBins_; ++k)
524 T re = freqBuffer_[
static_cast<size_t>(2 * k)];
525 T im = freqBuffer_[
static_cast<size_t>(2 * k + 1)];
526 T mag = std::sqrt(re * re + im * im) * invGain_;
532 if (k == 0 || k == numBins_ - 1) mag *= T(0.5);
534 magnitudesState_[
static_cast<size_t>(k)] =
535 smoothing * magnitudesState_[
static_cast<size_t>(k)] + oneMinusSmooth * mag;
539 const T peakDecayDb = decayRate *
static_cast<T
>(hopSize_) /
static_cast<T
>(sampleRate_);
542 auto& slot = outSlots_[
static_cast<size_t>(writeSlot_)];
543 std::atomic<unsigned>& seq = *slot.seq;
554 seq.fetch_add(1u, std::memory_order_acq_rel);
555 std::atomic_thread_fence(std::memory_order_release);
559 for (
int k = 0; k < numBins_; ++k)
561 T dB =
gainToDecibels(magnitudesState_[
static_cast<size_t>(k)], floorDb);
562 slot.magnitudesDb[
static_cast<size_t>(k)].store(dB, std::memory_order_relaxed);
570 T& peak = peakState_[
static_cast<size_t>(k)];
571 peak = std::max(dB, std::max(floorDb, peak - peakDecayDb));
572 slot.peakDb[
static_cast<size_t>(k)].store(peak, std::memory_order_relaxed);
579 seq.fetch_add(1u, std::memory_order_release);
584 const int old = pendingSlot_.exchange(writeSlot_ | kFreshBit, std::memory_order_acq_rel);
585 writeSlot_ = old & kSlotMask;
586 newDataReady_.store(
true, std::memory_order_release);
589 double sampleRate_ = 48000.0;
594 std::unique_ptr<FFTReal<T>> fft_;
595 std::vector<T> window_;
597 T windowGain_ = T(1);
600 std::vector<T> inputRing_;
601 int ringWritePos_ = 0;
602 int samplesUntilFFT_ = 0;
604 std::vector<T> fftBuffer_;
605 std::vector<T> freqBuffer_;
608 std::vector<T> magnitudesState_;
609 std::vector<T> peakState_;
687 static constexpr int kFreshBit = 4;
688 static constexpr int kSlotMask = 3;
692 static constexpr double kAutoSpanRef = 2048.0;
693 static constexpr double kAutoSpanRate = 48000.0;
694 static constexpr int kAutoMinFft = 256;
695 static constexpr int kAutoMaxFft = 16384;
701 static constexpr int kSnapshotAttempts = 4;
703 static_assert(std::atomic<T>::is_always_lock_free,
704 "SpectrumAnalyzer requires lock-free atomic<T> slot words "
705 "(audio-thread stores must not lock)");
706 static_assert(std::atomic<unsigned>::is_always_lock_free,
707 "SpectrumAnalyzer requires a lock-free seqlock counter "
708 "(audio-thread RMWs must not lock)");
712 std::unique_ptr<std::atomic<T>[]> magnitudesDb;
713 std::unique_ptr<std::atomic<T>[]> peakDb;
717 std::unique_ptr<std::atomic<unsigned>> seq;
719 std::array<OutSlot, 3> outSlots_;
721 mutable int readSlot_ = 1;
722 mutable std::atomic<int> pendingSlot_{ 2 };
731 mutable std::vector<T> magSnapshot_;
732 mutable std::vector<T> peakSnapshot_;
733 mutable int magSnapshotHalf_ = 0;
734 mutable int peakSnapshotHalf_ = 0;
735 mutable long long staleSnapshots_ = 0;
738 void acquireLatestSlot() const noexcept
740 int expected = pendingSlot_.load(std::memory_order_acquire);
741 while (expected & kFreshBit)
743 if (pendingSlot_.compare_exchange_weak(expected, readSlot_,
744 std::memory_order_acq_rel,
745 std::memory_order_acquire))
747 readSlot_ = expected & kSlotMask;
768 [[nodiscard]]
const T* snapshotFrame(
bool peakArray)
const noexcept
770 std::vector<T>& store = peakArray ? peakSnapshot_ : magSnapshot_;
771 int& half = peakArray ? peakSnapshotHalf_ : magSnapshotHalf_;
775 if (numBins_ <= 0)
return store.data();
777 T*
const spare = store.data()
778 +
static_cast<size_t>(half ^ 1) *
static_cast<size_t>(numBins_);
780 for (
int attempt = 0; attempt < kSnapshotAttempts; ++attempt)
783 const OutSlot& slot = outSlots_[
static_cast<size_t>(readSlot_)];
784 const std::atomic<T>*
const src =
785 (peakArray ? slot.peakDb : slot.magnitudesDb).get();
786 const std::atomic<unsigned>& seq = *slot.seq;
788 const unsigned s0 = seq.load(std::memory_order_acquire);
789 if ((s0 & 1u) != 0u)
continue;
791 for (
int k = 0; k < numBins_; ++k)
792 spare[
static_cast<size_t>(k)] =
793 src[
static_cast<size_t>(k)].load(std::memory_order_relaxed);
795 std::atomic_thread_fence(std::memory_order_acquire);
796 if (seq.load(std::memory_order_relaxed) == s0)
805 +
static_cast<size_t>(half) *
static_cast<size_t>(numBins_);
808 int ringMask_ = 2047;
810 std::atomic<T> smoothing_ { T(0.8) };
811 std::atomic<T> peakDecayRate_ { T(10) };
812 std::atomic<T> floorDb_ { T(-100) };
813 std::atomic<bool> peakHoldEnabled_ {
false };
815 std::atomic<bool> newDataReady_{
false };