FFT spectrogram, smoothing problem


class AnalyzerComponent : public juce::Component,
    private juce::Timer
{
public:
    AnalyzerComponent(SIGMusicFFTDemoAudioProcessor& ap_) : ap(ap_),
        forwardFFT(ap.fftOrder),
        window(ap.fftSize, juce::dsp::WindowingFunction<float>::hann)
    {
        // 初始化平滑的数组
        smoothedScopeData.assign(ap.scopeSize, 0.0f);
        previousFrame.assign(ap.scopeSize, 0.0f);

        setOpaque(true);
        startTimerHz(24);
    }

    ~AnalyzerComponent() override {}

    void timerCallback() override
    {
        if (ap.nextFFTBlockReady)
        {
            drawNextFrameOfSpectrum();
            ap.nextFFTBlockReady = false;
            repaint();
        }
    }

    void paint(juce::Graphics& g) override
    {
        g.fillAll(juce::Colours::black);

        g.setOpacity(1.0f);
        g.setColour(juce::Colours::white);
        drawFrame(g);
    }


    float aWeighting(float freq)
    {
        const float c1 = 12194.217f;
        const float c2 = 20.598997f;
        const float c3 = 107.65265f;
        const float c4 = 737.86223f;
        float f = freq * freq;
        float num = c1 * c1 * (f * f);
        float den = (f + c2 * c2) * std::sqrt((f + c3 * c3) * (f + c4 * c4)) * (f + c1 * c1);

        float aWeighted = 1.2589f * num / den;
        aWeighted = std::max(0.0f, 2.0f + 20.0f * std::log10(aWeighted));

        return aWeighted;
    }

    void drawNextFrameOfSpectrum()
    {

        window.multiplyWithWindowingTable(ap.fftData, ap.fftSize);
        forwardFFT.performFrequencyOnlyForwardTransform(ap.fftData);

        auto mindB = -100.0f;
        auto maxdB = 0.0f;
        float sampleRate = ap.getSampleRate();
        auto frequencyResolution = sampleRate / ap.fftSize;

        // 使用对数刻度选择绘图点
        std::vector<float> logFreqBins;
        float minLogFreq = std::log10(10.0f); // 20 Hz
        float maxLogFreq = std::log10(21000.0f); // 20 kHz
        float logFreqRange = maxLogFreq - minLogFreq;
        int numPoints = ap.scopeSize; // 选择绘图点的数量

        // 在对数尺度上生成你想绘制的频率点
        for (int i = 0; i < numPoints; ++i)
        {
            float logFreq = minLogFreq + logFreqRange * (static_cast<float>(i) / (numPoints - 1));
            logFreqBins.push_back(std::pow(10.0f, logFreq));
        }

        for (size_t i = 0; i < ap.scopeSize; ++i)
        {
            int fftDataIndex = static_cast<int>(logFreqBins[i] / frequencyResolution);
            fftDataIndex = std::min(fftDataIndex, ap.fftSize / 2 - 1);
            // 获取FFT bin的线性幅度值 (未转换为分贝)
            float magnitude = ap.fftData[fftDataIndex];
            // 计算并应用A-weighting调整
            float aWeightingFactor = std::pow(10.0f, aWeighting(logFreqBins[i]) / 20.0f);
            magnitude *= aWeightingFactor;

            // 将加权的线性幅度转换为分贝

            auto levelDb = juce::Decibels::gainToDecibels(magnitude * 0.0009f, mindB);
            // 映射到0-1范围
            auto level = juce::jmap(juce::jlimit(mindB, 0.0f, levelDb), mindB, 0.0f, 0.0f, 1.0f);
            // 将结果保存在平滑过的数据中
            smoothedScopeData[i] = previousFrame[i] * previousWeight + level * currentWeight;
        }
        previousFrame = smoothedScopeData; // 更新 previousFrame 为当前平滑后的数据
    }


    void AnalyzerComponent::drawFrame(juce::Graphics& g)
    {
        auto width = getBounds().getWidth();
        auto height = getBounds().getHeight();

        juce::Path spectrumPath;
        spectrumPath.startNewSubPath(0, height);

        // 插值以产生更平滑的曲线
        for (int i = 0; i < ap.scopeSize - 1; ++i)
        {
            float x1 = juce::jmap(i, 0, ap.scopeSize - 1, 0, width);
            float y1 = juce::jmap(smoothedScopeData[i], 0.0f, 1.0f, (float)height, 0.f);

            // 计算下一个点的位置
            float x2 = juce::jmap(i + 1, 0, ap.scopeSize - 1, 0, width);
            float y2 = juce::jmap(smoothedScopeData[static_cast<std::vector<float, std::allocator<float>>::size_type>(i) + 1], 0.0f, 1.0f, (float)height, 0.f);

            // 创建两点之间的二次Bezier曲线(或使用其他插值方法)
            spectrumPath.quadraticTo(x1, y1, (x1 + x2) * 0.5f, (y1 + y2) * 0.5f);
        }
        // 画出最后一段到最后一个点
        spectrumPath.lineTo(width, juce::jmap(smoothedScopeData[ap.scopeSize - 1], 0.0f, 1.0f, (float)height, 0.f));

        spectrumPath.lineTo(width, height);
        spectrumPath.closeSubPath();

        g.setColour(juce::Colours::red);
        g.setOpacity(0.3f);
        g.fillPath(spectrumPath);
    }



private:

    std::vector<float> smoothedScopeData;
    std::vector<float> previousFrame; // 用于存储上一帧的平滑数据
    SIGMusicFFTDemoAudioProcessor& ap;
    juce::dsp::FFT forwardFFT;
    juce::dsp::WindowingFunction<float> window;

    float previousWeight = 0.9f; // 上一帧的权重
    float currentWeight = 0.1f;  // 当前帧的权重

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AnalyzerComponent);
};


After drawing my spectrogram, I don’t know if there are rectangular lines in low-frequency areas. How do I apply linear interpolation or linear smoothing? I’m stumped

With a logarithmic frequency scale, you’ll have very low resolution at lower frequencies and so you’re getting less than one frequency per pixel.

int fftDataIndex = static_cast<int>(logFreqBins[i] / frequencyResolution);

This will likely be returning the same index several times in a row as you iterate ap.scopeSize since the cast will truncate.

An easy fix is to simply ignore cases where the FFT index is the same as the previous one:

int prevFftIndex = -1;

for (size_t i = 0; i < ap.scopeSize; ++i)
{
    // ...
    int fftDataIndex = static_cast<int>(logFreqBins[i] / frequencyResolution);
    fftDataIndex = std::min(fftDataIndex, ap.fftSize / 2 - 1);

    if (fftDataIndex == prevFftIndex)
        continue;

    prevFftIndex = fftDataIndex;
    // ...
}

It’s really better now! But the low-frequency is now too sparse :smiling_face_with_three_hearts: :smiling_face_with_tear:

You’ll need a way to not add the skipped points to the path. Maybe initialise them to -1 and only add the points to the path if they’re above 0.

it so hard…

help me master :smiling_face_with_three_hearts: