ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

音频节奏检测代码示例:谱通量法实现onset检测与BPM估计

音频节奏检测代码示例:谱通量法实现onset检测与BPM估计 简介这份音频节奏检测Onset Detection代码示例面向音频处理与音乐信息检索方向的开发者尤其适合希望从理论走向实践的入门与进阶学习者。资源以Java项目形式组织演示如何从音频信号中定位节拍起始点覆盖音频预处理、能量计算、STFT与MFCC特征提取、阈值判定、事件检测及后处理等关键环节并配有测试音频便于直接运行验证。压缩包共64个文件以java源码与class编译文件为主体辅以xml配置、mp3测试音频及项目工程文件整体约1009KB结构紧凑、便于导入IDE后按模块阅读。目前已有1599人学习下载。通过运行示例读者可理解节拍检测从采样、分帧到峰值判定的完整链路掌握阈值策略与平滑滤波的调参思路并可将方法迁移至音乐推荐、自动伴奏、舞蹈同步等实际场景。1. 音频节奏检测到底在检测什么从一段鼓点说起你手里有一段 30 秒的鼓 loop想让它跟着节拍自动切片、自动对齐、自动生成 MIDI 鼓谱第一步永远绕不开同一个问题这一下鼓到底打在了第几毫秒。音频节奏检测Onset Detection干的就是这件事——找出音频信号里能量或频谱发生突变的时间点也就是音符的起始位置。它不负责告诉你这是底鼓还是军鼓也不负责告诉你 BPM 是多少它只回答「这里有一个新的声音事件开始了」。很多人第一次搜「音频节奏检测 代码示例」脑子里想的其实是「给我一个能跑出 BPM 的脚本」。但真正落地时会发现BPM 估计和 onset 检测是两层东西onset 是底层事件BPM 是在 onset 序列上做周期推断。把 onset 做准了BPM、节拍对齐、切片全都顺onset 做歪了后面全是玄学。这篇就按一线做法把 onset detection 的代码示例拆开讲清楚谱通量怎么算、峰值怎么挑、参数怎么调、哪里最容易翻车。适合谁看做音乐信息检索MIR的、做自动混音/自动切片的、做节奏游戏的、做音频标注工具的。只要你的输入是波形输出需要「时间点」这篇就能直接抄作业。2. 谱通量法onset detection 代码示例里最稳的起点2.1 为什么先讲谱通量而不是直接上神经网络onset detection 的算法家族大致分三类时域能量法、频域谱通量法、数据驱动法CNN/CRNN。时域能量法实现最简单但对钢琴、弦乐这类缓起音soft onset几乎失效因为能量上升太慢阈值一卡就漏。数据驱动法精度高但你要有标注数据、要训练、要调超参对「我只想先跑通一个代码示例」的人来说门槛太高。谱通量Spectral Flux是中间那个甜点它把音频分帧做 STFT逐帧比较相邻帧的频谱差异差异大的地方就是 onset。它对缓起音比纯能量法敏感得多实现又只有几十行。我一般会用它作为 baseline先跑通、先看结果再决定要不要上模型。常见做法是谱通量 峰值拾取 后处理这套组合在鼓、拨弦、打击乐上已经够用。核心公式很直白对每一帧把当前帧的幅度谱和上一帧逐 bin 相减只保留正差值因为 onset 是能量增加再求和。这个和就是该帧的 onset 强度。整段音频算完你会得到一条「onset 强度曲线」接下来就是在这条曲线上找峰。2.2 用 librosa 跑通最小可复现示例下面这段是我常用的最小骨架依赖只有 librosa、numpy、soundfile。它做四件事读音频、算谱通量、拾峰、把结果打成时间戳。import numpy as np import librosa def spectral_flux_onsets(path, sr22050, hop512, n_fft2048, delta0.07): # 读音频统一单声道、统一采样率避免后续帧率对不上 y, sr librosa.load(path, srsr, monoTrue) # STFT得到复数谱取幅度 S np.abs(librosa.stft(y, n_fftn_fft, hop_lengthhop)) # 谱通量相邻帧差分只保留能量增加的部分 diff np.diff(S, axis1) diff[diff 0] 0.0 flux np.sum(diff, axis0) # 归一化方便统一设阈值 flux flux / (np.max(flux) 1e-9) # 峰值拾取局部极大 超过阈值 delta onset_frames librosa.util.peak_pick( flux, pre_max3, post_max3, pre_avg3, post_avg5, deltadelta, wait5 ) # 帧号转秒 onset_times librosa.frames_to_time(onset_frames, srsr, hop_lengthhop) return onset_times, flux if __name__ __main__: times, flux spectral_flux_onsets(drum_loop.wav) for t in times: print(f{t:.3f}s)逻辑说明librosa.stft把波形切成帧np.diff沿时间轴做相邻帧差分负值清零是因为我们只关心「新增能量」。peak_pick是 librosa 自带的峰值拾取器比手写scipy.signal.find_peaks多了一层局部平均约束抗噪更好。参数说明这几个是必须理解的hop512帧移。它直接决定时间分辨率。sr22050 时512/22050 ≈ 23ms也就是两个 onset 靠得比 23ms 近就分不开。想更细就降到 256但计算量和误检都会上升。n_fft2048FFT 窗长。窗越长频率分辨率越高、时间分辨率越差。2048 在 22050 采样率下约 93ms 窗是打击乐的常用折中。delta0.07峰值阈值。这是最需要按素材调的参数后面避坑章会专门讲。wait5两个峰之间至少隔 5 帧防止一个 onset 被拾成好几个。2.3 帧号、秒、采样点三者别搞混新手最容易翻车的地方是单位。peak_pick返回的是帧索引frames_to_time才转成秒。如果你后面要跟采样点对齐比如做切片还得再乘 sr。我见过有人直接把帧号当采样点用结果切片位置差了 20 多倍还以为是算法不准。记住这条链帧号 → 秒除以 sr 乘 hop→ 采样点乘 sr。三个单位在代码里最好用变量名区分开比如onset_frames、onset_times、onset_samples别全叫onsets。3. 参数怎么调把 onset 检测从「能跑」调到「能用」3.1 阈值 delta 与 wait 的联动关系delta和wait不是独立的。delta决定「多强才算一个 onset」wait决定「两个 onset 至少隔多远」。如果你把delta调低想多检出一些弱 onset同时wait又设得大那些靠得近的弱 onset 会被wait吃掉你会觉得「调低阈值怎么反而没多出来」。正确做法是先固定wait到一个符合音乐常识的值再单独扫delta。wait怎么定看你的素材最快节奏。160 BPM 的十六分音符间隔约 94mshop512 时约 4 帧。所以wait至少不能大于这个数否则快速音符会被合并。我一般从 3 到 5 起步密集素材降到 2。delta怎么定先跑一遍看flux的分布。如果flux大部分值都在 0.05 以下偶尔冲到 0.3那delta0.07可能偏低会拾到一堆噪声如果flux普遍在 0.2 以上delta0.07又太松。稳妥做法是取flux的某个分位数比如 90 分位作为初始delta再人工听几个点微调。# 用分位数自动给一个 delta 初值避免拍脑袋 delta_init np.percentile(flux, 90) print(suggested delta:, delta_init)3.2 预加重、对数压缩与归一化三个提升检出率的预处理原始谱通量对低频和高频的敏感度不一样直接算容易在低频轰隆处误检。三个常见预处理能明显改善第一预加重pre-emphasis。在 STFT 前对波形做一阶高通补偿高频衰减。librosa 里可以手动实现y np.append(y[0], y[1:] - 0.97 * y[:-1])。0.97 是常用系数别乱改。第二对数压缩。把幅度谱取 log 再算通量能压缩动态范围让弱 onset 不被强 onset 淹没。做法是S np.log1p(S)。注意是log1p不是log避免 log(0)。第三归一化。前面代码里flux / max(flux)是全局归一化。如果音频动态变化大比如前段安静后段爆全局归一化会让安静段全被压没。这时改用局部归一化对flux做滑动窗口减均值除标准差。# 局部归一化滑动窗口 z-score窗口约 1 秒 win int(1.0 * sr / hop) kernel np.ones(win) / win local_mean np.convolve(flux, kernel, modesame) local_std np.sqrt(np.convolve((flux - local_mean) ** 2, kernel, modesame)) 1e-9 flux_norm (flux - local_mean) / local_std这三个预处理叠加后同一段鼓 loop 的检出率通常能从「漏一半」提到「基本齐」。代价是参数变多所以建议一次只加一个听效果再决定留不留。3.3 用 mir_eval 量化评估别只靠耳朵耳朵听只能判断「大概齐」要对比两套参数谁更好得有数字。MIR 领域标准评估工具是mir_eval它按 F1、精确率、召回率算 onset 匹配允许一个时间容差窗口默认 50ms。import mir_eval # ref_times 是人工标注的 onset 时间est_times 是算法输出 f1, prec, rec mir_eval.onset.f_measure(ref_times, est_times, window0.05) print(fF1{f1:.3f} P{prec:.3f} R{rec:.3f})window0.05表示 50ms 内算命中。打击乐可以收紧到 0.03钢琴这种缓起音放宽到 0.1 更合理。没有标注数据怎么办可以拿一段自己手动点几个 onset 当 mini 验证集哪怕只有 20 个点也比纯拍脑袋强。这一步是很多人跳过的但恰恰是「能跑」和「能用」的分界线。4. 避坑与排查onset detection 代码示例里没人告诉你的五件事4.1 现象安静段疯狂误检爆音段反而漏检原因用了全局归一化安静段的微小波动被放大到和爆音段同一量级阈值相对就失效了。解决换成 3.2 里的局部归一化或者对flux做动态阈值——每个点跟它前后一段窗口的均值比超过均值加若干倍标准差才算峰。librosa 的peak_pick里的pre_avg/post_avg就是这个思路的简化版把这两个值调大能缓解。4.2 现象一个鼓点被检出两三个 onset原因wait太小或者 STFT 窗太长导致一个瞬态在相邻几帧都有能量上升。解决先把wait提到 4 到 5如果还重复检查n_fft是不是过大降到 1024 试试。另外可以在峰值拾取后加一步「合并 30ms 内的相邻峰」取强度最大的那个。4.3 现象换了采样率结果全变原因hop和n_fft是按采样点算的采样率一变同样的hop对应的时间就变了。22050 下 hop512 是 23ms44100 下就变成 11.6ms。解决要么统一重采样到固定 sr要么按时间反推 hophop int(sr * 0.01)表示固定 10ms 帧移。我一般选前者统一到 22050省心。4.4 现象钢琴、弦乐几乎检不出 onset原因谱通量假设 onset 是能量突增但缓起音的能量是慢慢爬上去的相邻帧差异很小。解决这类素材要换思路。一是用对数谱通量log 压缩后再差分二是改用「相位偏差」特征三是直接上 CRNN 模型。如果只是偶尔处理几段最省事的是把delta降到很低接受一些误检再靠后处理筛。4.5 现象代码跑得通但结果和 Audacity 标注对不上原因Audacity 等工具标注的是听觉上的「起音感知点」可能比物理能量突变晚几十毫秒这是心理声学现象不是 bug。解决评估时把window放宽到 0.05 到 0.1别用 0.01 去卡。如果下游任务对时间敏感比如切片以物理 onset 为准再统一加一个固定偏移补偿。5. 从 onset 到节拍把时间戳变成能用的节奏信息拿到 onset 时间戳只是第一步。真正做节奏相关的事你还需要把 onset 序列变成 BPM 和节拍网格。这里给一个我常用的轻量做法对 onset 时间戳做自相关或互相关找周期。def estimate_bpm(onset_times, bpm_min60, bpm_max180): # 把 onset 转成脉冲序列采样率 100Hz 够用 fs 100.0 t_max onset_times[-1] 1.0 n int(t_max * fs) pulse np.zeros(n) for t in onset_times: idx int(t * fs) if idx n: pulse[idx] 1.0 # 自相关 ac np.correlate(pulse, pulse, modefull)[n-1:] ac ac / (ac[0] 1e-9) # 在 BPM 范围内找最大峰 lag_min int(fs * 60.0 / bpm_max) lag_max int(fs * 60.0 / bpm_min) lag lag_min np.argmax(ac[lag_min:lag_max]) return 60.0 * fs / lag逻辑说明把 onset 打成脉冲序列自相关在「周期整数倍」处出现峰第一个落在合理 BPM 范围内的峰对应的 lag 就是拍周期。fs100是脉冲序列的采样率跟音频采样率无关只是给自相关一个时间网格。参数说明bpm_min/bpm_max是搜索范围电子舞曲可以设 100 到 180古典可以设 60 到 120。范围设太宽容易锁到倍频或半频这是自相关法的经典问题——它分不清 120 BPM 和 60 BPM因为两者周期是整数倍关系。解决办法是结合 onset 强度加权或者用 comb filter 代替纯自相关。验证方法把估计出的 BPM 和节拍网格画在波形上肉眼对几个小节。如果网格整体偏移但间距对那是相位问题调一下起始偏移即可如果间距都不对多半是锁到了倍频把bpm_max收窄再试。一个具体技巧onset 检测的输出不要只存时间戳把每个 onset 的flux强度一起存下来。做 BPM 估计时用强度加权自相关强 onset 权重高能显著减少弱 onset 带来的周期抖动。这个改动只有几行但在我处理过的鼓 loop 上BPM 估计的稳定性提升很明显。我自己踩过最深的坑是早期太相信「算法输出即真理」不做人工抽检就往下游灌。后来养成习惯任何 onset 结果先随机抽 10 个点在 Audacity 里对一遍确认没有系统性偏移再继续。这个习惯帮我省下的返工时间远比调参本身多。希望帮到你。本文还有配套的精品资源点击获取
返回列表