ARTICLE DETAIL

资讯详情

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

用Python和Librosa分析古典音乐录音:波形、频谱与Chroma特征实战

用Python和Librosa分析古典音乐录音:波形、频谱与Chroma特征实战 上次在整理一批古典音乐录音文件时我注意到一类非常典型的命名方式作曲家_作品名_作品编号_演奏者。比如本文要展开的H.Busser___Petite Suite, Op.12 (Leonard Garrison)一眼看过去像唱片目录但从技术角度来看它其实是一个非常适合用来做“音乐信息检索MIRMusic Information Retrieval”实验的原始素材。这篇文章不是音乐鉴赏课而是从工程视角出发演示如何用 Python 对一部古典小型组曲的录音做音频特征提取。我们会用到librosa、matplotlib、numpy等常用库把一首长笛与钢琴作品拆成波形、频谱、chroma 特征、节拍和能量曲线从而理解“计算机是怎么听音乐的”。如果你正在接触音频处理、音乐科技、数字人文或者只是好奇如何用代码分析古典音乐录音这篇教程会比较适合你。全程以可复现的代码为主不需要太深的乐理背景跟着步骤走就能跑出结果。1. 认识 H.Busser《Petite Suite, Op.12》一个很好的音频分析样本1.1 从文件名构成开始理解先看文件名H.Busser___Petite Suite, Op.12 (Leonard Garrison)。这里面的H.Busser指向法国作曲家 H. Busser通常写作 Henri Busser。他在 19 世纪末到 20 世纪中叶活跃创作过歌剧、芭蕾、管弦乐和不少室内乐作品。Petite Suite是“小型组曲”的意思Op.12是作品编号。Leonard Garrison则通常与长笛演奏录音相关。从数据分析的角度这个命名本身就有价值作曲家、曲名、编号、演奏者被压缩在一个字符串里天然适合做“曲目信息解析”的练习。不过本文不打算写一个解析文件名的正则表达式而是把重点放在“拿到一段音频之后能提取出哪些可以用来描述音乐的特征”。需要说明的是我并不打算在这里给出关于这部作品具体乐章的考据因为不同版本、不同编曲下的录音差异可能很大。我们真正关心的是在给定一部长约几分钟的古典室内乐录音后如何通过程序自动分析它的声音属性。1.2 为什么用古典小型组曲做音频分析第一它的时长通常不会太长。相比整部交响曲或整幕歌剧一首小型组曲的长度更适合在本地做快速实验不会因为音频文件过大而频繁等待。第二室内乐编制的声部相对清晰。长笛与钢琴的组合中长笛的频段集中在 200Hz 到 4kHz 左右钢琴覆盖范围更宽。分析这类音频时频谱里往往可以看到比较清晰的乐器能量分布便于我们理解频谱图与音色之间的关系。第三古典作品有相对明确的节拍、句读和段落边界。虽然机器不能像人一样理解“乐思”但它可以通过能量变化、频谱质心变化、节拍周期性等特征把一首音乐切分成不同的结构片段。这对做音乐推荐、自动标注、教学辅助系统都有实用价值。1.3 本文技术方案概览整体流程可以分为四步准备环境与项目目录加载音频并检查基本信息提取波形、频谱、chroma 特征与节拍保存分析结果并讨论常见问题。核心工具是librosa它是 Python 生态里最常用的音频特征库之一。搭配soundfile处理音频解码使用matplotlib做可视化。为了让结果方便查看我们会把特征数据保存成 CSV把图片保存到独立目录。2. 环境准备与项目结构2.1 Python 环境与工具链开始之前请确认你的电脑上已经安装了 Python 3。我建议使用 3.9 或更高的版本新版本对类型注解和第三方库的支持更稳定。如果你没有装 Python建议先去官网安装或者在本地使用 Anaconda 管理环境。这一步不是必须使用 Docker。直接在虚拟环境里安装依赖即可。在 Windows、macOS 和 Linux 上后面的命令基本通用只有激活虚拟环境的命令略有差异。为了不让第三方包污染系统 Python建议先创建一个独立的虚拟环境python -m venv venvLinux/macOS 激活source venv/bin/activateWindows 激活venv\Scripts\activate2.2 安装依赖库本文要用到的核心库包括librosa音频加载、特征提取soundfile音频文件读写numpy数值计算matplotlib绘图pandas整理特征并导出 CSVscipy部分高级计算依赖。安装命令如下pip install librosa soundfile numpy matplotlib pandas scipy如果在 Windows 上遇到音频解码报错通常还要安装或确认ffmpeg是否存在。ffmpeg是一个开源的音视频处理工具不是 Python 包。有些系统自带的 libsndfile 不支持 mp3需要额外用 ffmpeg 转码。你可以用下面的命令确认是否能正常读取 flac 或 wav 文件python -c import soundfile; print(soundfile.available_formats())如果输出里包含WAV、FLAC等常见格式说明音频底层库已经可用。2.3 项目目录与输入音频准备建议先建立如下结构避免到后面文件混乱busser_petite_suite_analysis/ ├── data/ │ ├── raw/ │ │ └── busser_petite_suite_op12.flac │ └── processed/ ├── output/ │ ├── figures/ │ └── busser_feature_summary.csv ├── src/ │ ├── 01_load_audio.py │ ├── 02_visualize.py │ └── 03_features.py └── requirements.txt你可以把任意一段自己合法获取的古典音频命名为busser_petite_suite_op12.flac放入data/raw目录。如果你只有 mp3可以先转成 wav 或 flac不建议直接用 mp3 做精细频谱分析因为压缩编码会损失一部分高频细节。如果你暂时没有该录音也可以换成任何测试音频只需同步修改脚本中的文件路径即可。本文代码演示的重点是流程不是某一段特定音频。3. 核心概念拆解计算机如何“听懂”音乐3.1 音频在 Python 中的表示方式音频本质上是空气振动的采样记录。当我们用librosa.load()读取一段音频后得到的是一个一维数组y和一个采样率sr。y中每个数值代表某个采样时刻的振幅sr表示每秒钟采样多少个点。例如sr22050表示每秒有 22050 个采样点。CD 音质通常是 44100Hz但为了计算提速音频特征分析中常统一降到 22050Hz。人耳能听到的频率大约在 20Hz 到 20000Hz 之间22050Hz 的采样率已经足以覆盖音频分析的大部分场景。采样率不是越高越好。采样率越高数据量越大后续 STFT短时傅里叶变换和特征矩阵的计算就越慢。对特征提取来说22050Hz 是一个效率与信息量都比较均衡的默认值。3.2 频谱与短时傅里叶变换光看波形很难判断音高和音色所以需要把时间域信号转换成频率域信息。短时傅里叶变换STFT会把音频切成很多小帧对每一帧做傅里叶变换从而得到“时间 - 频率 - 能量”的三维分布。librosa.stft()会返回一个复数矩阵通常我们用amplitude_to_db()把幅度转换成以分贝为单位的数值再通过specshow()绘制成频谱图。频谱图横轴是时间纵轴是频率颜色深浅代表能量高低。古典音乐里钢琴的高频泛音、长笛的气息声都会在频谱图中留下不同形态的痕迹。这种可视化非常直观也是入门音频分析时最先该做的步骤之一。3.3 chroma 特征更像音乐视角的和声特征纯频谱只是描述物理声音和“音高类别”还有距离。比如 C 大调主和弦和 A 小调主和弦在频谱上都很复杂但人类会把它抽象成不同的音名。为了模拟这种抽象出现了 chroma 特征。chroma 特征把八度合并到一起只保留 12 个半音类别也就是 C、C#、D、D#、E、F、F#、G、G#、A、A#、B。无论音区是高还是低只要属于同一个音名类别就会被归入同一维。librosa.feature.chroma_cqt()基于 CQT常 Q 变换计算 chroma 特征比传统的 chroma_stft 更适合音乐分析因为它对音高的分辨率更好。得到的结果是一个12 x N矩阵其中N是帧数。每一列代表某一帧中 12 个音级上的能量分布。3.4 频谱质心、RMS 与节拍频谱质心描述的是声音的“亮度”。一段明亮、乐器穿透力强的音频频谱质心一般偏高。长笛在高音区时频谱质心往往明显上升。这个单一数值虽然不能完全代表音色但可以用于判断段落之间是否存在对比。RMS均方根能量则反映音量大小。结合频谱质心可以大致观察音乐从哪里开始变强、哪里进入安静段落、哪里可能产生明显的分段边界。节拍跟踪是另一个常用功能。librosa.beat.beat_track()会尝试估计音频的 BPM并返回检测到的节拍帧位置。古典音乐常有变速、渐慢、华彩段落自动节拍检测不会像流行音乐那么稳定但这个输出仍然可以作为参考帮助我们大致切分出小节位置。4. 完整实战案例上加载音频并观察波形与频谱4.1 音频加载与基础信息新建文件src/01_load_audio.py写入下面的代码。这段代码会让音频加载到一个普通 numpy 数组里并输出基础信息。# 文件路径src/01_load_audio.py import librosa import numpy as np AUDIO_PATH data/raw/busser_petite_suite_op12.flac y, sr librosa.load(AUDIO_PATH, sr22050, monoTrue) duration len(y) / sr print(f采样率: {sr} Hz) print(f时长: {duration:.2f} 秒) print(f样本点数: {y.shape[0]}) print(f峰值: {np.max(np.abs(y)):.4f}) print(f整段RMS: {float(np.sqrt(np.mean(y ** 2))):.4f})运行python src/01_load_audio.py预期会得到类似这样的输出采样率: 22050 Hz 时长: 312.45 秒 样本点数: 6894162 峰值: 0.9845 整段RMS: 0.0612为什么把采样率设为 22050因为对大多数音乐特征提取来说人耳主要敏感频段都能保留下来计算开销却只有 44100Hz 的四分之一。monoTrue会把双声道混合成单声道避免左右声道内容不一致导致特征计算偏差。4.2 波形与频谱可视化接着新建src/02_visualize.py绘制波形和频谱图。# 文件路径src/02_visualize.py import os import librosa import librosa.display import matplotlib.pyplot as plt import numpy as np AUDIO_PATH data/raw/busser_petite_suite_op12.flac OUTPUT_DIR output/figures os.makedirs(OUTPUT_DIR, exist_okTrue) y, sr librosa.load(AUDIO_PATH, sr22050, monoTrue) hop_length 512 D librosa.stft(y, n_fft2048, hop_lengthhop_length) S_db librosa.amplitude_to_db(np.abs(D), refnp.max) plt.figure(figsize(14, 6)) librosa.display.specshow( S_db, srsr, hop_lengthhop_length, x_axistime, y_axislog, ) plt.colorbar(format%2.0f dB) plt.title(Busser Petite Suite Op.12 - Spectrogram) plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, spectrogram.png), dpi150) print(图片已保存:, os.path.join(OUTPUT_DIR, spectrogram.png))运行python src/02_visualize.py这会在output/figures下生成一张频谱图。频谱图的纵轴使用对数频率刻度这样更接近人耳对音高的感知。低频部分位于图像下方高频泛音位于上方。观察时你可以重点关注几个信息横向深色区块之间的空白通常对应乐句之间的呼吸或停顿颜色明亮的横线代表持续存在的音高可能是长笛的长音也可能是钢琴的持续和弦高频区域的明暗变化能反映出演奏者音色的统一性。4.3 进一步观察波形与频谱组合图为了把时间信息整合得更完整可以把波形和频谱画在同一张图上。这个步骤不是必需的但更适合后续做封面图或汇报材料。# 文件路径src/02_visualize_waveform.py import os import librosa import librosa.display import matplotlib.pyplot as plt import numpy as np AUDIO_PATH data/raw/busser_petite_suite_op12.flac OUTPUT_DIR output/figures os.makedirs(OUTPUT_DIR, exist_okTrue) y, sr librosa.load(AUDIO_PATH, sr22050, monoTrue) hop_length 512 D librosa.stft(y, n_fft2048, hop_lengthhop_length) S_db librosa.amplitude_to_db(np.abs(D), refnp.max) fig, ax plt.subplots(2, 1, figsize(14, 8)) librosa.display.waveshow(y, srsr, axax[0]) ax[0].set(titleWaveform, xlabelTime (s), ylabelAmplitude) img librosa.display.specshow( S_db, srsr, hop_lengthhop_length, x_axistime, y_axislog, axax[1], ) ax[1].set(titleSpectrogram, xlabelTime (s), ylabelFrequency (Hz)) fig.colorbar(img, axax[1], format%2.0f dB) plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, waveform_and_spectrogram.png), dpi150) print(组合图已保存:, os.path.join(OUTPUT_DIR, waveform_and_spectrogram.png))5. 完整实战案例下提取和声、节拍与分段特征5.1 提取 chroma 特征与音高能量分布下方代码计算 chroma_cqt 特征并输出全曲能量最突出的几个音级。注意这并不等同于判断调性只是表示哪些音级在整段时间里出现得更频繁、能量更强。# 文件路径src/03_features.py import os import librosa import numpy as np import pandas as pd AUDIO_PATH data/raw/busser_petite_suite_op12.flac OUTPUT_DIR output os.makedirs(OUTPUT_DIR, exist_okTrue) HOP_LENGTH 512 y, sr librosa.load(AUDIO_PATH, sr22050, monoTrue) chroma librosa.feature.chroma_cqt(yy, srsr, hop_lengthHOP_LENGTH) mean_chroma np.mean(chroma, axis1) note_names [C, C#, D, D#, E, F, F#, G, G#, A, A#, B] top_idx np.argsort(mean_chroma)[::-1][:3] print(全曲能量较高的音级) for i in top_idx: print(f {note_names[i]}: {mean_chroma[i]:.3f}) tempo, beats librosa.beat.beat_track(yy, srsr, hop_lengthHOP_LENGTH) tempo_value float(np.atleast_1d(tempo)[0]) print(f\n估计BPM: {tempo_value:.2f}) print(f检测到节拍数量: {len(beats)}) beat_times librosa.frames_to_time(beats, srsr, hop_lengthHOP_LENGTH) print(前8个节拍时间(秒):, [round(t, 2) for t in beat_times[:8]]) cent librosa.feature.spectral_centroid(yy, srsr, hop_lengthHOP_LENGTH)[0] rms librosa.feature.rms(yy, hop_lengthHOP_LENGTH)[0] times librosa.frames_to_time( np.arange(len(cent)), srsr, hop_lengthHOP_LENGTH, ) summary_df pd.DataFrame({ time_sec: times, spectral_centroid_hz: cent, rms: rms, }) csv_path os.path.join(OUTPUT_DIR, busser_feature_summary.csv) summary_df.to_csv(csv_path, indexFalse) print(\n特征摘要已保存:, csv_path)运行脚本python src/03_features.py输出大致如下全曲能量较高的音级 D: 0.332 A: 0.287 F#: 0.251 估计BPM: 96.32 检测到节拍数量: 484 前8个节拍时间(秒): [0.98, 1.95, 2.91, 3.87, ...] 特征摘要已保存: output/busser_feature_summary.csv这里解释一下代码逻辑np.mean(chroma, axis1)把时间帧方向求平均得到每个音级的平均能量argsort()[::-1][:3]拿到能量最高的前三个音级下标beat_track返回的tempo在不同librosa版本里可能是标量或数组因此用np.atleast_1d把结果统一成数组再取第一个元素frames_to_time将帧号转换为时间坐标单位是秒方便后续查看某个节拍落在音频第几秒。5.2 将 chroma 特征保存为图片CSV 数据适合继续做统计但如果你想直观看到和声轮廓随时间的变化仍建议把 chroma 画成热力图。# 文件路径src/04_plot_chroma.py import os import librosa import librosa.display import matplotlib.pyplot as plt AUDIO_PATH data/raw/busser_petite_suite_op12.flac OUTPUT_DIR output/figures os.makedirs(OUTPUT_DIR, exist_okTrue) HOP_LENGTH 512 y, sr librosa.load(AUDIO_PATH, sr22050, monoTrue) chroma librosa.feature.chroma_cqt(yy, srsr, hop_lengthHOP_LENGTH) plt.figure(figsize(14, 6)) librosa.display.specshow( chroma, srsr, hop_lengthHOP_LENGTH, x_axistime, y_axischroma, ) plt.colorbar() plt.title(Chroma Features - Busser Petite Suite Op.12) plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, chroma.png), dpi150) print(chroma 特征图已保存:, os.path.join(OUTPUT_DIR, chroma.png))运行后查看chroma.png。横向是与音频时长一致的时间轴纵向是 12 个音级。颜色偏亮的位置表示该时间点对应的音级能量较高。你会发现在旋律变化、和声转换时色块会发生明显的亮暗切换。对没有乐谱的人来说这提供了一种“用图像理解音乐走向”的方式。5.3 简单分段用 RMS 和谱质心观察结构很多古典作品虽然乐章内部有复杂变化但整体音量轮廓通常有规律。利用上一步保存的 CSV我们可以简单画出 RMS 和频谱质心曲线从而大致推测安静段落、高潮段落都在什么时间点。可以用 pandas 做一次快速统计# 文件路径src/05_summary_stats.py import pandas as pd df pd.read_csv(output/busser_feature_summary.csv) print(时长范围, df[time_sec].min(), ~, df[time_sec].max(), 秒) print(RMS 最高的 5 个时间点) print(df.sort_values(rms, ascendingFalse).head()[[time_sec, rms]]) print(\n频谱质心最高的 5 个时间点) print(df.sort_values(spectral_centroid_hz, ascendingFalse).head()[[time_sec, spectral_centroid_hz]])这段代码并不会直接给出“第几章第几段”但通过观察排序后的时间点可以知道哪些位置是全曲音量最大、音色最亮的瞬间。之后再回到频谱图中定位这些时间点往往能找到段落边界所在。6. 常见问题与排查思路在实际运行过程中比较容易遇到的坑集中在音频解码、依赖版本、路径和绘图这几个方面。下面整理了一份排查表问题现象常见原因解决思路librosa.load无法读取文件缺少解码库或文件本身为加密格式安装 ffmpeg 或先用格式转换工具转成 wav/flac读取后听到明显杂音或刺耳高频音频为 mp3 且有损压缩或采样率被二次转换优先使用原始 wav/flac避免多次重采样chroma_cqt计算非常慢音频过长或没有设置合理的 hop_length先降低采样率或按段落截取片段绘制频谱图时坐标轴单位为帧而不是秒没有把sr和hop_length传给specshow检查librosa.display.specshow参数是否完整beat_track返回的 tempo 是数组导致格式化报错不同librosa版本返回值类型不同使用float(np.atleast_1d(tempo)[0])兼容处理中文字体在图中显示为方框系统缺少中文字体或 matplotlib 未配置改用英文标题或设置plt.rcParams[font.sans-serif]分析整首组曲后内存占用过高chroma 特征矩阵过大把长音频切分成多个短段分别处理pandas 读取 CSV 后特征曲线锯齿明显hop_length 过小导致帧数太多适当增大 hop_length例如 1024 或 2048如果出现No such file or directory优先检查脚本运行时所在的终端目录是否在项目根目录。建议所有脚本都从busser_petite_suite_analysis根目录执行而不是直接在src目录下执行。相对路径只有在当前工作目录正确时才能解析。如果希望进一步定位某个特征异常时间点可以结合时间坐标回听音频。这个步骤往往比调参数更有效因为机器特征只能提供线索最终判断需要你用自己的耳朵去验证。7. 最佳实践与工程建议7.1 项目目录与命名规范音频分析项目的目录最好不要只有一个 Jupyter Notebook 文件。把原始音频、中间特征、可视化图片和核心脚本分开管理能显著提高后期回溯效率。一个比较适合小型研究项目结构如下audio-analysis/ ├── config/ │ └── analysis.yaml ├── data/ │ ├── raw/ │ ├── processed/ │ └── external/ ├── output/ │ ├── figures/ │ └── tables/ ├── src/ │ ├── features/ │ ├── models/ │ └── visualization/ └── tests/如果你只是做一次性脚本至少也应该保持data/raw与output分离。原始音频是只读资产不要用程序覆盖它所有生成结果都应该放在output中方便随时删除重建。7.2 保证实验可复现音频处理库更新比较频繁librosa的某些函数签名和返回值类型在几个大版本之间可能发生变化。为了让实验可复现建议在项目根目录生成一份依赖清单pip freeze requirements.txt甚至在正式记录实验时把 Python 版本和librosa.__version__也写进结果文件。很多“上次能跑这次不能跑”的问题都是依赖版本变化引起的。参数设置也需要统一。sr22050、hop_length512、n_fft2048在多个脚本里重复出现时建议放在同一个配置文件中或至少放在脚本顶部的常量区。如果在 Jupyter 里经常修改参数也建议最后把固定参数回写到脚本或配置文件中否则很难回溯真正有效的参数。7.3 面向版权与数据安全的合规意识这一点很重要。不要随意下载、传播未经授权的商业录音。本文演示中的《Petite Suite, Op.12》虽然从作曲年代来看可能已经进入公有领域但具体录音版本通常仍然保留录音版权。宁可自行演奏录制或者使用获得明确授权的音频也不要去解析来源不明的文件。如果需要处理大量音频数据不要在公共网络环境中随意共享原始文件。特征数据和频谱图通常不涉及原始内容但也要根据项目方规定决定是否可以公开。如果是在企业内做音乐推荐或音频审核系统应优先使用公司已授权的版权库并遵守最小权限原则分配数据访问权限。7.4 不要过度解读自动分析结果自动节拍检测对古典音乐并不总是准确。古典作品的弹性速度rubato会给节拍跟踪带来很大挑战。当你看到某个 BPM 数值时不要立刻认为这就是整首作品的速度。BPM 只是一个统计意义上的估计值更合理的做法是按段落输出速度曲线而不是给出单一数值。同样的chroma 平均能量高不代表音乐就在该调上只是说明某些音级累计出现的能量更多。要学会区分“声学特征”和“音乐理论结论”之间的差距。技术工具能提供证据但解释权仍然在人和乐理知识手里。8. 后续可以继续做什么8.1 向 MIDI 与 MusicXML 方向扩展音频特征提取只是单条技术路线。如果你还想在乐谱层面做内容分析可以引入music21库。它能解析 MusicXML、MIDI 等符号化音乐格式还可以在 Python 中直接创建音符、和弦、谱表。例如从乐谱文件入手你能得到准确的音高序列和时值不需要再做节拍估计。但对于一些没有乐谱或没有 MIDI 的珍贵历史录音音频分析依然是唯一可行的方式。8.2 与乐谱做自动对齐当你同时拥有音频和 MusicXML 乐谱时可以尝试做 audio-to-score alignment。这个方向属于音乐信息检索的前沿问题目标是让计算机知道“乐谱上的某个音符大致对应录音中的第几秒”。听起来很复杂但可以从单旋律片段做起逐步引入钢琴或长笛录音。实现这类系统通常要结合 chroma 特征、动态时间规整DTW和隐马尔可夫模型学习曲线比较陡峭。如果你刚接触这个领域建议先用本文的方式把音频特征跑通再思考如何做对齐。8.3 从单曲扩展为数据集当你的分析对象从一首《Petite Suite, Op.12》扩展到几十部作品时就需要考虑批量处理。这时可以写一个简单的 Python 脚本扫描整个目录逐首提取特征并把结果汇总到一个大的 CSV 或 Parquet 文件中。后续就能做聚类分析看哪些作曲家或演奏风格在音色特征上更接近。分析古典音乐录音是一件很有趣的交叉工作它同时涉及信号处理、音乐学和软件开发。先把音频加载、频谱可视化、chroma 特征和节拍跟踪这套链路跑通再把目光放到更复杂的任务上后面会越走越顺。希望这份实战笔记能让你在处理第一部古典音乐录音时少踩一些坑。
返回列表