RAM2118 commited on
Commit
f6aee3c
·
verified ·
1 Parent(s): 64c9482

Sprint 4: Add AudioAnalyser — LUFS, FFT bands, stereo width, centroid, true peak

Browse files

Runs in processBlock (audio thread), stores results in atomics.
MCP get_analysis reads atomics from HTTP thread — zero contention.

Output format: "-14.2 LUFS | TP:-1.1 | sub:ok bass:+3 low:ok mid:ok hi:-2 brill:ok | W:0.72 | bright"
Max ~60 tokens per call."

Files changed (1) hide show
  1. Source/Plugin/AudioAnalyser.h +259 -0
Source/Plugin/AudioAnalyser.h ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ #include <juce_audio_basics/juce_audio_basics.h>
4
+ #include <juce_dsp/juce_dsp.h>
5
+ #include <atomic>
6
+ #include <array>
7
+ #include <cmath>
8
+ #include <string>
9
+
10
+ // Real-time audio analyser — runs in processBlock, results read from MCP thread.
11
+ // All inter-thread communication via std::atomic — no locks in audio path.
12
+
13
+ class AudioAnalyser
14
+ {
15
+ public:
16
+ AudioAnalyser() = default;
17
+
18
+ void prepareToPlay(double sampleRate, int blockSize)
19
+ {
20
+ currentSampleRate = sampleRate;
21
+ currentBlockSize = blockSize;
22
+
23
+ // FFT setup (1024-point)
24
+ fftBuffer.fill(0.0f);
25
+ fftWritePos = 0;
26
+
27
+ // LUFS — simplified momentary loudness (400ms window)
28
+ lufsWindowSamples = (int)(sampleRate * 0.4);
29
+ lufsAccumulator = 0.0;
30
+ lufsSampleCount = 0;
31
+
32
+ // Reset all outputs
33
+ momentaryLUFS.store(-100.0f);
34
+ truePeak.store(-100.0f);
35
+ stereoWidth.store(0.0f);
36
+ spectralCentroid.store(0.0f);
37
+ isSilent.store(true);
38
+
39
+ for (auto& b : bands)
40
+ b.store(0.0f);
41
+ }
42
+
43
+ // Called from audio thread — MUST be lock-free
44
+ void processBlock(const juce::AudioBuffer<float>& buffer)
45
+ {
46
+ const int numSamples = buffer.getNumSamples();
47
+ const int numChannels = buffer.getNumChannels();
48
+ if (numSamples == 0 || numChannels == 0) return;
49
+
50
+ const float* left = buffer.getReadPointer(0);
51
+ const float* right = numChannels > 1 ? buffer.getReadPointer(1) : left;
52
+
53
+ // --- True Peak ---
54
+ float peak = 0.0f;
55
+ for (int i = 0; i < numSamples; ++i)
56
+ {
57
+ float absL = std::fabs(left[i]);
58
+ float absR = std::fabs(right[i]);
59
+ peak = std::max(peak, std::max(absL, absR));
60
+ }
61
+ float peakDb = peak > 0.0f ? 20.0f * std::log10(peak) : -100.0f;
62
+ truePeak.store(peakDb);
63
+
64
+ // --- Silence Gate ---
65
+ if (peakDb < -60.0f)
66
+ {
67
+ isSilent.store(true);
68
+ return;
69
+ }
70
+ isSilent.store(false);
71
+
72
+ // --- LUFS (simplified momentary — sum of squares over 400ms) ---
73
+ for (int i = 0; i < numSamples; ++i)
74
+ {
75
+ float mono = (left[i] + right[i]) * 0.5f;
76
+ lufsAccumulator += (double)(mono * mono);
77
+ lufsSampleCount++;
78
+
79
+ if (lufsSampleCount >= lufsWindowSamples)
80
+ {
81
+ double meanSquare = lufsAccumulator / (double)lufsSampleCount;
82
+ float lufs = (meanSquare > 0.0) ? (float)(-0.691 + 10.0 * std::log10(meanSquare)) : -100.0f;
83
+ momentaryLUFS.store(lufs);
84
+ lufsAccumulator = 0.0;
85
+ lufsSampleCount = 0;
86
+ }
87
+ }
88
+
89
+ // --- Stereo Width (correlation-based) ---
90
+ float sumMid = 0.0f, sumSide = 0.0f;
91
+ for (int i = 0; i < numSamples; ++i)
92
+ {
93
+ float mid = (left[i] + right[i]) * 0.5f;
94
+ float side = (left[i] - right[i]) * 0.5f;
95
+ sumMid += mid * mid;
96
+ sumSide += side * side;
97
+ }
98
+ float totalEnergy = sumMid + sumSide;
99
+ float width = (totalEnergy > 1e-10f) ? sumSide / totalEnergy : 0.0f;
100
+ // Smooth
101
+ float prevWidth = stereoWidth.load();
102
+ stereoWidth.store(prevWidth * 0.9f + width * 0.1f);
103
+
104
+ // --- FFT accumulator (ring buffer → run FFT when full) ---
105
+ for (int i = 0; i < numSamples; ++i)
106
+ {
107
+ float mono = (left[i] + right[i]) * 0.5f;
108
+ fftBuffer[fftWritePos] = mono;
109
+ fftWritePos++;
110
+
111
+ if (fftWritePos >= kFFTSize)
112
+ {
113
+ runFFT();
114
+ fftWritePos = 0;
115
+ }
116
+ }
117
+ }
118
+
119
+ // Called from HTTP/MCP thread — thread-safe read of atomic values
120
+ std::string getCompactAnalysis() const
121
+ {
122
+ if (isSilent.load())
123
+ return "silent";
124
+
125
+ char buf[256];
126
+ float lufs = momentaryLUFS.load();
127
+ float tp = truePeak.load();
128
+ float width = stereoWidth.load();
129
+ float centroid = spectralCentroid.load();
130
+
131
+ // Band deviations from flat (0 = flat reference)
132
+ // Only report bands that deviate > ±2dB
133
+ std::string bandStr;
134
+ static const char* bandNames[] = {"sub", "bass", "low", "mid", "hi", "pres", "brill"};
135
+ for (int i = 0; i < 7; ++i)
136
+ {
137
+ float db = bands[i].load();
138
+ if (db > 2.0f)
139
+ bandStr += std::string(bandNames[i]) + ":+" + std::to_string((int)db) + " ";
140
+ else if (db < -2.0f)
141
+ bandStr += std::string(bandNames[i]) + ":" + std::to_string((int)db) + " ";
142
+ }
143
+ if (bandStr.empty())
144
+ bandStr = "flat ";
145
+
146
+ // Brightness descriptor from centroid
147
+ const char* brightnessDesc = "balanced";
148
+ if (centroid > 4000.0f) brightnessDesc = "bright";
149
+ else if (centroid < 1500.0f) brightnessDesc = "dark";
150
+
151
+ snprintf(buf, sizeof(buf), "%.1f LUFS | TP:%.1f | %s| W:%.2f | %s",
152
+ lufs, tp, bandStr.c_str(), width, brightnessDesc);
153
+
154
+ return std::string(buf);
155
+ }
156
+
157
+ private:
158
+ static constexpr int kFFTOrder = 10;
159
+ static constexpr int kFFTSize = 1 << kFFTOrder; // 1024
160
+
161
+ void runFFT()
162
+ {
163
+ // Apply window
164
+ std::array<float, kFFTSize> windowed;
165
+ for (int i = 0; i < kFFTSize; ++i)
166
+ windowed[i] = fftBuffer[i] * hanningWindow(i, kFFTSize);
167
+
168
+ // JUCE FFT requires 2x buffer (real + imaginary interleaved)
169
+ std::array<float, kFFTSize * 2> fftData{};
170
+ for (int i = 0; i < kFFTSize; ++i)
171
+ fftData[i] = windowed[i];
172
+
173
+ juce::dsp::FFT fft(kFFTOrder);
174
+ fft.performFrequencyOnlyForwardTransform(fftData.data());
175
+
176
+ // Now fftData[0..kFFTSize/2] contains magnitudes
177
+ int halfSize = kFFTSize / 2;
178
+ float binWidth = (float)currentSampleRate / (float)kFFTSize;
179
+
180
+ // --- 7 Band Energy ---
181
+ // sub: 20-60, bass: 60-250, low: 250-500, mid: 500-2k, hi: 2k-6k, pres: 6k-12k, brill: 12k-20k
182
+ static const float bandEdges[] = {20, 60, 250, 500, 2000, 6000, 12000, 20000};
183
+
184
+ float bandEnergy[7] = {};
185
+ int bandCount[7] = {};
186
+
187
+ for (int bin = 1; bin < halfSize; ++bin)
188
+ {
189
+ float freq = bin * binWidth;
190
+ float mag = fftData[bin];
191
+
192
+ for (int b = 0; b < 7; ++b)
193
+ {
194
+ if (freq >= bandEdges[b] && freq < bandEdges[b + 1])
195
+ {
196
+ bandEnergy[b] += mag * mag;
197
+ bandCount[b]++;
198
+ break;
199
+ }
200
+ }
201
+ }
202
+
203
+ // Convert to dB relative to average
204
+ float totalEnergy = 0.0f;
205
+ for (int b = 0; b < 7; ++b)
206
+ totalEnergy += bandEnergy[b];
207
+
208
+ float avgEnergy = totalEnergy / 7.0f;
209
+
210
+ for (int b = 0; b < 7; ++b)
211
+ {
212
+ float bandDb = 0.0f;
213
+ if (avgEnergy > 1e-10f && bandEnergy[b] > 1e-10f)
214
+ bandDb = 10.0f * std::log10(bandEnergy[b] / avgEnergy);
215
+ bands[b].store(bandDb);
216
+ }
217
+
218
+ // --- Spectral Centroid ---
219
+ float weightedSum = 0.0f;
220
+ float magnitudeSum = 0.0f;
221
+ for (int bin = 1; bin < halfSize; ++bin)
222
+ {
223
+ float freq = bin * binWidth;
224
+ float mag = fftData[bin];
225
+ weightedSum += freq * mag;
226
+ magnitudeSum += mag;
227
+ }
228
+ float centroid = (magnitudeSum > 1e-10f) ? weightedSum / magnitudeSum : 0.0f;
229
+ // Smooth
230
+ float prevCentroid = spectralCentroid.load();
231
+ spectralCentroid.store(prevCentroid * 0.8f + centroid * 0.2f);
232
+ }
233
+
234
+ static float hanningWindow(int i, int size)
235
+ {
236
+ return 0.5f * (1.0f - std::cos(2.0f * 3.14159265358979f * i / (float)(size - 1)));
237
+ }
238
+
239
+ // State
240
+ double currentSampleRate = 44100.0;
241
+ int currentBlockSize = 512;
242
+
243
+ // FFT ring buffer
244
+ std::array<float, kFFTSize> fftBuffer{};
245
+ int fftWritePos = 0;
246
+
247
+ // LUFS accumulator
248
+ int lufsWindowSamples = 17640; // 400ms at 44.1kHz
249
+ double lufsAccumulator = 0.0;
250
+ int lufsSampleCount = 0;
251
+
252
+ // Thread-safe outputs (written in audio thread, read from HTTP thread)
253
+ std::atomic<float> momentaryLUFS{-100.0f};
254
+ std::atomic<float> truePeak{-100.0f};
255
+ std::atomic<float> stereoWidth{0.0f};
256
+ std::atomic<float> spectralCentroid{0.0f};
257
+ std::atomic<bool> isSilent{true};
258
+ std::array<std::atomic<float>, 7> bands{}; // sub, bass, low, mid, hi, pres, brill
259
+ };