在现代互动设备或者节奏类应用中,如何精准地获取音乐的节拍成为一个关键问题。本文提供了一种基于频谱分析的实时鼓点检测方案,并给出完整的核心实现代码,适用于 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 游戏力反馈助手 项目的一部分,已开放源代码,基于宽松的开源协议发布,允许免费商用及二次开发,无需额外授权。