MUSICXML_BUSINESS_IMPACT.md 22 KB

MusicXML 业务影响分析文档

版本:1.0
创建日期:2026-01-30
目的:分析 MusicXML 各元素对业务功能的影响,指导开发优先级
关联文档MUSICXML_TAGS_MANUAL.md


目录

  1. 业务功能概述
  2. times 数组影响分析 ⚠️ 核心
  3. 渲染影响分析
  4. 播放影响分析 ⚠️ 关键
  5. 交互影响分析
  6. 数据模型影响分析
  7. 优先级矩阵

1. 业务功能概述

1.1 核心业务功能

功能 说明 依赖的 MusicXML 元素
乐谱渲染 将 MusicXML 渲染为简谱/五线谱 几乎所有元素
播放控制 播放乐谱,高亮当前音符 note, duration, sound, repeat
歌词高亮 播放时同步高亮歌词 lyric
选段播放 选择小节范围播放 measure, barline
评测着色 根据评测结果给音符着色 note (id)
首调/固定调 切换显示模式 key, pitch, alter
速度控制 调整播放速度 sound (tempo)

1.2 数据流

MusicXML 文件
     │
     ▼
┌─────────────────┐
│  MusicXML Parser │  ← 解析元素
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   通用数据模型   │  ← Score/Part/Measure/Note
└────────┬────────┘
         │
    ┌────┴────┐
    │         │
    ▼         ▼
┌───────┐  ┌───────────┐
│ Render │  │  times[]  │  ← 业务层核心数据
└───┬───┘  └─────┬─────┘
    │            │
    ▼            ▼
  SVG/DOM      播放/高亮/评测

2. times 数组影响分析 ⚠️ 核心

state.times 是业务层最核心的数据结构,几乎所有播放、高亮、评测功能都依赖它。

2.1 times 数组字段与 MusicXML 元素对应

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 - 同小节音符 选段功能

2.2 MusicXML 元素对 times 的影响级别

⚠️ 高影响(必须正确处理)

元素 影响的 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> 可选的力度字段 播放音量

3. 渲染影响分析

3.1 简谱渲染元素影响

渲染内容 依赖的 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

3.2 布局影响

布局内容 依赖的 MusicXML 元素 计算方式
小节宽度 time/beats, beat-type beats/beat-type × 4 × 单位间距
音符 X 坐标 duration, divisions timestamp × 单位间距
多声部 Y 坐标 voice, staff 按声部分配 Y
换行 print@new-system, 行宽 累计宽度超过行宽
歌词区域 lyric 在音符下方

4. 播放影响分析 ⚠️ 关键

4.1 播放顺序影响

以下元素会改变播放顺序,必须特别处理!

4.1.1 <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 + 一遍的总时长

4.1.2 <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

4.1.3 <sound dacapo> D.C.

场景

A B C D.C.

播放顺序:A B C A B C(从头再来一遍)

4.1.4 <sound dalsegno> D.S.

场景

A (Segno) B C D D.S.

播放顺序:A B C D B C D(从 Segno 再来)

4.1.5 <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)

4.2 时间计算影响

元素 影响方式 处理要点
<sound tempo> 改变后续音符的播放速度 从变速点重新计算时间
<tie> 合并两个音符的时值 第二个音符不单独发声
<grace> 不占用主要时间 可以不计入 times
<fermata> 延长音符时值 可选:增加播放时长

4.3 播放状态机

┌─────────────────────────────────────────────────────────────┐
│                        播放状态机                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────┐     play      ┌─────────┐                         │
│   │ IDLE │ ──────────► │ PLAYING │                         │
│   └──┬──┘               └────┬────┘                         │
│      │                       │                              │
│      │      pause            │                              │
│      │    ◄──────────        │                              │
│      │                       │                              │
│      │      ┌────────────────┼────────────────┐             │
│      │      │                │                │             │
│      │      ▼                ▼                ▼             │
│      │  ┌───────┐      ┌──────────┐    ┌──────────┐        │
│      │  │ REPEAT │      │  ENDING  │    │  JUMP    │        │
│      │  │ 反复   │      │  跳房子  │    │ D.S./D.C.│        │
│      │  └───┬───┘      └────┬─────┘    └────┬─────┘        │
│      │      │               │               │               │
│      │      │  继续播放     │  切换结尾    │  跳转         │
│      │      └───────────────┴───────────────┘               │
│      │                       │                              │
│      │                       ▼                              │
│      │                 ┌──────────┐                         │
│      └────────────────►│   END    │                         │
│           stop          └──────────┘                         │
│                                                              │
└─────────────────────────────────────────────────────────────┘

5. 交互影响分析

5.1 点击跳转

功能:点击音符跳转到对应位置播放

依赖

  • 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);
  }
}

5.2 选段播放

功能:选择小节范围播放

依赖

  • 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);
}

5.3 评测着色

功能:根据评测结果给音符着色

依赖

  • 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;
  }
}

6. 数据模型影响分析

6.1 Score 模型影响

属性 来源 MusicXML 影响
title work-title, movement-title 显示标题
composer creator[@type="composer"] 显示作曲家
parts part-list/score-part 声部信息
defaultTempo sound[@tempo] 默认速度
defaultKey key 默认调号
defaultTime time 默认拍号

6.2 Part 模型影响

属性 来源 MusicXML 影响
id part@id 声部标识
name part-name 声部名称
measures measure[] 小节列表
staves staves 谱表数量

6.3 Measure 模型影响

属性 来源 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[] 跳房子

6.4 Note 模型影响

属性 来源 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 记号

7. 优先级矩阵

7.1 按功能影响排序

优先级 元素 影响功能 不处理的后果
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 渲染 方向标记缺失

7.2 开发顺序建议

第 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

7.3 测试用例优先级

测试场景 优先级 涉及元素
基础四分音符序列 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

附录:业务代码示例

A.1 times 数组生成伪代码

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;
}

A.2 播放顺序计算伪代码

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