把乐器音频转成可计算、可解释的「动态音色空间」:提取声学特征 → 可视化音色轨迹 → 在主观感知维度上做相关性验证 → 归档案档。
本项目由 3 个脚本重构而来:
| 原始文件 | 重构后的位置 |
|---|---|
project2.py |
timbre_space/(config / audio / features / trajectory / users / storage / pipeline / ui)+ app.py |
visualize copy 2.py |
其独有的完整特征集(带宽 / 滚降 / 对比度 / 平坦度 / pyin 基频 / hpss 谐波分离)并入 timbre_space/features.py、audio.py |
relitu3.py |
timbre_space/perception.py + scripts/plot_perception_heatmap.py |
timbre_app/
├── app.py # Web 界面入口
├── requirements.txt
├── timbre_space/ # 主包
│ ├── config.py # 配置:路径 / 音频 / 绘图参数
│ ├── fonts.py # 跨平台中英文字体解析
│ ├── audio.py # 音频加载、预处理、波形与频谱图
│ ├── features.py # 全局汇总特征 + 逐帧特征
│ ├── trajectory.py # 轨迹样条平滑 + 3D/2D 可视化
│ ├── perception.py # 感知相关性热力图
│ ├── users.py # 用户注册与登录
│ ├── storage.py # 历史任务归档
│ ├── pipeline.py # 流程调度(界面与 CLI 共用,不依赖 gradio)
│ └── ui.py # Gradio 界面
├── scripts/
│ ├── plot_perception_heatmap.py # 出热力图(relitu3.py 的 CLI 版)
│ └── analyze_audio.py # 批量提特征 + 出轨迹图
├── tests/
│ ├── test_smoke.py # 离线自检(不需要 librosa)
│ └── test_pipeline_e2e.py # 端到端(需要真实音频)
├── data/samples/ # 示例数据
│ ├── 感知相关性.csv
│ └── 感知不相似度矩阵.xlsx
└── runtime/ # 运行产物(自动创建,已 gitignore)
├── user_data/ # users.json + 每用户目录
├── history/ # history.json + 每任务结果目录
└── temp/ # 中间文件与可下载 CSV
cd timbre_app
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txt依赖:librosa / numpy / pandas / scipy / matplotlib / seaborn / openpyxl / gradio。
界面层对 Gradio 5 与 6 都做了兼容(Gradio 6 把 css、theme 从 Blocks() 移到了
launch(),Dataframe 的 height 改成了 max_height,timbre_space/ui.py 里已自动探测)。
python tests/test_smoke.py # 离线:字体 / 热力图 / 轨迹 / 用户 / 归档
python tests/test_pipeline_e2e.py # 端到端:需要 librosa + 一个真实音频test_pipeline_e2e.py 默认从项目上一级的乐器库里自动挑一个音频,也可以指定:
set TIMBRE_TEST_AUDIO=D:\some\place\sample.wav
python tests/test_pipeline_e2e.pypython app.py # 默认 127.0.0.1:7860,自动开浏览器
python app.py --port 8000
python app.py --host 0.0.0.0 --share # 生成临时公网链接
python app.py --runtime-dir D:/data/timbre_runtime界面分 5 页:用户管理 → 音频上传 → 特征提取 → 动态可视化 → 历史记录。
分析产物会落到 runtime/history/<任务ID>/,包含波形频谱图、3D/2D 轨迹图、
逐帧特征 CSV、全局特征 CSV。
python scripts/plot_perception_heatmap.py
python scripts/plot_perception_heatmap.py \
--input data/samples/感知相关性.csv \
--output 图/感知相关性_热力图.png
python scripts/plot_perception_heatmap.py \
--input data/samples/感知不相似度矩阵.xlsx默认按论文级排版:宋体中文 + Times New Roman 西文(与原 relitu3.py 一致),600 dpi。
配色会按矩阵类型自动切换:
| 输入 | 判定依据 | 配色 |
|---|---|---|
感知相关性.csv(有负值) |
出现负相关 | vlag 发散色,色心锁 0:正相关偏红、负相关偏蓝、0 为白 |
感知不相似度矩阵.xlsx(全非负) |
距离矩阵没有负值 | 单向色阶 YlOrRd,否则整张图会挤在色带一侧读不出层次 |
Excel 的表头行会自动定位。感知不相似度矩阵.xlsx 的第 0 行是合并标题、
第 1 行才是 乐器声音样本 | 古筝 | 中胡 | ...,原脚本按第 0 行切会直接报错。
python scripts/analyze_audio.py "某个音频.wav"
python scripts/analyze_audio.py --instrument-dir "D:/YCHEN/timbre_space/audioclassification/flute" --limit 30
python scripts/analyze_audio.py --glob "D:/.../**/*.wav" --summary 特征汇总.csv
python scripts/analyze_audio.py <多个文件> --no-plots # 只提特征,批量时更快from timbre_space import TimbrePipeline, settings
pipeline = TimbrePipeline()
features, plots = pipeline.analyze_once("audio.wav")
print(features.global_features)
print(plots)所有配置集中在 timbre_space/config.py,路径全部基于文件位置推导,与启动时的工作目录无关。
| 配置项 | 默认值 | 说明 |
|---|---|---|
AudioConfig.target_sr |
22050 | 重采样目标采样率 |
AudioConfig.max_duration |
10.0 | 每段音频最长处理秒数 |
AudioConfig.n_mfcc |
20 | MFCC 维数 |
PlotConfig.dpi |
300 | CLI / 归档出图 dpi |
PlotConfig.app_dpi |
200 | 界面内出图 dpi(兼顾速度) |
PlotConfig.heatmap_dpi |
600 | 热力图 dpi(印刷级) |
可用环境变量覆盖路径:
TIMBRE_RUNTIME_DIR:运行产物目录TIMBRE_DATA_ROOT:乐器音频库所在根目录(默认取项目上一级,即D:\YCHEN\timbre_space)
功能增强
- 特征集取两者之长:保留
visualize copy 2.py的频谱带宽 / 滚降 / 对比度 / 平坦度、 20 维 MFCC、pyin基频、hpss谐波与打击乐分离,同时采用project2.py的模块边界。 单段音频实测输出 34 项全局特征。 - 新增谐波能量占比、浊音占比、频谱质心标准差等衍生特征。
- 逐帧特征做 min-max 归一化,避免 3D 轨迹被量纲最大的频谱质心完全支配。
- 热力图配色按矩阵类型自动切换,Excel 表头行自动定位(详见上一节)。
- 命令行支持批量与通配符,
--no-plots可跳过出图只提特征,适合跑数据集。
Bug 修复
| 问题 | 原状 | 现状 |
|---|---|---|
| 3D 图文件名硬编码 | visualize copy 2.py 写死 '小提琴_dynamic_3d.png',分析任何乐器都覆盖同一文件 |
文件名由音频文件名派生 |
| 历史记录未做归属校验 | visualize copy 2.py 的 load_history_task 不校验用户 |
HistoryStore.get 强制按用户名过滤 |
| task_id 冲突 | 秒级/整数时间戳,同秒两次分析互相覆盖;毫秒级在紧循环里仍会碰 | 毫秒时间戳 + 4 位随机后缀 |
| JSON 索引写坏 | seek(0) 后 dump,新内容更短时尾部残留旧字节 → JSON 损坏 |
临时文件 + os.replace 原子替换 |
| 登录态串号 | current_user 挂在全局单例,多窗口互相顶掉 |
gr.State(Session()) 每会话独立 |
| 密码强度 | 无盐 MD5 | PBKDF2-HMAC-SHA256(随机盐 + 20 万次迭代),旧 MD5 记录可登录并自动升级 |
| 中文乱码 | 字体路径写死 Linux 服务器 /hdd/.../simsun.ttc,Windows 上中文变方块 |
按操作系统探测候选字体 |
| 论文图字体降级 | — | 衬线中文(宋体)与黑体系分开解析,论文图仍用宋体 |
| 样条插值崩溃 | make_interp_spline 直接调用,时间轴重复/非单调时抛异常 |
先排序去重,再按点数降阶 |
| Excel 表头误判 | 按第 0 行切列名,遇到带标题行的表直接报错 | 在前若干行里自动定位表头 |
| 相对路径漂移 | 数据目录相对 cwd,换目录启动就找不到 | 全部基于 Path(__file__) 推导 |
| 删除越界 | 清空历史时直接 rmtree(record['save_dir']) |
校验目标必须是 history 目录的子目录 |
| 界面兼容性 | 只适用于 Gradio 4/5 | 探测并兼容 Gradio 5 / 6 |
感知相关性.csv 是 16 个主观听觉形容词与 3 个 UMAP 维度的相关系数矩阵。
从数值可以读出三条几乎正交的语义轴:
| 维度 | 语义轴 | 正向代表 | 负向代表 |
|---|---|---|---|
| 维度 1 | 明亮度 | 明亮 +0.85 / 尖锐 +0.84 / 清脆 +0.79 | 暗淡 −0.88 / 单薄 −0.46 |
| 维度 2 | 厚实度 | 浑厚 +0.73 / 厚实 +0.71 / 丰满 +0.68 | 单薄 −0.77 / 干瘪 −0.63 |
| 维度 3 | 粗糙度 | 粗糙 +0.84 / 混浊 +0.76 | 清脆 −0.44 |
互为反义的词对在同一条轴上符号相反(明亮/暗淡、丰满/单薄、清脆/混浊), 说明客观声学特征空间与主观感知维度是对齐的——这正是本课题的核心论点。
重构后已实际运行并全部通过(Windows / Python 3.13 / librosa 1.0.0 / gradio 6.27.0):
| 检查项 | 结果 |
|---|---|
python -m py_compile(14 个 .py) |
通过 |
tests/test_smoke.py |
9 / 9 通过 |
tests/test_pipeline_e2e.py(真实大提琴音频) |
25 / 25 通过 |
scripts/analyze_audio.py |
输出 37 列汇总表 + 2 张轨迹图 |
scripts/plot_perception_heatmap.py |
CSV 与 xlsx 两条分支均出图,与原 感知相关性_热力图.png 数值一致 |
| Gradio 界面构建 | 72 个组件,无异常 |
| 界面实际启动 | HTTP 200,标题与自定义 CSS 均正常注入 |
runtime/ 目录是运行产物(用户库、历史记录、临时文件),已在 .gitignore 中,
可随时整个删掉;下次启动会自动重建。