| #pragma once |
|
|
| #include <juce_audio_basics/juce_audio_basics.h> |
| #include <juce_dsp/juce_dsp.h> |
| #include <atomic> |
| #include <array> |
| #include <cmath> |
| #include <string> |
|
|
| |
| |
|
|
| class AudioAnalyser |
| { |
| public: |
| AudioAnalyser() = default; |
|
|
| void prepareToPlay(double sampleRate, int blockSize) |
| { |
| currentSampleRate = sampleRate; |
| currentBlockSize = blockSize; |
|
|
| |
| fftBuffer.fill(0.0f); |
| fftWritePos = 0; |
|
|
| |
| lufsWindowSamples = (int)(sampleRate * 0.4); |
| lufsAccumulator = 0.0; |
| lufsSampleCount = 0; |
|
|
| |
| 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); |
| } |
|
|
| |
| 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; |
|
|
| |
| 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); |
|
|
| |
| if (peakDb < -60.0f) |
| { |
| isSilent.store(true); |
| return; |
| } |
| isSilent.store(false); |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| 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; |
| |
| float prevWidth = stereoWidth.load(); |
| stereoWidth.store(prevWidth * 0.9f + width * 0.1f); |
|
|
| |
| 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; |
| } |
| } |
| } |
|
|
| |
| 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(); |
|
|
| |
| |
| 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 "; |
|
|
| |
| 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; |
|
|
| void runFFT() |
| { |
| |
| std::array<float, kFFTSize> windowed; |
| for (int i = 0; i < kFFTSize; ++i) |
| windowed[i] = fftBuffer[i] * hanningWindow(i, kFFTSize); |
|
|
| |
| std::array<float, kFFTSize * 2> fftData{}; |
| for (int i = 0; i < kFFTSize; ++i) |
| fftData[i] = windowed[i]; |
|
|
| |
| fft.performFrequencyOnlyForwardTransform(fftData.data()); |
|
|
| |
| int halfSize = kFFTSize / 2; |
| float binWidth = (float)currentSampleRate / (float)kFFTSize; |
|
|
| |
| |
| 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; |
| } |
| } |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| 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; |
| |
| 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))); |
| } |
|
|
| |
| double currentSampleRate = 44100.0; |
| int currentBlockSize = 512; |
|
|
| |
| juce::dsp::FFT fft{kFFTOrder}; |
|
|
| |
| std::array<float, kFFTSize> fftBuffer{}; |
| int fftWritePos = 0; |
|
|
| |
| int lufsWindowSamples = 17640; |
| double lufsAccumulator = 0.0; |
| int lufsSampleCount = 0; |
|
|
| |
| 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{}; |
| }; |
|
|