版本:1.2
创建日期:2026-01-30
最后更新:2026-01-30
状态:设计阶段
构建一个通用的乐谱渲染引擎,具备以下能力:
⚠️ 重要提醒:在开始任何编码工作之前,必须先完成 MusicXML 规范的完整学习。 详见 工作计划 - 阶段零
| 特性 | OSMD | VexFlow | 我们的引擎 |
|---|---|---|---|
| 简谱支持 | ❌ | ❌ | ✅ |
| 五线谱支持 | ✅ | ✅ | ✅(规划中) |
| 首调/固定调 | ❌ | ❌ | ✅ |
| MusicXML解析 | ✅ | ❌ | ✅ |
| 自主可控 | ❌(依赖) | ❌(依赖) | ✅ |
| 定制性 | 低 | 中 | 高 |
┌─────────────────────────────────────────────────────────────┐
│ 输入层 (Parser Layer) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │
│ │ MusicXML Parser │ │ MIDI Parser │ │ Other Parser │ │
│ │ (核心,优先实现) │ │ (未来扩展) │ │ (未来扩展) │ │
│ └────────┬────────┘ └────────┬────────┘ └──────┬───────┘ │
└───────────┼─────────────────────┼──────────────────┼─────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ 核心数据模型层 (Model Layer) │
│ │
│ Score ─┬─ Part ─┬─ Measure ─┬─ Note │
│ │ │ ├─ Forward/Backup │
│ │ │ ├─ Direction │
│ │ │ ├─ Attributes │
│ │ │ ├─ Barline │
│ │ │ └─ Harmony │
│ │ └─ Attributes (key, time, clef, etc.) │
│ └─ PartInfo (name, instrument, etc.) │
│ │
│ 【核心原则】:完整保留MusicXML的所有信息,不做任何简化 │
└─────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ 渲染层 (Renderer Layer) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │
│ │ Jianpu Renderer │ │ Staff Renderer │ │ Other │ │
│ │ (简谱渲染器) │ │ (五线谱渲染器) │ │ (未来扩展) │ │
│ │ 【已有,需重构】 │ │ 【待实现】 │ │ │ │
│ └─────────────────┘ └─────────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ 输出层 (Output Layer) │
│ SVG / Canvas / PDF / 其他格式 │
└─────────────────────────────────────────────────────────────┘
MusicXML文件
│
▼
┌──────────────┐
│ MusicXML │ 解析XML,生成通用数据模型
│ Parser │
└──────┬───────┘
│
▼
┌──────────────┐
│ Score │ 通用音乐数据模型
│ (核心模型) │ 完整保留所有MusicXML信息
└──────┬───────┘
│
├─────────────────┬─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Jianpu │ │ Staff │ │ MIDI │
│ Renderer │ │ Renderer │ │ Generator │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
简谱 SVG 五线谱 SVG MIDI文件
数据模型1:1对应MusicXML结构,确保不丢失任何信息。
// 顶层结构
Score
├── identification // 作品信息(标题、作者等)
├── defaults // 默认设置(页面、字体等)
├── credits[] // 版权信息
├── partList[] // 声部列表
└── parts[] // 声部内容
└── Part
└── measures[]
└── Measure
├── attributes // 拍号、调号、谱号等
├── notes[] // 音符和休止符
├── forwards[] // 时间前进
├── backups[] // 时间后退
├── directions[] // 方向标记
├── harmonies[] // 和弦符号
└── barlines[] // 小节线
interface Score {
// 作品标识
workTitle?: string;
workNumber?: string;
movementTitle?: string;
movementNumber?: string;
// 创作者信息
identification?: Identification;
// 默认设置
defaults?: Defaults;
// 声部
partList: PartInfo[];
parts: Part[];
}
interface Part {
id: string;
measures: Measure[];
}
interface PartInfo {
id: string;
name?: string;
abbreviation?: string;
instrument?: Instrument;
midiInstrument?: MidiInstrument;
}
interface Measure {
number: number;
width?: number;
implicit?: boolean;
// 小节内容(按时间顺序)
elements: MeasureElement[];
// 便捷访问
attributes?: Attributes;
notes: Note[];
directions: Direction[];
barlines: Barline[];
}
type MeasureElement =
| Note
| Forward
| Backup
| Direction
| Attributes
| Harmony
| Barline;
interface Note {
// 基本信息
pitch?: Pitch; // 音高(休止符为空)
rest?: Rest; // 休止符信息
unpitched?: Unpitched; // 无音高(打击乐)
// 时值
duration: number; // 实际时值(divisions单位)
type?: NoteType; // 音符类型(whole, half, quarter等)
dots: number; // 附点数量
// 连接
tie?: Tie[];
chord?: boolean; // 是否是和弦的一部分
// 声部和谱表
voice: number;
staff?: number;
// 符干和符杠
stem?: Stem;
beam?: Beam[];
// 记号
notations?: Notations;
// 歌词
lyrics?: Lyric[];
// 装饰音
grace?: Grace;
cue?: boolean;
}
interface Pitch {
step: 'C' | 'D' | 'E' | 'F' | 'G' | 'A' | 'B';
octave: number; // 八度(4 = 中央C所在八度)
alter?: number; // 变音(-1=降, 0=还原, 1=升, 0.5=微分音)
}
interface Notations {
// 连线
tied?: Tied[];
slur?: Slur[];
// 连音
tuplet?: Tuplet[];
// 滑音
glissando?: Glissando[];
slide?: Slide[];
// 演奏技法
articulations?: Articulations;
ornaments?: Ornaments;
technical?: Technical;
dynamics?: Dynamics;
// 其他
fermata?: Fermata[];
arpeggiate?: Arpeggiate;
nonArpeggiate?: NonArpeggiate;
accidentalMark?: AccidentalMark[];
}
interface Direction {
placement?: 'above' | 'below';
directive?: boolean;
directionTypes: DirectionType[];
offset?: number;
sound?: Sound;
staff?: number;
voice?: number;
}
type DirectionType =
| { type: 'words'; value: Words }
| { type: 'metronome'; value: Metronome }
| { type: 'dynamics'; value: Dynamics }
| { type: 'wedge'; value: Wedge }
| { type: 'pedal'; value: Pedal }
| { type: 'octaveShift'; value: OctaveShift }
| { type: 'segno'; value: Segno }
| { type: 'coda'; value: Coda }
| { type: 'rehearsal'; value: Rehearsal }
| { type: 'dashes'; value: Dashes }
| { type: 'bracket'; value: Bracket }
| /* ... 更多类型 */;
interface Barline {
location: 'left' | 'middle' | 'right';
barStyle?: BarStyle;
repeat?: Repeat;
ending?: Ending;
fermata?: Fermata[];
segno?: Segno;
coda?: Coda;
}
type BarStyle =
| 'regular' | 'dotted' | 'dashed' | 'heavy'
| 'light-light' | 'light-heavy' | 'heavy-light' | 'heavy-heavy'
| 'tick' | 'short' | 'none';
简谱渲染需要支持两种唱名模式,业务层通过切换开关选择:
| 模式 | 英文名 | 核心规则 | 适用场景 |
|---|---|---|---|
| 首调 | Movable-Do | 调式主音 = do(1) | 声乐、民乐、音乐教育 |
| 固定调 | Fixed-Do | C = do(1),永不改变 | 器乐、绝对音高训练 |
以 G大调 的旋律为例(实际音高:G A B C D E F#):
| 实际音高 | 固定调渲染 | 首调渲染 |
|---|---|---|
| G4 | 5 | 1 |
| A4 | 6 | 2 |
| B4 | 7 | 3 |
| C5 | 1̇ (高音1) | 4 |
| D5 | 2̇ | 5 |
| E5 | 3̇ | 6 |
| F#5 | #4̇ | 7 |
关键差异:
1=C(始终以C为基准)1=G(显示当前调的主音)设计思路:核心数据模型始终保存绝对音高(与MusicXML一致),渲染时根据模式动态转换。
// 核心模型:始终保存绝对音高
interface Note {
pitch: {
step: 'C' | 'D' | 'E' | 'F' | 'G' | 'A' | 'B'; // 绝对音名
octave: number; // 绝对八度
alter?: number; // 变音记号
};
// ... 其他属性
}
// 简谱渲染时的转换结果
interface JianpuPitch {
number: number; // 简谱数字 1-7,0=休止符
octave: number; // 八度偏移(相对于中音区)
accidental?: 'sharp' | 'flat' | 'natural'; // 升降号
}
/** 唱名模式 */
type SolfegeMode = 'movable' | 'fixed';
/** 谱面类型 */
type NotationType = 'staff' | 'jianpu-movable' | 'jianpu-fixed';
/** 渲染配置 */
interface RenderConfig {
// ... 其他配置
/**
* 唱名模式
* - 'movable': 首调(主音为do)
* - 'fixed': 固定调(C永远为do)
*/
solfegeMode: SolfegeMode;
}
固定调转换简单直接:音名直接映射到数字。
const STEP_TO_JIANPU: Record<string, number> = {
'C': 1, 'D': 2, 'E': 3, 'F': 4, 'G': 5, 'A': 6, 'B': 7
};
function toFixedDo(pitch: Pitch): JianpuPitch {
return {
number: STEP_TO_JIANPU[pitch.step],
octave: pitch.octave - 4, // 4为中央C的八度
accidental: alterToAccidental(pitch.alter),
};
}
首调转换需要考虑调号,计算音符相对于主音的音级。
const STEP_ORDER = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
/**
* 将绝对音高转换为首调简谱
* @param pitch 绝对音高
* @param keySignature 调号信息
*/
function toMovableDo(pitch: Pitch, keySignature: KeySignature): JianpuPitch {
const keyRoot = keySignature.key; // 如 'G', 'D', 'F' 等
const stepIndex = STEP_ORDER.indexOf(pitch.step);
const keyIndex = STEP_ORDER.indexOf(keyRoot[0]); // 取第一个字符,处理 'Bb' 等情况
// 计算相对音级(0-6)
const relativeDegree = (stepIndex - keyIndex + 7) % 7;
const number = relativeDegree + 1; // 转为1-7
// 计算八度偏移
const octaveOffset = calculateOctaveOffset(pitch, keySignature);
// 计算升降号(只有非调内音需要显示)
const accidental = calculateAccidentalForMovable(pitch, keySignature);
return { number, octave: octaveOffset, accidental };
}
/**
* 计算首调模式下的升降号
* 规则:调内音不显示升降号,临时变化音需要显示
*/
function calculateAccidentalForMovable(
pitch: Pitch,
keySignature: KeySignature
): 'sharp' | 'flat' | 'natural' | undefined {
const expectedAlter = getExpectedAlterInKey(pitch.step, keySignature);
const actualAlter = pitch.alter ?? 0;
if (actualAlter === expectedAlter) {
return undefined; // 调内音,不显示
}
// 临时变化音
if (actualAlter > expectedAlter) return 'sharp';
if (actualAlter < expectedAlter) return 'flat';
if (actualAlter === 0 && expectedAlter !== 0) return 'natural';
return undefined;
}
/** 调号信息(fifths = 升号数,负数为降号数) */
const KEY_SIGNATURE_MAP: Record<number, {
key: string;
alterations: Map<string, number>; // 调内音的升降
}> = {
// 升号调
0: { key: 'C', alterations: new Map() },
1: { key: 'G', alterations: new Map([['F', 1]]) },
2: { key: 'D', alterations: new Map([['F', 1], ['C', 1]]) },
3: { key: 'A', alterations: new Map([['F', 1], ['C', 1], ['G', 1]]) },
4: { key: 'E', alterations: new Map([['F', 1], ['C', 1], ['G', 1], ['D', 1]]) },
5: { key: 'B', alterations: new Map([['F', 1], ['C', 1], ['G', 1], ['D', 1], ['A', 1]]) },
6: { key: 'F#', alterations: new Map([['F', 1], ['C', 1], ['G', 1], ['D', 1], ['A', 1], ['E', 1]]) },
7: { key: 'C#', alterations: new Map([['F', 1], ['C', 1], ['G', 1], ['D', 1], ['A', 1], ['E', 1], ['B', 1]]) },
// 降号调
-1: { key: 'F', alterations: new Map([['B', -1]]) },
-2: { key: 'Bb', alterations: new Map([['B', -1], ['E', -1]]) },
-3: { key: 'Eb', alterations: new Map([['B', -1], ['E', -1], ['A', -1]]) },
-4: { key: 'Ab', alterations: new Map([['B', -1], ['E', -1], ['A', -1], ['D', -1]]) },
-5: { key: 'Db', alterations: new Map([['B', -1], ['E', -1], ['A', -1], ['D', -1], ['G', -1]]) },
-6: { key: 'Gb', alterations: new Map([['B', -1], ['E', -1], ['A', -1], ['D', -1], ['G', -1], ['C', -1]]) },
-7: { key: 'Cb', alterations: new Map([['B', -1], ['E', -1], ['A', -1], ['D', -1], ['G', -1], ['C', -1], ['F', -1]]) },
};
class JianpuRenderer {
private config: RenderConfig;
/**
* 设置唱名模式
* @param mode 'movable' | 'fixed'
*/
setSolfegeMode(mode: SolfegeMode): void {
this.config.solfegeMode = mode;
this.rerender(); // 触发重新渲染
}
/**
* 获取当前唱名模式
*/
getSolfegeMode(): SolfegeMode {
return this.config.solfegeMode;
}
}
// 业务层切换开关
function onNotationModeChange(mode: 'staff' | 'jianpu-movable' | 'jianpu-fixed') {
switch (mode) {
case 'staff':
// 切换到五线谱渲染器
staffRenderer.render(score);
break;
case 'jianpu-movable':
// 简谱 + 首调
jianpuRenderer.setSolfegeMode('movable');
jianpuRenderer.render(score);
break;
case 'jianpu-fixed':
// 简谱 + 固定调
jianpuRenderer.setSolfegeMode('fixed');
jianpuRenderer.render(score);
break;
}
}
| 模式 | 调号显示格式 | 示例(G大调) |
|---|---|---|
| 首调 | 1={主音} |
1=G |
| 固定调 | 1=C(可选显示原调) |
1=C 或 1=C (G大调) |
| 优先级 | 功能 | 说明 |
|---|---|---|
| P0 | 固定调渲染 | 当前已实现,保持不变 |
| P0 | 首调渲染 | 核心功能,需要实现 |
| P1 | 运行时模式切换 | 业务层切换不重新解析 |
| P2 | 调号变化处理 | 乐曲中间转调时的处理 |
| P2 | 小调支持 | 自然小调、和声小调等 |
| 优先级 | 分类 | 包含元素 | 状态 |
|---|---|---|---|
| P0 | 结构 | score-partwise, part, measure | 🟡 部分 |
| P0 | 音符 | note, pitch, rest, duration, type | 🟡 部分 |
| P0 | 属性 | attributes, divisions, key, time, clef | 🟡 部分 |
| P1 | 小节线 | barline, bar-style, repeat, ending | 🟢 已实现 |
| P1 | 连接 | tie, slur, beam | 🟡 部分 |
| P1 | 歌词 | lyric, syllabic, text | 🟡 部分 |
| P2 | 方向 | direction, words, dynamics, wedge, metronome | 🔴 待实现 |
| P2 | 记号 | articulations, ornaments, fermata | 🔴 待实现 |
| P2 | 装饰音 | grace, cue | 🟡 部分 |
| P3 | 和弦 | harmony, root, kind, bass | 🔴 待实现 |
| P3 | 高级 | figured-bass, print, sound | 🔴 待实现 |
| 元素 | 描述 | 状态 |
|---|---|---|
<score-partwise> |
按声部组织的乐谱 | 🟢 |
<score-timewise> |
按时间组织的乐谱 | 🔴 |
<work> |
作品信息 | 🟡 |
<identification> |
标识信息 | 🟡 |
<defaults> |
默认设置 | 🔴 |
<credit> |
版权信息 | 🔴 |
<part-list> |
声部列表 | 🟡 |
<part> |
声部 | 🟢 |
<measure> |
小节 | 🟢 |
| 元素 | 描述 | 状态 |
|---|---|---|
<note> |
音符 | 🟢 |
<pitch> |
音高 | 🟢 |
<rest> |
休止符 | 🟢 |
<unpitched> |
无音高 | 🔴 |
<duration> |
时值 | 🟢 |
<type> |
音符类型 | 🟢 |
<dot> |
附点 | 🟢 |
<accidental> |
临时升降号 | 🟡 |
<time-modification> |
连音时值修改 | 🔴 |
<stem> |
符干 | 🔴 |
<notehead> |
符头 | 🔴 |
<beam> |
符杠 | 🔴 |
<notations> |
记号容器 | 🟡 |
<lyric> |
歌词 | 🟢 |
<chord> |
和弦标记 | 🟡 |
<grace> |
装饰音 | 🟡 |
<cue> |
提示音符 | 🔴 |
| 元素 | 描述 | 状态 |
|---|---|---|
<tied> |
连音线 | 🟡 |
<slur> |
圆滑线 | 🟡 |
<tuplet> |
连音符 | 🟡 |
<glissando> |
滑音 | 🔴 |
<slide> |
滑奏 | 🔴 |
<ornaments> |
装饰音记号 | 🔴 |
<articulations> |
演奏技法 | 🟡 |
<dynamics> |
力度 | 🟡 |
<fermata> |
延音记号 | 🟡 |
<arpeggiate> |
琶音 | 🔴 |
<technical> |
技术指示 | 🔴 |
| 元素 | 描述 | 状态 |
|---|---|---|
<accent> |
重音 | 🔴 |
<strong-accent> |
强重音 | 🔴 |
<staccato> |
断奏 | 🟡 |
<tenuto> |
保持音 | 🔴 |
<detached-legato> |
断连 | 🔴 |
<staccatissimo> |
顿音 | 🔴 |
<spiccato> |
跳弓 | 🔴 |
<breath-mark> |
呼吸记号 | 🔴 |
<caesura> |
停顿 | 🔴 |
| 元素 | 描述 | 状态 |
|---|---|---|
<trill-mark> |
颤音 | 🔴 |
<turn> |
回音 | 🔴 |
<delayed-turn> |
延迟回音 | 🔴 |
<inverted-turn> |
逆回音 | 🔴 |
<mordent> |
波音 | 🔴 |
<inverted-mordent> |
逆波音 | 🔴 |
<schleifer> |
滑音 | 🔴 |
<tremolo> |
震音 | 🔴 |
| 元素 | 描述 | 状态 |
|---|---|---|
<direction> |
方向标记容器 | 🔴 |
<direction-type> |
方向类型 | 🔴 |
<words> |
文字 | 🔴 |
<dynamics> |
力度 | 🔴 |
<wedge> |
渐强渐弱 | 🔴 |
<metronome> |
速度标记 | 🟡 |
<octave-shift> |
八度移位 | 🔴 |
<pedal> |
踏板 | 🔴 |
<segno> |
反复记号 | 🔴 |
<coda> |
尾声记号 | 🔴 |
<dashes> |
虚线 | 🔴 |
<bracket> |
括号 | 🔴 |
<rehearsal> |
排练号 | 🔴 |
| 元素 | 描述 | 状态 |
|---|---|---|
<barline> |
小节线 | 🟢 |
<bar-style> |
线型 | 🟢 |
<repeat> |
反复 | 🟢 |
<ending> |
跳房子 | 🟢 |
<fermata> |
延音记号 | 🟡 |
| 元素 | 描述 | 状态 |
|---|---|---|
<divisions> |
时值单位 | 🟢 |
<key> |
调号 | 🟢 |
<time> |
拍号 | 🟢 |
<clef> |
谱号 | 🔴 |
<staves> |
谱表数 | 🔴 |
<transpose> |
移调 | 🔴 |
| 元素 | 描述 | 状态 |
|---|---|---|
<harmony> |
和弦符号 | 🔴 |
<root> |
根音 | 🔴 |
<kind> |
和弦类型 | 🔴 |
<bass> |
低音 | 🔴 |
<degree> |
和弦变化 | 🔴 |
图例:
目标:建立完整的通用数据模型和MusicXML解析器
任务清单:
src/music-core/ 目录结构产出:
验收标准:
目标:将现有简谱渲染器迁移到新架构
任务清单:
src/renderers/jianpu/ 目录产出:
验收标准:
目标:实现五线谱渲染器
任务清单:
src/renderers/staff/ 目录产出:
目标:覆盖所有MusicXML元素,优化性能
任务清单:
src/
├── music-core/ # 【新增】通用核心层
│ ├── models/ # 数据模型
│ │ ├── Score.ts
│ │ ├── Part.ts
│ │ ├── Measure.ts
│ │ ├── Note.ts
│ │ ├── Pitch.ts
│ │ ├── Duration.ts
│ │ ├── Attributes.ts
│ │ ├── Direction.ts
│ │ ├── Notations.ts
│ │ ├── Barline.ts
│ │ ├── Harmony.ts
│ │ └── index.ts
│ ├── parser/ # 解析器
│ │ ├── MusicXMLParser.ts # 主解析器
│ │ ├── NoteParser.ts # 音符解析
│ │ ├── DirectionParser.ts # 方向解析
│ │ ├── AttributesParser.ts # 属性解析
│ │ └── index.ts
│ ├── utils/ # 工具函数
│ │ ├── PitchUtils.ts
│ │ ├── DurationUtils.ts
│ │ └── index.ts
│ └── index.ts
│
├── renderers/ # 【重组】渲染层
│ ├── common/ # 共享渲染工具
│ │ ├── SVGRenderer.ts
│ │ ├── LayoutEngine.ts
│ │ └── index.ts
│ │
│ ├── jianpu/ # 简谱渲染器
│ │ ├── JianpuRenderer.ts
│ │ ├── converter/ # 模型转换
│ │ │ └── JianpuConverter.ts
│ │ ├── drawer/ # 绘制器
│ │ │ ├── NoteDrawer.ts
│ │ │ ├── LineDrawer.ts
│ │ │ ├── LyricDrawer.ts
│ │ │ └── ...
│ │ ├── layout/ # 布局
│ │ │ └── JianpuLayoutEngine.ts
│ │ └── index.ts
│ │
│ └── staff/ # 【新增】五线谱渲染器
│ ├── StaffRenderer.ts
│ ├── drawer/
│ │ ├── StaveDrawer.ts
│ │ ├── NoteHeadDrawer.ts
│ │ ├── StemDrawer.ts
│ │ ├── BeamDrawer.ts
│ │ ├── ClefDrawer.ts
│ │ └── ...
│ ├── layout/
│ │ └── StaffLayoutEngine.ts
│ └── index.ts
│
├── adapters/ # 适配器(兼容层)
│ └── OSMDCompatibilityAdapter.ts
│
└── index.ts # 统一导出
Note, Pitch, MeasureJianpu 前缀,如 JianpuRendererStaff 前缀,如 StaffRenderer| 术语 | 英文 | 说明 |
|---|---|---|
| 简谱 | Jianpu / Numbered Musical Notation | 用数字1-7表示音高的记谱法 |
| 五线谱 | Staff Notation | 用五条线表示音高的记谱法 |
| 首调 | Movable-Do / Movable Solfège | 调式主音永远唱 do(1),音级相对于主音计算 |
| 固定调 | Fixed-Do / Fixed Solfège | C 永远唱 do(1),音名与唱名固定对应 |
| 小节 | Measure / Bar | 乐谱的基本时间单位 |
| 声部 | Part / Voice | 乐谱中的独立旋律线 |
| 时值 | Duration | 音符的持续时间 |
| 音高 | Pitch | 音符的高低 |
| 八度 | Octave | 音高的循环单位 |
| 调号 | Key Signature | 指定调性的升降号 |
| 拍号 | Time Signature | 指定节拍的记号 |
| 谱号 | Clef | 指定音高位置的记号 |
| 主音 | Tonic | 调式的第一音,决定调性 |
| 音级 | Scale Degree | 音符在调式中的相对位置(1-7) |
文档结束