Mema
Memory Matrix — multi-channel audio matrix monitor and router
Loading...
Searching...
No Matches
ProcessorDataAnalyzer.cpp
Go to the documentation of this file.
1/* Copyright (c) 2024-2025, Christian Ahrens
2 *
3 * This file is part of Mema <https://github.com/ChristianAhrens/Mema>
4 *
5 * This tool is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU Lesser General Public License version 3.0 as published
7 * by the Free Software Foundation.
8 *
9 * This tool is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
12 * details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this tool; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
20
21namespace Mema
22{
23
24
25//==============================================================================
27 m_fwdFFT(fftOrder),
28 m_windowF(fftSize, dsp::WindowingFunction<float>::hann)
29{
30 setHoldTime(1000);
31}
32
37
38void ProcessorDataAnalyzer::setUseProcessingTypes(bool useLevelProcessing, bool useBufferProcessing, bool useSepctrumProcessing)
39{
40 m_useLevelProcessing = useLevelProcessing;
41 m_useBufferProcessing = useBufferProcessing;
42 m_useSpectrumProcessing = useSepctrumProcessing;
43}
44
46{
47 return m_useLevelProcessing;
48}
49
51{
52 return m_useBufferProcessing;
53}
54
56{
57 return m_useSpectrumProcessing;
58}
59
60void ProcessorDataAnalyzer::initializeParameters(double sampleRate, int bufferSize)
61{
62 m_sampleRate = static_cast<unsigned long>(sampleRate);
63 m_samplesPerCentiSecond = static_cast<int>(sampleRate * 0.01f);
64 m_bufferSize = bufferSize;
65 m_missingSamplesForCentiSecond = static_cast<int>(m_samplesPerCentiSecond + 0.5f);
66 m_centiSecondBuffer.setSize(2, m_missingSamplesForCentiSecond, false, true, false);
67}
68
70{
71 m_sampleRate = 0;
72 m_samplesPerCentiSecond = 0;
73 m_bufferSize = 0;
74 m_centiSecondBuffer.clear();
75 m_missingSamplesForCentiSecond = 0;
76}
77
79{
80 m_holdTimeMs = holdTimeMs;
81
82 startTimer(m_holdTimeMs);
83}
84
86{
87 std::lock_guard<std::mutex> lock(m_callbackListenersMutex);
88 m_callbackListeners.add(listener);
89}
90
92{
93 std::lock_guard<std::mutex> lock(m_callbackListenersMutex);
94 m_callbackListeners.remove(m_callbackListeners.indexOf(listener));
95}
96
97void ProcessorDataAnalyzer::analyzeData(const juce::AudioBuffer<float>& buffer)
98{
99 if (!IsInitialized())
100 return;
101
102 int numChannels = buffer.getNumChannels();
103
104 if (numChannels != m_centiSecondBuffer.getNumChannels())
105 m_centiSecondBuffer.setSize(numChannels, m_samplesPerCentiSecond, false, true, true);
106 if (m_sampleRate != m_centiSecondBuffer.GetSampleRate())
107 m_centiSecondBuffer.SetSampleRate(m_sampleRate);
108
109 // Ensure per-channel FFT buffers are sized correctly
110 if (isSepctrumProcessingUsed() && m_FFTdata.size() != numChannels)
111 {
112 m_FFTdata.resize(numChannels);
113 m_FFTdataPos.resize(numChannels, 0);
114 for (auto& channelFFTdata : m_FFTdata)
115 channelFFTdata.resize(fftSize * 2, 0.0f);
116 }
117
118 int availableSamples = buffer.getNumSamples();
119 int readPos = 0;
120
121 while (availableSamples >= m_missingSamplesForCentiSecond)
122 {
123 int writePos = m_samplesPerCentiSecond - m_missingSamplesForCentiSecond;
124
125 for (int i = 0; i < numChannels; ++i)
126 {
128 {
129 // Generate signal buffer data
130 m_centiSecondBuffer.copyFrom(i, writePos, buffer.getReadPointer(i) + readPos, m_missingSamplesForCentiSecond);
131 }
132
134 {
135 // Generate level data
136 auto peak = m_centiSecondBuffer.getMagnitude(i, 0, m_samplesPerCentiSecond);
137 auto rms = m_centiSecondBuffer.getRMSLevel(i, 0, m_samplesPerCentiSecond);
138 auto hold = std::max(peak, m_level.GetLevel(i + 1).hold);
139 m_level.SetLevel(i + 1, ProcessorLevelData::LevelVal(peak, rms, hold, static_cast<float>(getGlobalMindB())));
140 }
141
143 {
144 // Generate spectrum data - all channels always process their audio data
145 // The FFT buffer accumulates samples for all channels
146 processSpectrumForChannel(i, m_centiSecondBuffer.getReadPointer(i), m_samplesPerCentiSecond);
147 }
148 }
149
151 BroadcastData(&m_level);
152
154 BroadcastData(&m_centiSecondBuffer);
155
157 BroadcastData(&m_spectrum);
158
159
160 readPos += m_missingSamplesForCentiSecond;
161 availableSamples -= m_missingSamplesForCentiSecond;
162 m_missingSamplesForCentiSecond = m_samplesPerCentiSecond;
163 }
164
165 // Handle remaining samples
166 if (availableSamples > 0)
167 {
168 int writePos = m_samplesPerCentiSecond - m_missingSamplesForCentiSecond;
169 for (int i = 0; i < numChannels; ++i)
170 {
171 m_centiSecondBuffer.copyFrom(i, writePos, buffer.getReadPointer(i) + readPos, availableSamples);
172 }
173 m_missingSamplesForCentiSecond -= availableSamples;
174 }
175}
176
177void ProcessorDataAnalyzer::processSpectrumForChannel(int channelIndex, const float* channelData, int numSamples)
178{
179 int samplesProcessed = 0;
180
181 // Fill FFT buffer until we have enough samples
182 while (samplesProcessed < numSamples)
183 {
184 int samplesNeeded = fftSize - m_FFTdataPos[channelIndex];
185 int samplesAvailable = numSamples - samplesProcessed;
186 int samplesToCopy = std::min(samplesNeeded, samplesAvailable);
187
188 // Copy samples into FFT buffer using JUCE's optimized copy
189 juce::FloatVectorOperations::copy(
190 m_FFTdata[channelIndex].data() + m_FFTdataPos[channelIndex],
191 channelData + samplesProcessed,
192 samplesToCopy
193 );
194
195 m_FFTdataPos[channelIndex] += samplesToCopy;
196 samplesProcessed += samplesToCopy;
197
198 // When we have enough samples, perform FFT
199 if (m_FFTdataPos[channelIndex] >= fftSize)
200 {
201 performFFTAndUpdateSpectrum(channelIndex);
202
203 // 25% overlap - good balance between smoothness and CPU usage
204 const int hopSize = (fftSize * 3) / 4;
205
206 // Shift the buffer efficiently
207 juce::FloatVectorOperations::copy(
208 m_FFTdata[channelIndex].data(),
209 m_FFTdata[channelIndex].data() + hopSize,
210 fftSize - hopSize
211 );
212
213 m_FFTdataPos[channelIndex] = fftSize - hopSize;
214 }
215 }
216}
217
218void ProcessorDataAnalyzer::performFFTAndUpdateSpectrum(int channelIndex)
219{
220 float* fftData = m_FFTdata[channelIndex].data();
221
222 // Apply windowing function
223 m_windowF.multiplyWithWindowingTable(fftData, fftSize);
224
225 // Perform FFT
226 m_fwdFFT.performFrequencyOnlyForwardTransform(fftData);
227
228 // Get spectrum bands for this channel
229 ProcessorSpectrumData::SpectrumBands spectrumBands = m_spectrum.GetSpectrum(channelIndex);
230 spectrumBands.mindB = static_cast<float>(getGlobalMindB());
231 spectrumBands.maxdB = static_cast<float>(getGlobalMaxdB());
232
233 const float nyquistFreq = m_sampleRate * 0.5f;
234
235 const float minDisplayFreq = 20.0f;
236 const float maxDisplayFreq = std::min(20000.0f, nyquistFreq);
237
238 spectrumBands.minFreq = minDisplayFreq;
239 spectrumBands.maxFreq = maxDisplayFreq;
240 spectrumBands.freqRes = (maxDisplayFreq - minDisplayFreq) / ProcessorSpectrumData::SpectrumBands::count;
241
242 const int usableFFTBins = fftSize / 2;
243 const float binFrequency = m_sampleRate / static_cast<float>(fftSize);
244
245 const float fftScale = 1.0f / static_cast<float>(fftSize);
246 const float windowCompensation = 2.0f;
247
248 // With overlap still use smoothing for visual nicety
249 const float smoothingFactor = 0.6f;
250
251 // Pre-calculate log values for efficiency
252 const float invBandCount = 1.0f / ProcessorSpectrumData::SpectrumBands::count;
253
254 for (int bandIndex = 0; bandIndex < ProcessorSpectrumData::SpectrumBands::count; ++bandIndex)
255 {
256 // Optimized logarithmic frequency calculation
257 float t = bandIndex * invBandCount;
258 float bandStartFreq = minDisplayFreq * std::pow(maxDisplayFreq / minDisplayFreq, t);
259 float bandEndFreq = minDisplayFreq * std::pow(maxDisplayFreq / minDisplayFreq, t + invBandCount);
260
261 int startBin = static_cast<int>(bandStartFreq / binFrequency);
262 int endBin = static_cast<int>(bandEndFreq / binFrequency);
263
264 startBin = juce::jlimit(0, usableFFTBins - 1, startBin);
265 endBin = juce::jlimit(startBin + 1, usableFFTBins, endBin);
266
267 int numBins = endBin - startBin;
268
269 // Calculate RMS
270 float bandSumSquared = 0.0f;
271 for (int bin = startBin; bin < endBin; ++bin)
272 {
273 float magnitude = fftData[bin] * fftScale * windowCompensation;
274 bandSumSquared += magnitude * magnitude;
275 }
276
277 // Calculate RMS value
278 float rmsValue = std::sqrt(bandSumSquared / numBins);
279
280 // Convert to dB with proper floor to avoid log(0)
281 const float minMagnitude = 0.00001f; // approximately -100 dB
282 rmsValue = std::max(rmsValue, minMagnitude);
283
284 float leveldB = juce::Decibels::gainToDecibels(rmsValue);
285 leveldB = juce::jlimit(spectrumBands.mindB, spectrumBands.maxdB, leveldB);
286 float normalizedLevel = juce::jmap(leveldB, spectrumBands.mindB, spectrumBands.maxdB, 0.0f, 1.0f);
287
288 // Light smoothing with overlap
289 float previousLevel = spectrumBands.bandsPeak[bandIndex];
290 float smoothedLevel;
291
292 if (normalizedLevel > previousLevel)
293 {
294 // Fast attack
295 smoothedLevel = 0.1f * previousLevel + 0.9f * normalizedLevel;
296 }
297 else
298 {
299 // Moderate decay with overlap
300 smoothedLevel = smoothingFactor * previousLevel + (1.0f - smoothingFactor) * normalizedLevel;
301 }
302
303 spectrumBands.bandsPeak[bandIndex] = smoothedLevel;
304 spectrumBands.bandsHold[bandIndex] = std::max(smoothedLevel, spectrumBands.bandsHold[bandIndex]);
305 }
306
307 m_spectrum.SetSpectrum(channelIndex, spectrumBands);
308 juce::FloatVectorOperations::clear(fftData, fftSize * 2);
309}
310
311void ProcessorDataAnalyzer::BroadcastData(AbstractProcessorData* data)
312{
313 std::lock_guard<std::mutex> lock(m_callbackListenersMutex);
314 for (Listener* l : m_callbackListeners)
315 l->processingDataChanged(data);
316}
317
319{
320 FlushHold();
321}
322
323void ProcessorDataAnalyzer::FlushHold()
324{
325 // clear level hold values
326 auto channelCount = static_cast<int>(m_level.GetChannelCount());
327 for (auto i = 0; i < channelCount; ++i)
328 {
329 m_level.SetLevel(i + 1, ProcessorLevelData::LevelVal(0.0f, 0.0f, 0.0f, static_cast<float>(getGlobalMindB())));
330 }
331
332 // clear spectrum hold values auto channelCount = m_level.GetChannelCount();
333 channelCount = static_cast<int>(m_spectrum.GetChannelCount());
334 for (auto i = 0; i < channelCount; ++i)
335 {
336 ProcessorSpectrumData::SpectrumBands spectrumBands = m_spectrum.GetSpectrum(i);
337 for (auto j = 0; j < ProcessorSpectrumData::SpectrumBands::count; ++j)
338 {
339 spectrumBands.bandsPeak[j] = 0.0f;
340 spectrumBands.bandsHold[j] = 0.0f;
341 }
342
343 m_spectrum.SetSpectrum(i, spectrumBands);
344 }
345}
346
347} // namespace Mema
unsigned long GetSampleRate()
Returns the sample rate associated with this buffer.
void SetSampleRate(unsigned long rate)
Sets the sample rate associated with this buffer.
void addListener(Listener *listener)
void analyzeData(const juce::AudioBuffer< float > &buffer)
Submits a new audio buffer for analysis.
void removeListener(Listener *listener)
void setUseProcessingTypes(bool useLevelProcessing, bool useBufferProcessing, bool useSepctrumProcessing)
Configures which data types (level, spectrum, audio signal) the analyzer computes.
void timerCallback() override
Timer callback that broadcasts pending data changes to all registered listeners.
void initializeParameters(double sampleRate, int bufferSize)
void SetLevel(unsigned long channel, LevelVal level)
LevelVal GetLevel(unsigned long channel)
unsigned long GetChannelCount() override
Returns the number of audio channels this data object covers.
unsigned long GetChannelCount() override
Returns the number of audio channels this data object covers.
const SpectrumBands & GetSpectrum(unsigned long channel)
void SetSpectrum(unsigned long channel, SpectrumBands spectrum)
Definition Mema.cpp:27
Per-channel level values in both linear and dB domains.