基于 Rust 的钢琴乐谱渲染引擎,将自定义 DSL 文本转换为 SVG 格式的专业钢琴谱。支持标准五线谱、指法标注、歌词展示、简谱等多种显示模式,可通过 WASM 在 Web 环境中使用。
- V2 行式 DSL:VexTab 风格行式语法,简洁高效(V1 块式语法已废弃)
- 多种显示模式:标准五线谱、五线谱+指法、仅指法、简谱+指法、测验模式、自定义模式
- 图层控制:音符、符杆、符尾、连梁、指法、歌词、简谱等图层可独立显隐
- 专业符号:Bravura (SMuFL) 字形已提取为 SVG path,零字体依赖,任何查看器都不出现方块
- 演奏法标记:staccato / accent / tenuto / marcato / fermata 等渲染
- 指法标注:支持左右手 1-5 指法标记,位置和样式可定制
- 歌词显示:支持多音节词、连字符、多段歌词
- 简谱模式:自动生成简谱数字和八度标记
- 精确时值运算:有理数(整数比例)计算时值,三连音等 tuplet 无浮点累积误差
- SVG 输出:自包含矢量输出(~19KB),无需外部字体
- WASM 绑定:编译为 WebAssembly,可在浏览器中实时渲染
- 配置驱动:通过 JSON 配置或预设模板控制所有渲染参数
piano-score/
├── crates/
│ ├── pscore-core/ # 领域模型(Score, Staff, Measure, Note, Pitch 等)
│ ├── pscore-parser/ # DSL 解析器(Lexer → AST → 语义分析 → Score)
│ ├── pscore-layout/ # 排版引擎(计算各元素的坐标和尺寸)
│ ├── pscore-render/ # SVG 渲染器(将布局结果转为 SVG 图形)
│ ├── pscore-config/ # 配置系统(显示模式、图层、布局、样式)
│ └── pscore-wasm/ # WASM 绑定(Web 环境集成接口)
├── examples/
│ └── demo/ # 命令行示例程序
└── assets/ # Bravura 字体文件
# 需要 Rust 工具链
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shcargo run -p demo将在项目根目录生成以下 SVG 文件:
output_standard.svg— 标准五线谱output_fingering.svg— 五线谱 + 指法output_finger_only.svg— 仅指法output_lyrics.svg— 五线谱 + 歌词output_numbered.svg— 简谱 + 指法
use pscore_config::{ScoreConfig, DisplayMode};
// 1. 解析 V2 DSL 文本(推荐)
let score = pscore_parser::parse_v2(dsl).expect("解析失败");
// 2. 选择显示模式并计算布局
let config = ScoreConfig::from_preset(DisplayMode::Standard);
let layout = pscore_layout::layout(&score, &config).expect("布局失败");
// 3. 渲染为 SVG
let svg = pscore_render::render_svg(&layout, &config).expect("渲染失败");
// 4. 保存或输出
std::fs::write("output.svg", svg).expect("写入失败");V2 采用 VexTab 风格的行式语法,每行一个声明或一组音符:
title "小星星"
tempo 120
key C
time 4/4
stave treble key=C time=4/4
notes C4:q D4:q E4:q F4:q |
notes G4:q G4:q A4:h |
stave bass
notes C3:q E3:q G3:q C4:q |
title "..."/composer "..."/tempo N/key C/time 4/4— 元数据行(全局)stave <clef> [key=...] [time=...]— 声明谱表(treble/bass/alto/tenor)notes <音符>... |— 音符行,|分隔小节,支持|||]|::|等小节线//— 行注释
格式:音名八度:时值[附点][合并属性]
| 组成 | 说明 | 示例 |
|---|---|---|
| 音名 | C D E F G A B(大写) | C4, G4, A4 |
| 八度 | 数字(4 = 中央C所在八度) | C4, C5, C3 |
| 时值 | 字母:w/h/q/e/s/t 或 数字:1/2/4/8/16/32 | C4:q 或 C4:4 |
| 附点 | 时值后加 . 或 .. |
D4:h. (附点二分), C4:q.. (复附点) |
| 休止符 | r:时值 |
r:q, r:e |
| 和弦 | <C4 E4 G4>:时值 |
<C4 E4 G4>:q |
| 连音 | ^3^ 或 ^5,4^(套用后续 N 个事件) |
^3^ C4:e D4:e E4:e |
用 [...] 将指法、歌词、演奏法合并到一块,| 分隔:
| 语法 | 说明 | 示例 |
|---|---|---|
R1–R5 |
右手 1-5 指 | [R1] |
L1–L5 |
左手 1-5 指 | [L2] |
文字 |
歌词 | [一闪] |
staccato / accent / tenuto 等 |
演奏法 | [staccato] |
组合示例:C4:e [R1|一闪|staccato] — 右手1指 + 歌词"一闪" + 断音
title "小星星"
tempo 120
key C
time 4/4
stave treble
notes C4:e[R1|一] C4:e[R1|闪] G4:e[R3|一] G4:e[R3|闪] A4:q[R4|亮] G4:q[R3|晶] |
notes F4:e[R2|满] F4:e[R2|天] E4:e[R1|都] E4:e[R1|是] D4:h.[R1|星] |
stave bass
notes C3:q E3:q G3:q C4:q |
notes F3:q A3:q C4:q F4:q |
V1 块式语法(score "..." { staff ... { measure { ... } } })已标记 #[deprecated],
仍可通过 pscore_parser::parse() 使用,但推荐迁移到 V2 pscore_parser::parse_v2()。
| 模式 | 说明 | 预设名称 |
|---|---|---|
| 标准五线谱 | 仅显示五线谱音符 | Standard |
| 五线谱+指法 | 五线谱 + 指法标注 | WithFingering |
| 五线谱+音名 | 五线谱 + 指法 + 音名 | WithNoteNames |
| 仅指法 | 隐藏五线谱,只显示指法 | FingeringOnly |
| 简谱+指法 | 简谱数字 + 指法标注 | NumberedWithFingering |
| 测验模式 | 部分隐藏,用于练习 | QuizMode |
| 自定义 | 完全自定义图层组合 | Custom |
// 使用预设
let config = ScoreConfig::from_preset(DisplayMode::WithFingering);
// 自定义:在预设基础上微调
let mut config = ScoreConfig::from_preset(DisplayMode::Standard);
config.layers.lyrics = true; // 显示歌词
config.layers.fingering = true; // 显示指法
config.layers.note_names = true; // 显示音名
config.staff.line_style = LineStyle::Dashed; // 虚线谱表ScoreConfig 由四个子配置组成:
| 参数 | 默认值 | 说明 |
|---|---|---|
page_width |
800.0 | 页面宽度 (px) |
page_height |
300.0 | 页面高度 (px) |
margin_top |
20.0 | 上边距 |
margin_bottom |
20.0 | 下边距 |
margin_left |
20.0 | 左边距 |
margin_right |
20.0 | 右边距 |
staff_spacing |
100.0 | 谱表间距(高低音谱表之间) |
line_spacing |
10.0 | 五线谱线间距 |
measure_padding |
5.0 | 小节内边距 |
| 参数 | 默认值 | 说明 |
|---|---|---|
notes |
true | 音符头 |
stems |
true | 符杆 |
flags |
true | 符尾 |
beams |
true | 连梁 |
fingering |
false | 指法标注 |
lyrics |
false | 歌词 |
note_names |
false | 音名 |
numbered_notes |
false | 简谱数字 |
ledger_lines |
true | 加线 |
ties |
true | 连音线 |
slurs |
true | 圆滑线 |
articulations |
true | 演奏法标记 |
| 参数 | 默认值 | 说明 |
|---|---|---|
scale |
1.0 | 整体缩放 |
note_color |
#000000 | 音符颜色 |
staff_line_color |
#000000 | 谱表线颜色 |
fingering_color |
#cc0000 | 指法颜色(红色) |
lyric_color |
#000000 | 歌词颜色 |
note_name_color |
#0066cc | 音名颜色(蓝色) |
music_font |
Bravura | 音乐字体 |
lyric_font_size |
12.0 | 歌词字号 |
fingering_font_size |
10.0 | 指法字号 |
note_name_font_size |
9.0 | 音名字号 |
| 参数 | 默认值 | 说明 |
|---|---|---|
visible |
true | 谱表是否可见 |
line_count |
5 | 谱表线数 |
line_style |
Solid | 线样式(Solid/Dashed/Dotted/Hidden) |
show_clef |
true | 显示谱号 |
show_key_signature |
true | 显示调号 |
show_time_signature |
true | 显示拍号 |
核心数据结构层次:
Score
├── metadata (标题、作曲、BPM 等)
└── staves: Vec<Staff>
├── clef (谱号)
├── key_signature (调号)
├── time_signature (拍号)
└── measures: Vec<Measure>
└── voices: Vec<Voice>
└── events: Vec<NoteEvent>
├── Note(NoteData)
│ ├── pitch: Pitch (音高,MIDI 编号)
│ ├── duration: Duration (时值 + 附点 + 连音)
│ ├── lyric: Option<Lyric>
│ ├── fingering: Option<Fingering>
│ ├── articulations: Vec<Articulation>
│ └── tie: TieType
├── Chord(Vec<NoteData>)
└── Rest(Duration)
DSL 文本
│
▼ Lexer(词法分析)
Tokens
│
▼ Parser(语法分析)
AST
│
▼ Semantic Analysis(语义分析)
Score(领域模型)
│
▼ Layout Engine(排版引擎)
LayoutResult(坐标 + 尺寸)
│
▼ SVG Renderer(渲染引擎)
SVG 字符串
两阶段排版:
- 度量计算:计算每个元素的宽度和高度
- 页面布局:确定每个元素的绝对坐标(处理换行、谱表间距等)
分层渲染,从底到顶:
- 背景层
- 谱表线层
- 符号层(音符头、谱号、拍号等)
- 标记层(指法、歌词、音名等)
音乐符号使用 Bravura 字体(SMuFL 标准)通过 Unicode 代码点渲染:
| 符号 | SMuFL 代码点 | 说明 |
|---|---|---|
| 黑色音符头 | U+E0A4 | 四分及更短时值 |
| 空心音符头 | U+E0A3 | 二分音符 |
| 全音符头 | U+E0A2 | 全音符 |
| 高音谱号 | U+E050 | G 谱号 |
| 低音谱号 | U+E062 | F 谱号 |
| 八分符尾 | U+E240 | 单符尾向上 |
| 休止符 | U+E4E5 | 四分休止符 |
wasm-pack build crates/pscore-wasm --target webimport init, { ScoreRenderer } from './pkg/pscore_wasm.js';
await init();
const renderer = new ScoreRenderer();
// 加载 V2 DSL 文本
renderer.load(`
title "测试"
stave treble
notes C4:q D4:q E4:q F4:q |
`);
// 设置显示模式
renderer.set_display_mode("with_fingering");
// 切换图层
renderer.set_layer_visible("lyrics", true);
renderer.set_layer_visible("fingering", true);
// 渲染为 SVG
const svgString = renderer.render_svg();
// 插入页面
document.getElementById('score-container').innerHTML = svgString;
// 自定义配置(JSON)
renderer.set_config(JSON.stringify({
mode: "Standard",
layers: { notes: true, fingering: true, lyrics: true },
style: { note_color: "#333333", scale: 1.2 }
}));| 方法 | 说明 |
|---|---|
new() |
创建渲染器实例 |
load(dsl) |
加载 V2 DSL 文本(推荐) |
load_v1(dsl) |
加载 V1 DSL 文本(已废弃) |
set_display_mode(mode) |
设置显示模式 |
set_layer_visible(layer, visible) |
切换图层显隐 |
set_config(json) |
设置完整配置(JSON) |
get_config() |
获取当前配置(JSON) |
render_svg() |
渲染为 SVG 字符串 |
显示模式值:standard | with_fingering | with_note_names | fingering_only | numbered_with_fingering | quiz | custom
图层值:fingering | lyrics | note_names | numbered_notes | stems | flags | staff_lines
MIT OR Apache-2.0