版本:1.0
创建日期:2026-01-30
目的:分析 MusicXML 各元素对业务功能的影响,指导开发优先级
关联文档:MUSICXML_TAGS_MANUAL.md
| 功能 | 说明 | 依赖的 MusicXML 元素 |
|---|---|---|
| 乐谱渲染 | 将 MusicXML 渲染为简谱/五线谱 | 几乎所有元素 |
| 播放控制 | 播放乐谱,高亮当前音符 | note, duration, sound, repeat |
| 歌词高亮 | 播放时同步高亮歌词 | lyric |
| 选段播放 | 选择小节范围播放 | measure, barline |
| 评测着色 | 根据评测结果给音符着色 | note (id) |
| 首调/固定调 | 切换显示模式 | key, pitch, alter |
| 速度控制 | 调整播放速度 | sound (tempo) |
MusicXML 文件
│
▼
┌─────────────────┐
│ MusicXML Parser │ ← 解析元素
└────────┬────────┘
│
▼
┌─────────────────┐
│ 通用数据模型 │ ← Score/Part/Measure/Note
└────────┬────────┘
│
┌────┴────┐
│ │
▼ ▼
┌───────┐ ┌───────────┐
│ Render │ │ times[] │ ← 业务层核心数据
└───┬───┘ └─────┬─────┘
│ │
▼ ▼
SVG/DOM 播放/高亮/评测
state.times是业务层最核心的数据结构,几乎所有播放、高亮、评测功能都依赖它。
| times 字段 | 数据类型 | 来源 MusicXML 元素 | 计算方式 | 业务用途 |
|---|---|---|---|---|
| 标识字段 | ||||
i |
number | - | 数组索引 | 遍历 |
noteId |
string | note@id | 直接使用或生成 | 音符唯一标识 |
id |
string | note@id | 同 noteId | 兼容字段 |
| 时间字段 | ||||
time |
number | duration, divisions | 累计时间 + duration/divisions × 60/BPM |
开始时间(秒) |
endtime |
number | 同上 | time + duration/divisions × 60/BPM |
结束时间(秒) |
relativeTime |
number | - | time - fixtime |
相对时间 |
fixtime |
number | time(拍号) | 节拍器拍数 × 60/BPM |
前奏时间 |
measureLength |
number | time(拍号), BPM | beats/beat-type × 4 × 60/BPM |
小节时长 |
| 小节字段 | ||||
MeasureNumberXML |
number | measure@number | 直接使用 | 小节号 |
measureListIndex |
number | - | 从 0 开始计数 | 小节索引 |
measureOpenIndex |
number | - | 计算 | 有效小节索引 |
| 音高字段 | ||||
pitch |
number | pitch/step | C=1...B=7 | 简谱数字 |
octave |
number | pitch/octave | octave - 4 | 八度偏移 |
halfTone |
number | pitch/step,octave,alter | MIDI 半音值 | 音频处理 |
frequency |
number | halfTone | 440 × 2^((halfTone-69)/12) |
音频频率 |
stepNote |
string | pitch/step | C,D,E,F,G,A,B | 音名 |
| 时值字段 | ||||
realValue |
number | duration, divisions | duration / divisions |
实际时值 |
noteType |
string | type | quarter,half,etc | 音符类型 |
dotCount |
number | dot | dot 元素数量 | 附点数量 |
| 状态字段 | ||||
isRestFlag |
boolean | rest | 是否存在 rest | 是否休止符 |
isGrace |
boolean | grace | 是否存在 grace | 是否装饰音 |
isChord |
boolean | chord | 是否存在 chord | 是否和弦音 |
isTie |
boolean | tie | 是否存在 tie | 是否延音 |
| DOM 引用 | ||||
svgElement |
SVGElement | - | 渲染生成 | DOM 元素 |
noteElement |
SVGElement | - | 渲染生成 | 音符元素 |
bbox |
DOMRect | - | getBBox() | 边界框 |
| 歌词字段 | ||||
formatLyricsEntries |
array | lyric | 解析 lyric 元素 | 歌词数据 |
lyric |
string | lyric/text | 第一遍歌词 | 显示歌词 |
| 相邻音符 | ||||
prevFrequency |
number | - | 前一音符频率 | 音频处理 |
nextFrequency |
number | - | 后一音符频率 | 音频处理 |
prevNote |
object | - | 前一音符引用 | 播放控制 |
nextNote |
object | - | 后一音符引用 | 播放控制 |
| 小节内音符 | ||||
measureNotes |
array | - | 同小节音符 | 选段功能 |
| 元素 | 影响的 times 字段 | 影响说明 |
|---|---|---|
<divisions> |
time, endtime, realValue, measureLength | 时值计算基础,错误会导致所有时间错误 |
<duration> |
time, endtime, realValue | 音符时长,直接影响播放 |
<pitch> |
pitch, octave, halfTone, frequency, stepNote | 音高信息,影响显示和音频 |
<rest> |
isRestFlag, pitch(=0) | 休止符标识,影响渲染和播放 |
<repeat> |
整个数组结构 | 反复会导致音符重复出现 |
<ending> |
部分数组元素 | 跳房子会导致某些音符跳过 |
<sound tempo> |
time, endtime, measureLength | 速度变化影响后续所有时间 |
<time> |
measureLength, fixtime | 拍号变化影响小节时长 |
| 元素 | 影响的 times 字段 | 影响说明 |
|---|---|---|
<tie> |
isTie, endtime(合并) | 延音线合并时值 |
<grace> |
isGrace, time(=0或特殊) | 装饰音不占主要时间 |
<chord> |
isChord, time(相同) | 和弦音共享时间戳 |
<dot> |
dotCount, realValue | 附点影响时值 |
<lyric> |
formatLyricsEntries, lyric | 歌词高亮功能 |
<key> |
- (影响渲染不影响 times) | 首调转换 |
| 元素 | 影响 | 影响说明 |
|---|---|---|
<accidental> |
渲染显示 | 不影响 times |
<articulations> |
渲染显示 | 不影响 times |
<ornaments> |
渲染显示 | 不影响 times |
<dynamics> |
可选的力度字段 | 播放音量 |
| 渲染内容 | 依赖的 MusicXML 元素 | 处理逻辑 |
|---|---|---|
| 音符数字(1-7) | pitch/step | C→1, D→2, ...B→7 |
| 休止符(0) | rest | 检测 rest 元素 |
| 高音点 | pitch/octave | octave > 4 时绘制 |
| 低音点 | pitch/octave | octave < 4 时绘制 |
| 升降号 | pitch/alter, accidental | alter=1→#, alter=-1→♭ |
| 附点 | dot | dot 元素数量 |
| 增时线 | duration, divisions | realValue >= 2 |
| 减时线 | duration, divisions | realValue < 1 |
| 歌词 | lyric/text | 解析 lyric 元素 |
| 小节线 | barline, bar-style | 解析 barline 元素 |
| 反复记号 | repeat, ending | 渲染视觉标记 |
| 调号显示 | key/fifths | 1=G 等 |
| 拍号显示 | time/beats, beat-type | 4/4 等 |
| 布局内容 | 依赖的 MusicXML 元素 | 计算方式 |
|---|---|---|
| 小节宽度 | time/beats, beat-type | beats/beat-type × 4 × 单位间距 |
| 音符 X 坐标 | duration, divisions | timestamp × 单位间距 |
| 多声部 Y 坐标 | voice, staff | 按声部分配 Y |
| 换行 | print@new-system, 行宽 | 累计宽度超过行宽 |
| 歌词区域 | lyric | 在音符下方 |
以下元素会改变播放顺序,必须特别处理!
<repeat> 反复场景:
||: A B C D :||
原始小节:1 2 3 4
播放顺序:1 2 3 4 1 2 3 4
times 数组处理:
// 错误做法:只生成一遍
times = [A, B, C, D]; // ❌ 只有 4 个元素
// 正确做法:生成两遍
times = [A, B, C, D, A', B', C', D']; // ✅ 8 个元素
// 注意:A' 和 A 的 noteId 不同,但音高、时值相同
// A' 的 time = A.time + 一遍的总时长
<ending> 跳房子场景:
||: A B [1. C :|| [2. D E ||
原始小节:1 2 3 4 5
播放顺序:1 2 3 1 2 4 5(跳过小节 3 的第二遍)
times 数组处理:
// 第一遍
times.push(A_1, B_1, C_1); // 第一房
// 第二遍
times.push(A_2, B_2); // 反复部分
times.push(D_2, E_2); // 第二房,跳过 C
<sound dacapo> D.C.场景:
A B C D.C.
播放顺序:A B C A B C(从头再来一遍)
<sound dalsegno> D.S.场景:
A (Segno) B C D D.S.
播放顺序:A B C D B C D(从 Segno 再来)
<sound tocoda> + <coda> D.S. al Coda场景:
A (Segno) B (To Coda) C D.S. al Coda || (Coda) E F
播放顺序:A B C B E F(到 To Coda 跳到 Coda)
| 元素 | 影响方式 | 处理要点 |
|---|---|---|
<sound tempo> |
改变后续音符的播放速度 | 从变速点重新计算时间 |
<tie> |
合并两个音符的时值 | 第二个音符不单独发声 |
<grace> |
不占用主要时间 | 可以不计入 times |
<fermata> |
延长音符时值 | 可选:增加播放时长 |
┌─────────────────────────────────────────────────────────────┐
│ 播放状态机 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────┐ play ┌─────────┐ │
│ │ IDLE │ ──────────► │ PLAYING │ │
│ └──┬──┘ └────┬────┘ │
│ │ │ │
│ │ pause │ │
│ │ ◄────────── │ │
│ │ │ │
│ │ ┌────────────────┼────────────────┐ │
│ │ │ │ │ │
│ │ ▼ ▼ ▼ │
│ │ ┌───────┐ ┌──────────┐ ┌──────────┐ │
│ │ │ REPEAT │ │ ENDING │ │ JUMP │ │
│ │ │ 反复 │ │ 跳房子 │ │ D.S./D.C.│ │
│ │ └───┬───┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ │ │ 继续播放 │ 切换结尾 │ 跳转 │
│ │ └───────────────┴───────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────┐ │
│ └────────────────►│ END │ │
│ stop └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
功能:点击音符跳转到对应位置播放
依赖:
svgElement - 获取点击的元素noteId - 找到对应的 times 项time - 设置播放位置处理:
function onNoteClick(event) {
const noteId = event.target.closest('.vf-stavenote')?.id;
const timesItem = times.find(t => t.noteId === noteId);
if (timesItem) {
player.seekTo(timesItem.time);
}
}
功能:选择小节范围播放
依赖:
MeasureNumberXML - 小节号measureNotes - 小节内音符time, endtime - 时间范围处理:
function setSelection(startMeasure, endMeasure) {
const startItem = times.find(t => t.MeasureNumberXML === startMeasure);
const endItem = times.findLast(t => t.MeasureNumberXML === endMeasure);
player.setRange(startItem.time, endItem.endtime);
}
功能:根据评测结果给音符着色
依赖:
noteId - 音符标识svgElement - DOM 元素处理:
function colorNote(noteId, color) {
const timesItem = times.find(t => t.noteId === noteId);
if (timesItem?.svgElement) {
const bg = timesItem.svgElement.querySelector('.vf-custom-bg');
if (bg) bg.style.fill = color;
}
}
| 属性 | 来源 MusicXML | 影响 |
|---|---|---|
| title | work-title, movement-title | 显示标题 |
| composer | creator[@type="composer"] | 显示作曲家 |
| parts | part-list/score-part | 声部信息 |
| defaultTempo | sound[@tempo] | 默认速度 |
| defaultKey | key | 默认调号 |
| defaultTime | time | 默认拍号 |
| 属性 | 来源 MusicXML | 影响 |
|---|---|---|
| id | part@id | 声部标识 |
| name | part-name | 声部名称 |
| measures | measure[] | 小节列表 |
| staves | staves | 谱表数量 |
| 属性 | 来源 MusicXML | 影响 |
|---|---|---|
| number | measure@number | 小节号 |
| width | measure@width | 布局宽度 |
| divisions | divisions | 时值基准 |
| key | key | 调号(可变化) |
| time | time | 拍号(可变化) |
| notes | note[] | 音符列表 |
| barlineLeft | barline[@location="left"] | 左小节线 |
| barlineRight | barline[@location="right"] | 右小节线 |
| repeatStart | repeat[@direction="forward"] | 反复开始 |
| repeatEnd | repeat[@direction="backward"] | 反复结束 |
| endings | ending[] | 跳房子 |
| 属性 | 来源 MusicXML | 影响 |
|---|---|---|
| id | note@id 或生成 | 唯一标识 |
| pitch | pitch/step | 音高(1-7) |
| octave | pitch/octave | 八度 |
| alter | pitch/alter | 升降 |
| duration | duration | 原始时值 |
| realValue | duration/divisions | 实际时值 |
| type | type | 音符类型 |
| dots | dot 数量 | 附点数 |
| isRest | rest 存在 | 休止符 |
| isGrace | grace 存在 | 装饰音 |
| isChord | chord 存在 | 和弦音 |
| tie | tie | 延音线 |
| accidental | accidental | 临时升降号 |
| lyrics | lyric[] | 歌词 |
| voice | voice | 声部 |
| staff | staff | 谱表 |
| notations | notations | 记号 |
| 优先级 | 元素 | 影响功能 | 不处理的后果 |
|---|---|---|---|
| P0 | divisions | 所有时值计算 | 所有时间错误 |
| P0 | duration | 播放、times | 播放时间错误 |
| P0 | pitch | 渲染、音频 | 音符显示和声音错误 |
| P0 | rest | 渲染、播放 | 休止符无法识别 |
| P0 | repeat | 播放顺序 | 反复无法执行 |
| P0 | ending | 播放顺序 | 跳房子无法执行 |
| P1 | key | 首调模式 | 首调显示错误 |
| P1 | time | 布局 | 小节宽度错误 |
| P1 | sound.tempo | 播放速度 | 变速无效 |
| P1 | tie | 播放 | 延音无效 |
| P1 | lyric | 歌词高亮 | 歌词功能缺失 |
| P1 | dot | 渲染、时值 | 附点显示/计算错误 |
| P1 | barline | 渲染 | 小节线显示错误 |
| P2 | segno/coda | 播放跳转 | D.S./D.C. 无效 |
| P2 | grace | 渲染 | 装饰音无法显示 |
| P2 | chord | 渲染 | 和弦无法显示 |
| P2 | accidental | 渲染 | 临时升降号缺失 |
| P2 | slur/tied | 渲染 | 连线无法显示 |
| P3 | articulations | 渲染 | 演奏记号缺失 |
| P3 | ornaments | 渲染 | 装饰音记号缺失 |
| P3 | dynamics | 渲染 | 力度记号缺失 |
| P3 | direction | 渲染 | 方向标记缺失 |
第 1 阶段:基础播放(P0)
├── divisions 处理器
├── duration 解析
├── pitch 解析
├── rest 识别
└── 基础 times 生成
第 2 阶段:完整播放(P0+P1)
├── repeat 处理
├── ending 处理
├── tie 处理
├── tempo 变化处理
└── 完整 times 生成
第 3 阶段:渲染完善(P1)
├── key/time 解析
├── barline 解析
├── lyric 解析
├── dot 处理
└── 基础渲染
第 4 阶段:高级功能(P2)
├── segno/coda 跳转
├── grace 装饰音
├── chord 和弦
├── accidental 临时升降号
└── slur/tied 连线
第 5 阶段:完善记号(P3)
├── articulations
├── ornaments
├── dynamics
└── direction
| 测试场景 | 优先级 | 涉及元素 |
|---|---|---|
| 基础四分音符序列 | P0 | note, pitch, duration, divisions |
| 混合时值 | P0 | type, dot |
| 休止符 | P0 | rest |
| 简单反复 | P0 | repeat |
| 带跳房子反复 | P0 | repeat, ending |
| 变速 | P1 | sound.tempo |
| 延音线 | P1 | tie, tied |
| 歌词 | P1 | lyric |
| 调号变化 | P1 | key |
| 拍号变化 | P1 | time |
| D.S. al Coda | P2 | segno, coda, sound |
| 装饰音 | P2 | grace |
| 和弦 | P2 | chord |
| 三连音 | P2 | time-modification |
| 演奏记号 | P3 | articulations |
function generateTimes(score) {
const times = [];
let currentTime = fixtime; // 加上节拍器前奏
let currentTempo = score.defaultTempo;
// 处理反复结构
const playbackOrder = calculatePlaybackOrder(score);
for (const section of playbackOrder) {
for (const measureRef of section.measures) {
const measure = score.getMeasure(measureRef);
// 检查速度变化
if (measure.tempoChange) {
currentTempo = measure.tempoChange;
}
for (const note of measure.notes) {
// 跳过装饰音(可选)
if (note.isGrace) continue;
// 计算时值
const realValue = note.duration / measure.divisions;
const durationSeconds = realValue * (60 / currentTempo);
// 创建 times 项
const timesItem = {
i: times.length,
noteId: generateNoteId(note, section),
time: currentTime,
endtime: currentTime + durationSeconds,
realValue: realValue,
pitch: note.isRest ? 0 : stepToJianpu(note.pitch.step),
octave: note.isRest ? 0 : note.pitch.octave - 4,
halfTone: calculateHalfTone(note),
frequency: calculateFrequency(note),
isRestFlag: note.isRest,
MeasureNumberXML: measure.number,
// ... 其他字段
};
times.push(timesItem);
// 更新时间(和弦音不增加时间)
if (!note.isChord) {
currentTime += durationSeconds;
}
}
}
}
// 填充相邻音符引用
fillAdjacentNotes(times);
return times;
}
function calculatePlaybackOrder(score) {
const sections = [];
let currentSection = { measures: [], iteration: 1 };
for (const part of score.parts) {
let i = 0;
while (i < part.measures.length) {
const measure = part.measures[i];
// 检查反复开始
if (measure.repeatStart) {
currentSection = { measures: [], iteration: 1, repeatStart: i };
}
// 检查跳房子
if (measure.endings.length > 0) {
const ending = measure.endings.find(e =>
e.numbers.includes(currentSection.iteration)
);
if (!ending) {
i++;
continue; // 跳过不属于当前遍次的结尾
}
}
currentSection.measures.push(i);
// 检查反复结束
if (measure.repeatEnd) {
sections.push({ ...currentSection });
if (currentSection.iteration < measure.repeatTimes) {
currentSection.iteration++;
i = currentSection.repeatStart; // 回到反复开始
continue;
}
}
// 检查 D.S./D.C.
if (measure.sound?.dacapo) {
i = 0; // 从头
continue;
}
if (measure.sound?.dalsegno) {
i = findSegno(part, measure.sound.dalsegno);
continue;
}
i++;
}
}
return sections;
}
文档结束
最后更新:2026-01-30
版本:1.0