2026-09-03 · 对 669 首古风简谱歌(
raw_jianpu_669)的记谱方法做全量普查、归一化到主流写法, 并以"双渲染对比 + SVG 逐字节对比"完成验证闭环。
669 首谱面({song_id}_原始简谱.json)虽然都是同一套简谱 DSL 语法,但每首歌的写法选择不同,
导致下游(渲染、对齐、统计)处处踩坑。本研究回答三个问题:
- 669 个谱的记谱方法到底有几种?(普查)
- 能不能统一成一种写法,且不改变任何音乐语义?(归一化)
- 怎么证明"没改变"?(验证闭环)
三大正交方言轴(全量实测):
| 方言轴 | 变体分布(歌数) |
|---|---|
| 减时线(8 变体) | x 396 / _ 87 / _= 75 / x= 62 / x_= 31 / x_ 17 / = 1 / 无 1 |
| 低八度点(8 变体) | g+d 213 / g 144 / g+, 129 / d 93 / , 56 / … |
| 段落结构(8 变体) | {play:}+段名行 248 / 跳房[1: 187 / 无结构 132 / {{}} 18 / … |
- DSL 语义词表(实测确认):数字 1-7 音阶、
0休止、-延一拍、_/x各=一条减时线(八分)、__/xx/=两条(十六分)、.附点、g高八度点、d/,低八度点、b/#升降、( )连音/倚音、[1,2:跳房、{!拼音!}歌词槽、{{ }}段结构、{play:}{bpm:}{cn:}{f:}等 20 种标签 - 411 首(61%)无
{bpm:}标记 → 渲染器一律填默认 72(必须区分 explicit/default) - keynote 26 种写法(
1=A4带八度、1=F G双调、1=B全角等号、37 首为空)
- raw
songdata.lyric= 字符串里套 JSON 数组;/分组(567 首)、//空行(386 首)、_延音填充(39 首)、多空格对齐、中文标点 669 首全混排、日语歌词(群青) - 拼音槽体系
{!c.an_gq_ia.n_g!}与汉字体系并存(69 首) - 歌词段数与谱面段落结构基本不对应(93% 的
{play:}歌不匹配)→ 歌词数组无结构语义 - 多段词机制(227 首两段词):同一旋律按遍展开,每遍挂一段词;词段缺句回落挂段 1(实证:某段播 3 遍挂同一句词,该句只在段 1 存在)
原始 DSL(紧缩)/ 渲染 DSL(playback)/ 行内 jianpu+中文拍注 / MIDI 数组(谱面域)/ MIDI(演唱域)/ 42 字段事件 / c-t-dur 结构化 / MIDI 文件两套(PPQ480 秒轴、PPQ3360 拍轴)/ f0 Hz。
统一目标 = 众数写法:减时线一律 x 系列(x/xx/xxx),低音点一律 d(高音点 g 本已统一);
标签、段落、歌词槽、调号等非音符记号原样保留。
5 条改写规则(顺序敏感!):
___ → xxx (三条线)
__ → xx (两条线)
, → d (低音点, 必须先于 =→xx, 否则 "7,=" 的 = 漏转)
= → xx (等号=两条线)
_ → x (一条线; 正文内全部)
保护机制:{…} 标签、{!…!} 歌词槽、{{ }} 段边界、[1,2: 跳房头内部不转换;
含未证实记号 =}(15 首)整首跳过、原文照存。
| 层 | 方法 | 结果 |
|---|---|---|
| events 语义 | 原版与统一版各过官方渲染器,逐事件 diff 音高/拍轴/秒轴/歌词槽/定位 | 654/654 全等 |
| SVG 视觉 | 各渲染 SVG,剥除嵌入源数据 payload 与溯源列坐标后逐字节对比 | 654/654 全等 |
| 残留自检 | 统一版正文逐段扫描旧方言记号(渲染对比抓不到漏转——两边一致地"没转"也 PASS) | 0 残留 |
| 歌词守恒 | 音符级字槽 vs raw 原文字数对账 | 0 缺失 |
结果:654/669 统一并验证通过,15 首含 =} 原文照存(标记 skip),0 失败。
实际改写 287 首,规则命中 5.8 万处(_→x 31142、=→xx 20878、,→d 6050、___→xxx 90)。
SVG 仅有的差异 = 嵌入的源数据 payload(notation 字段原文 vs 统一文)与 溯源列坐标 firstColumn/lastColumn(
=→xx加长导致 +1 平移)——都是"源文本不同"的记录, 画出来的图形逐字节相同。
研究确认存在两层统一 JSON,分工如下:
第一层:songdata 统一(统一简谱/) |
第二层:cell 统一(0830/songs_unified/) |
|
|---|---|---|
| 形态 | {songdata:{notation, notation_raw, lyric, lyric_layer}} |
{notes:[{content,octave,underBarCount,dotCount,noteNumber}], raw} |
| 统一层面 | 文本层(方言 → x-d 规范,可逆) | 结构层(减时线/附点/音高全部字段化,方言在字段层天然归一) |
| 角色 | 渲染器输入(喂 json2svg / render.py) | 渲染结果对照表:notes[i] ↔ SVG 第 i 个音符(noteCount = svg_note_count,牵丝戏 200=200) |
一两句 LRC
↓ 词序匹配(⚠️不能用 LRC 时间戳: LRC=实际音频时间轴, 谱面=bpm 合成时间轴)
播放展开流 → 字槽 loc={(line,col,occ)} → 这两句占的谱面位置
↓ 按 loc 切第一层 songdata
组装"子段 songdata json"(谱面文本切片 + 歌词切片)
↓ render.py / json2svg
单独渲染的一段 SVG ✓
基建先例(已跑通):0830/ 的 6141_十年人间拆段版(原始简谱_子段版.json → 每段独立页面+MIDI)、
render.py 通用渲染器、分段铁律(房子不单独成段、|:/{DS}/|| 清理、{{}} 过滤、
词块按遍整挂、; 延音占位)。
拆段判定三连续:词序连续(句 i+1 紧跟句 i)、版面连续(谱面行差≤1,SVG 连续流水排版)、
播放连续(前句末音 offset = 后句首音 onset,零间隔);间奏行 = 天然段界。
兜底:重复段句按第 n 遍匹配(occ)、谱面缺词段挂最近音符标记补挂。
牵丝戏 33 句实测:17 句对三连续全满足、1 断点=间奏、失败句全部分类可兜底(详见 拆段方案.md)。
第二层(songs_unified)用作切完后的校验:切段渲染出的音符数/音高应等于 notes[i..j] 切片。
├── README.md 本文档
├── schema.md 统一数据模型设计(bai-ma-unified-v1)
├── 验证报告.md 结果总表+方言分布+跳过清单
├── 拆段方案.md LRC 句 → 段级 json+SVG 的拆段研究
├── 统一简谱/ ★ 669 首统一版(每首含 notation / notation_raw / notation_dialect / lyric_layer)
├── lib/
│ ├── dialect.py 方言归一化器(5 规则 + 保护段 + 未证实记号检测)
│ └── lyric_slots.py 歌词字槽提取(音符级 0..n 字 + 守恒检查)
├── run_normalize.py 批量归一化 + 双渲染验证 + 残留自检
├── verify_svg.py 全量 SVG 逐字节验证
├── integrate_lyric_layer.py 字槽集成
└── logs/ 逐歌验证明细(json)
统一版每首歌的 json 字段:
python3 run_normalize.py # 归一化 + 双渲染验证 + 残留自检(669 首, ~8min)
python3 integrate_lyric_layer.py # 歌词字槽集成
python3 verify_svg.py # SVG 全量逐字节验证依赖:Node.js(苍强渲染器 export_playback_full.js / json2svg.py)。
{"songdata": { "notation": "…0x5dx1x7dx…", // 统一写法(x-d 规范) "notation_raw": "…0_5,_1_7,_…", // 原文一字不动(可逆锚) "notation_dialect": {"line_x":…, "oct_comma":…}, // 转换前方言指纹 "lyric_layer": { // 歌词层 "raw_lyric": "原样", "slots": [{line,col,occ,chars}], "conservation": {"missing":[]} } }}