From cc4dd60ce3dd5a8e332b150e658f5e738b7fd4b9 Mon Sep 17 00:00:00 2001 From: Stijn van Beek Date: Sat, 4 Apr 2026 20:02:02 +0200 Subject: [PATCH] FFT buffer size independent from audio service buffer size --- .../napfft/src/fftaudionodecomponent.cpp | 10 +++- .../napfft/src/fftaudionodecomponent.h | 1 + system_modules/napfft/src/fftbuffer.cpp | 57 +++++++++---------- system_modules/napfft/src/fftbuffer.h | 10 ++-- system_modules/napfft/src/fftnode.cpp | 7 +-- system_modules/napfft/src/fftnode.h | 3 +- 6 files changed, 45 insertions(+), 43 deletions(-) diff --git a/system_modules/napfft/src/fftaudionodecomponent.cpp b/system_modules/napfft/src/fftaudionodecomponent.cpp index a64f1f10b7..9b41a773a9 100644 --- a/system_modules/napfft/src/fftaudionodecomponent.cpp +++ b/system_modules/napfft/src/fftaudionodecomponent.cpp @@ -17,6 +17,7 @@ RTTI_BEGIN_CLASS(nap::FFTAudioNodeComponent) RTTI_PROPERTY("Input", &nap::FFTAudioNodeComponent::mInput, nap::rtti::EPropertyMetaData::Required) RTTI_PROPERTY("Overlaps", &nap::FFTAudioNodeComponent::mOverlaps, nap::rtti::EPropertyMetaData::Default) RTTI_PROPERTY("Channel", &nap::FFTAudioNodeComponent::mChannel, nap::rtti::EPropertyMetaData::Default) + RTTI_PROPERTY("FFTBufferSize", &nap::FFTAudioNodeComponent::mFFTBufferSize, nap::rtti::EPropertyMetaData::Default) RTTI_END_CLASS RTTI_BEGIN_CLASS_NO_DEFAULT_CONSTRUCTOR(nap::FFTAudioNodeComponentInstance) @@ -38,8 +39,15 @@ namespace nap if (!errorState.check(mResource->mChannel < mInput->getChannelCount(), "%s: Channel exceeds number of input channels", mResource->mID.c_str())) return false; + // Validate FFT buffer size + if (!(mResource->mFFTBufferSize & (mResource->mFFTBufferSize - 1)) == 0) + { + errorState.fail("FFT buffer size needs to be a power of 2."); + return false; + } + auto& node_manager = mAudioService->getNodeManager(); - mFFTNode = node_manager.makeSafe(node_manager); + mFFTNode = node_manager.makeSafe(node_manager, mResource->mFFTBufferSize); mFFTNode->mInput.connect(*mInput->getOutputForChannel(mResource->mChannel)); mFFTBuffer = &mFFTNode->getFFTBuffer(); diff --git a/system_modules/napfft/src/fftaudionodecomponent.h b/system_modules/napfft/src/fftaudionodecomponent.h index 7e7a74f295..9fdb3feaa0 100644 --- a/system_modules/napfft/src/fftaudionodecomponent.h +++ b/system_modules/napfft/src/fftaudionodecomponent.h @@ -33,6 +33,7 @@ namespace nap nap::ComponentPtr mInput; ///< Property: 'Input' The component whose audio output will be measured. FFTBuffer::EOverlap mOverlaps = FFTBuffer::EOverlap::One; ///< Property: 'Overlaps' Number of overlaps, more increases fft precision in exchange for performance int mChannel = 0; ///< Property: 'Channel' Channel of the input that will be analyzed. + int mFFTBufferSize = 2048; ///< Property: 'FFTBufferSize' Number of samples being analyzed per window. }; diff --git a/system_modules/napfft/src/fftbuffer.cpp b/system_modules/napfft/src/fftbuffer.cpp index 5e08502b0a..79beb12d88 100644 --- a/system_modules/napfft/src/fftbuffer.cpp +++ b/system_modules/napfft/src/fftbuffer.cpp @@ -87,6 +87,7 @@ namespace nap // Create sample buffer mSampleBufferFormatted.resize(data_size * 2); mSampleBufferWindowed.resize(data_size); + mCircularBuffer.resize(data_size * 4); // Compute hamming window mForwardHammingWindow.resize(data_size); @@ -114,31 +115,31 @@ namespace nap void FFTBuffer::supply(const std::vector& samples) { - // Try to lock and copy the audio buffer for FFT analysis. - // This prevents the audio thread from stalling when the analysis thread is working on a (previous) set at the same time. - // We do however attempt to store it for the most up to date, accurate image. - std::unique_lock lock(mSampleBufferMutex, std::defer_lock); - if (lock.try_lock()) + auto writePosition = mWritePosition.load(); + int index = writePosition % mCircularBuffer.size(); + for (auto& sample : samples) { - mSampleBufferA = samples; - mSampleData = true; + mCircularBuffer[index++] = sample; + if (index >= mCircularBuffer.size()) + index = 0; } + mWritePosition.store(writePosition + samples.size()); } void FFTBuffer::transform() { - // Check if there's something to consume -> atomic dirty check first to minimize overhead - // Note that we could use a try_lock construction for both producer and consumer threads, - // But it's safer to always transform available sample data, instead of potentially not consuming *any* data. - if (mSampleData.load()) + auto writePosition = mWritePosition.load(); + auto readPosition = mReadPosition.load(); + + // Check if there is a new FFT buffer of data available + if (writePosition - readPosition >= mContext->getSize()) { - // Lock when available and swap buffer memory locations -> making the previously transformed buffer available for storage. - { - std::lock_guard lock(mSampleBufferMutex); - std::swap(mSampleBufferA, mSampleBufferB); - mSampleData = false; - } + // If we are more than two FFT buffers behind proceed to the newest one available + // This way we don't create latency when transform() is not called regularly enough. + while (readPosition + mContext->getSize() < writePosition - mContext->getSize()) + readPosition += mContext->getSize(); + createImage(); } } @@ -160,22 +161,16 @@ namespace nap // Copy second half to first half std::memcpy(mSampleBufferFormatted.data(), half_ptr, data_bytes); - // Copy new samples to second half - if (mSampleBufferB.size() == data_size) - { - std::memcpy(half_ptr, mSampleBufferB.data(), data_bytes); - } - else if (mSampleBufferB.size() > data_size) - { - // Zero-padding - std::fill(mSampleBufferFormatted.begin(), mSampleBufferFormatted.end(), 0.0f); - std::memcpy(half_ptr, mSampleBufferB.data(), data_bytes); - } - else + // Copy data from circular buffer to the second half of the formatted buffer + auto readPosition = mReadPosition.load(); + int circularBufferIndex = readPosition % mCircularBuffer.size(); + for (int i = 0; i < data_size; ++i) { - NAP_ASSERT_MSG(false, "Specified sample buffer size too small"); - return; + half_ptr[i] = mCircularBuffer[circularBufferIndex++]; + if (circularBufferIndex >= mCircularBuffer.size()) + circularBufferIndex = 0; } + mReadPosition.store(readPosition + data_size); // Copy data to windowed array const uint hop_count = static_cast(mOverlap); diff --git a/system_modules/napfft/src/fftbuffer.h b/system_modules/napfft/src/fftbuffer.h index 6bdd1a0ea5..859f44a2a4 100644 --- a/system_modules/napfft/src/fftbuffer.h +++ b/system_modules/napfft/src/fftbuffer.h @@ -11,6 +11,8 @@ #include #include +#include "audio/utility/audiotypes.h" + namespace nap { /** @@ -88,10 +90,9 @@ namespace nap float mHammingWindowSum; //< The sum of all window function coefficients float mNormalizationFactor; //< Inverse of the window sum (2/sum) - - std::mutex mSampleBufferMutex; //< The mutex for accessing the sample buffer - std::vector mSampleBufferA; //< Samples provided by the audio node - std::vector mSampleBufferB; //< Thread safe copy of original samples + std::vector mCircularBuffer; //< Circular buffer that the audio thread writes in and the analysis thread reads from + std::atomic mWritePosition = { 0 }; //< Write position on audio thread in the circular buffer + std::atomic mReadPosition = { 0 }; //< Read position on analysis thread in the circular buffer std::vector mSampleBufferFormatted; //< The sample buffer before application of a window function std::vector mSampleBufferWindowed; //< The sample buffer after application of a window function @@ -100,6 +101,5 @@ namespace nap EOverlap mOverlap; //< The number of audio buffer overlaps for FFT analysis (hops) uint mHopSize; //< The number of bins of a single hop - std::atomic mSampleData = { false }; //< Amplitudes dirty checking flag, prevents redundant FFT analyses }; } diff --git a/system_modules/napfft/src/fftnode.cpp b/system_modules/napfft/src/fftnode.cpp index d96747b879..cd40973abc 100644 --- a/system_modules/napfft/src/fftnode.cpp +++ b/system_modules/napfft/src/fftnode.cpp @@ -14,13 +14,10 @@ RTTI_END_CLASS namespace nap { - FFTNode::FFTNode(audio::NodeManager& nodeManager, FFTBuffer::EOverlap overlaps) : + FFTNode::FFTNode(audio::NodeManager& nodeManager, int fftBufferSize, FFTBuffer::EOverlap overlaps) : audio::Node(nodeManager) { - const auto buffer_size = getNodeManager().getInternalBufferSize(); - assert(buffer_size >= 2); - - mFFTBuffer = std::make_unique(buffer_size, overlaps); + mFFTBuffer = std::make_unique(fftBufferSize, overlaps); getNodeManager().registerRootProcess(*this); } diff --git a/system_modules/napfft/src/fftnode.h b/system_modules/napfft/src/fftnode.h index 4b5a715c59..1c409a9b1d 100644 --- a/system_modules/napfft/src/fftnode.h +++ b/system_modules/napfft/src/fftnode.h @@ -34,9 +34,10 @@ namespace nap /** * @param audioService the NAP audio service. * @param nodeManager the node manager this node must be registered to. + * @param fftBufferSize size of the fft analysis window in samples. * @param overlaps the number of overlaps */ - FFTNode(audio::NodeManager& nodeManager, FFTBuffer::EOverlap overlaps = FFTBuffer::EOverlap::One); + FFTNode(audio::NodeManager& nodeManager, int fftBufferSize, FFTBuffer::EOverlap overlaps = FFTBuffer::EOverlap::One); // Destructor virtual ~FFTNode();