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