#pragma once #include #include #include #include #include #include // 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& 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 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 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 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 momentaryLUFS{-100.0f}; std::atomic truePeak{-100.0f}; std::atomic stereoWidth{0.0f}; std::atomic spectralCentroid{0.0f}; std::atomic isSilent{true}; std::array, 7> bands{}; // sub, bass, low, mid, hi, pres, brill };