pluginbridge / Source /Plugin /AudioAnalyser.h
Sitar118
fix: restore v0.5.2 architecture + wire AudioAnalyser correctly
fdc4e41
Raw
History Blame Contribute Delete
8.66 kB
#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
};