在现代互动设备或者节奏类应用中,如何精准地获取音乐的节拍成为一个关键问题。本文提供了一种基于频谱分析的实时鼓点检测方案,并给出完整的核心实现代码,适用于 C# 环境(使用 NAudio 库)。

基本原理:

采集音频样本;

通过 FFT 分析音频频谱能量分布;

重点提取低频能量(鼓点通常集中在低频);

与能量历史做对比判断是否为鼓点;

使用滑动平均方式预测节拍间隔,辅助未来的鼓点触发。

核心实现代码:

private Queue<double> beatIntervals = new Queue<double>();
private const int maxIntervalHistory = 10;
private DateTime predictedNextBeatTime = DateTime.MinValue;

// 初始化
public void StartAudioProcessing()
{
    if (isRunning) return;

    audioProcessor = new SystemAudioProcessor();
    audioProcessor.OnAudioSamplesAvailable += OnAudioSamples;
    audioProcessor.Start();

    isRunning = true;
    predictedNextBeatTime = DateTime.Now.AddMilliseconds(500); // 初始化预测时间
    Growl.Success("启动音乐律动");
}

// FFT 缓冲及结果数组
private readonly NAudio.Dsp.Complex[] fftBuffer = new NAudio.Dsp.Complex[1024];
private readonly double[] magnitudes = new double[240];
private readonly int[] bandCounts = new int[240];

// 音频数据回调处理
private void OnAudioSamples(float[] samples)
{
    const int fftLength = 1024;
    if (samples.Length < fftLength) return;

    // 填充 FFT 输入
    NAudio.Dsp.Complex[] fftBuffer = new NAudio.Dsp.Complex[fftLength];
    unsafe
    {
        fixed (float* samplePtr = samples)
        fixed (NAudio.Dsp.Complex* fft = fftBuffer)
        {
            for (int i = 0; i < fftLength; i++)
            {
                fft[i].X = samplePtr[i];
                fft[i].Y = 0;
            }
        }
    }

    // 进行快速傅立叶变换
    FastFourierTransform.FFT(true, 10, fftBuffer);

    // 分析频谱
    const double sampleRate = 48000.0;
    const double freqResolution = sampleRate / fftLength;
    const double bandWidth = 100.0;
    const int barCount = 240;
    const int lowFreqLimit = (int)(150 / bandWidth);

    double[] magnitudes = new double[barCount];
    int[] bandCounts = new int[barCount];

    unsafe
    {
        fixed (double* mags = magnitudes)
        fixed (int* bands = bandCounts)
        fixed (NAudio.Dsp.Complex* fft = fftBuffer)
        {
            for (int i = 0; i < barCount; i++) { mags[i] = 0; bands[i] = 0; }

            for (int i = 0; i < fftLength / 2; i++)
            {
                double freq = i * freqResolution;
                int bandIndex = (int)(freq / bandWidth);
                if (bandIndex >= barCount) break;

                float x = fft[i].X;
                float y = fft[i].Y;
                mags[bandIndex] += Math.Sqrt(x * x + y * y);
                bands[bandIndex]++;
            }
        }
    }

    // 平均化每个频段
    for (int i = 0; i < barCount; i++)
    {
        if (bandCounts[i] > 0)
            magnitudes[i] /= bandCounts[i];
    }

    // 低频能量
    double lowFreqEnergy = 0;
    for (int i = 0; i <= lowFreqLimit; i++)
        lowFreqEnergy += magnitudes[i];
    lowFreqEnergy /= (lowFreqLimit + 1);

    // 判断是否触发鼓点
    lowEnergyHistory.Enqueue(lowFreqEnergy);
    if (lowEnergyHistory.Count > energyHistorySize)
        lowEnergyHistory.Dequeue();

    double avgEnergy = lowEnergyHistory.Average();
    var now = DateTime.Now;

    double predictedInterval = GetAverageBeatInterval();
    if (predictedInterval > 200)
    {
        if (predictedNextBeatTime > DateTime.MinValue.AddMilliseconds(200))
        {
            var predictedTriggerTime = predictedNextBeatTime.AddMilliseconds(-200);
            if (now >= predictedTriggerTime && now - lastFeedbackTime > minFeedbackInterval)
            {
                predictedNextBeatTime = predictedNextBeatTime.AddMilliseconds(predictedInterval);
                TriggerBeatFeedback();//输出节拍,调用硬件
            }
        }
    }

    // 实际鼓点判断
    if ((lowFreqEnergy - avgEnergy) > avgEnergy * 0.2 &&
        (now - lastBeatTime) > beatInterval)
    {
        double interval = (now - lastBeatTime).TotalMilliseconds;
        beatIntervals.Enqueue(interval);
        if (beatIntervals.Count > maxIntervalHistory)
            beatIntervals.Dequeue();

        lastBeatTime = now;
        predictedNextBeatTime = now.AddMilliseconds(GetAverageBeatInterval());
        TriggerBeatFeedback();//输出节拍,调用硬件
    }
}

// 获取平均鼓点间隔
private double GetAverageBeatInterval()
{
    return beatIntervals.Count > 0 ? beatIntervals.Average() : 500.0;
}

总结

该方案实现了 “鼓点预测 + 实时触发” 的双重机制,兼顾响应速度和稳定性。适合需要精准律动感知的交互式设备、节奏灯光控制系统,或音乐可视化应用。

该代码为 GRFF 游戏力反馈助手 项目的一部分,已开放源代码,基于宽松的开源协议发布,允许免费商用及二次开发,无需额外授权。

最后修改:2025 年 04 月 22 日
你觉得有用就点个赞吧~