# 同时音符修复指南 (Simultaneous Notes Fix Guide)

## 问题描述

在原始 MIDI 文件中，同一个拍子（同一时间点）可以有多个音符（和弦），但输出的改编 MIDI 只包含一个音符，导致音乐数据大量丢失。

## 修复方案

### 1. MIDI 输出缓冲重写 (midiDataToBuffer)

**关键改进：**
- 不再逐个音符写入，而是创建完整的 MIDI 事件列表
- 将所有音符的 Note On 和 Note Off 事件合并为一个时间线
- 按时间排序所有事件，确保同时的音符都被保留
- 正确计算 delta time（同一时间的后续事件 delta = 0）

**之前的问题：**
```javascript
// ❌ 旧代码：每个音符单独处理，后续同时音符的 delta = 0
for (let i = 0; i < sortedNotes.length; i++) {
    const note = sortedNotes[i];
    let deltaTime = note.startTime;
    if (i > 0) {
        deltaTime = note.startTime - sortedNotes[i - 1].startTime;
    }
    // 如果两个音符同时发生，第二个的 deltaTime = 0，但整个 Note On/Off 可能被忽略
}
```

**修复后：**
```javascript
// ✅ 新代码：创建完整事件时间线
const allEvents = [];
for (const note of track.notes) {
    allEvents.push({
        time: note.startTime,
        type: 'noteOn',
        note: note.note,
        velocity: note.velocity
    });
    allEvents.push({
        time: note.startTime + note.duration,
        type: 'noteOff',
        note: note.note
    });
}

// 按时间排序，同时确保 noteOff 在 noteOn 之前
allEvents.sort((a, b) => {
    if (a.time !== b.time) {
        return a.time - b.time;
    }
    return (a.type === 'noteOff' ? 0 : 1) - (b.type === 'noteOff' ? 0 : 1);
});

// 逐个写入所有事件
let lastEventTime = 0;
for (const event of allEvents) {
    const deltaTime = event.time - lastEventTime;
    lastEventTime = event.time;
    // 写入 deltaTime + MIDI 事件
}
```

### 2. 改进的调试日志

**在转调时追踪同时音符：**
```javascript
// 在 transposeNotes 中追踪
const notesByTime = {};
for (const note of track.notes) {
    // ... 转调逻辑 ...
    if (!notesByTime[note.startTime]) {
        notesByTime[note.startTime] = [];
    }
    notesByTime[note.startTime].push(note.note);
}

// 记录同时音符
for (const time in notesByTime) {
    if (notesByTime[time].length > 1) {
        console.log(`[Simultaneous] Time ${time}: ${notesByTime[time].length} notes`);
    }
}
```

**在下载时验证：**
```javascript
console.log(`Original notes: ${this.currentMidiData.notes.all.length}`);
console.log(`Adapted notes: ${adapted.notes.all.length}`);
console.log(`[Simultaneous] Position ${time}: ${count} notes - ${notes.join(', ')}`);
console.log(`Total simultaneous notes: ${simultaneousCount}`);
```

### 3. 分析界面改进

**显示同时音符统计：**
- 在分析结果中显示：`🎵 Simultaneous notes: X (at Y positions)`
- 在调试工具中用黄色背景标记同时音符
- 在适配后显示同时音符的完整信息

## 测试验证步骤

### Step 1: 在主应用中测试

1. 打开主应用：[index.html](index.html)
2. 上传包含和弦的 MIDI 文件（如钢琴曲、弦乐曲）
3. 在分析结果中查看：
   - ✅ 显示的 BPM 是否正确
   - ✅ 显示的 "Simultaneous notes" 数是否 > 0（如有和弦的话）
   - ✅ "Total notes" 是否等于原文件

4. 选择转调或八度调整
5. 下载改编的 MIDI
6. **在 F12 浏览器控制台查看日志**：
   ```
   [MIDI Output] Note On: note=60, velocity=100, time=0
   [MIDI Output] Note On: note=64, velocity=100, time=0      ← 同时的
   [MIDI Output] Note On: note=67, velocity=100, time=0      ← 同时的
   [MIDI Output] Note Off: note=60, time=480
   [MIDI Output] Note Off: note=64, time=480
   [MIDI Output] Note Off: note=67, time=480
   [MIDI Output] Total events written: 3 notes
   ```

### Step 2: 用高级调试工具验证

1. 打开调试工具：[debug-advanced.html](debug-advanced.html)
2. 上传相同的 MIDI 文件
3. 在"所有音符列表"中查看：
   - 同一行时间的多个音符会用黄色背景标记
   - 音符名称后会有 🎵 标记（表示同时）
   - 例如：
     ```
     #1 | 60 | C4 🎵 | 0   | 480 | 100 | ✓
     #2 | 64 | E4 🎵 | 0   | 480 | 100 | ✓
     #3 | 67 | G4 🎵 | 0   | 480 | 100 | ✓
     ```

4. 应用适配（转调）
5. 在调试日志中查看详细的同时音符信息：
   ```
   [时间 0 处有 3 个同时音符: C4, E4, G4]
   同时音符数量: 3
   ```

### Step 3: 用外部 MIDI 编辑器验证

1. 下载改编后的 MIDI 文件
2. 用 MIDI 编辑器打开（如 MuseScore、FL Studio、Cakewalk）
3. 验证：
   - ✅ 所有原始音符是否都存在
   - ✅ 同时发生的音符是否仍然同时（在同一拍子）
   - ✅ BPM 是否与原文件相同
   - ✅ 音符顺序是否保留

## 关键代码位置

| 文件 | 函数 | 修改内容 |
|------|------|---------|
| js/midi-adapter.js | `midiDataToBuffer()` | ✅ 完全重写，使用事件时间线 |
| js/midi-adapter.js | `transposeNotes()` | ✅ 添加同时音符追踪日志 |
| js/app.js | `displayAnalysis()` | ✅ 显示同时音符统计 |
| js/app.js | `updateAdaptation()` | ✅ 追踪适配后的同时音符 |
| js/app.js | `downloadAdaptedMidi()` | ✅ 详细的下载验证日志 |
| debug-advanced.html | `displayNotesList()` | ✅ 突出显示同时音符 |
| debug-advanced.html | `adaptAndShowResults()` | ✅ 计数同时音符位置 |

## 预期结果

### 音符数量验证

```
原始 MIDI:        10 个音符（含 3 个同时音符在位置 0）
适配后 MIDI:      10 个音符（保留所有，包括同时音符）
                  ✅ 数量相同

下载文件验证:
- MIDI 编辑器打开 ✅
- 音符完整 ✅
- 同时音符仍在相同时间 ✅
- BPM 正确 ✅
```

### 控制台日志示例

```javascript
// 解析时
Parsed MIDI: {format: 0, tracks: 1, notes: 10, unique: [60, 62, 64, 65, 67, 69, 71, 72]}

// 转调时
Transposing by 0 semitones
Transpose complete: 10 kept, 0 clamped
[Simultaneous] Time 0: 3 notes

// 下载时
=== DOWNLOAD MIDI ===
Method: transpose
Transpose amount: 0 semitones
Original notes: 10
Adapted notes: 10
BPM: 120
[Simultaneous] Position 0: 3 notes - C4, E4, G4
Total simultaneous notes: 3
[MIDI Output] Note On: note=60, velocity=100, time=0
[MIDI Output] Note On: note=64, velocity=100, time=0
[MIDI Output] Note On: note=67, velocity=100, time=0
[MIDI Output] Total events written: 3 notes
=== END DOWNLOAD MIDI ===
```

## 故障排除

### 问题：下载的 MIDI 仍然只有部分音符

**检查清单：**
1. ❓ 浏览器控制台（F12）是否显示错误？
   - 如果有，记下错误信息
2. ❓ 是否使用了"移除"适配方法？
   - 使用"转调"或"八度调整"应该保留所有音符
3. ❓ 原始 MIDI 是否真的有同时音符？
   - 用高级调试工具检查 "所有音符列表"
   - 寻找相同 "Start" 时间的音符
4. ❓ 下载的文件是否有效？
   - 尝试用 MIDI 编辑器打开
   - 检查文件大小（应该大于原始文件）

### 问题：同时音符显示错误

**调试步骤：**
1. 打开浏览器控制台（F12）
2. 在 debug-advanced.html 上传文件
3. 查看日志中的：
   ```
   [Simultaneous] Time X: Y notes
   ```
4. 与"所有音符列表"中的黄色行对应

### 问题：BPM 丢失

**检查：**
1. 原始 MIDI 是否包含 Set Tempo 信息？
2. 分析结果中是否显示了 BPM？
3. 下载文件的控制台是否显示 `BPM: XXX`？
4. 用 MIDI 编辑器检查下载文件的属性

## 工具列表

- **[主应用](index.html)** - 实际使用
- **[高级调试](debug-advanced.html)** - 详细诊断，包括同时音符检测
- **[修复验证](fix-verification.html)** - 修复总结和测试指南
- **[测试 MIDI 生成](generate-test-midi.html)** - 创建包含同时音符的测试文件

## 技术细节

### MIDI 事件顺序

同一时间的多个事件应该按以下顺序排列：
1. Note Off (降低优先级：0)
2. Note On (降低优先级：1)

这确保了当两个音符在同一时间点"切换"时，旧音符被正确关闭，新音符被立即打开。

```javascript
// 排序逻辑
allEvents.sort((a, b) => {
    if (a.time !== b.time) {
        return a.time - b.time;  // 不同时间：按时间排序
    }
    // 相同时间：noteOff 优先 (0 < 1)
    return (a.type === 'noteOff' ? 0 : 1) - 
           (b.type === 'noteOff' ? 0 : 1);
});
```

### Delta Time 计算

Delta time 是从上一个事件到当前事件的时间差（以 ticks 为单位）。

```
Event Timeline:
─────────────────────────
Time 0:   Note On C4 (deltaTime=0, 第一个事件)
Time 0:   Note On E4 (deltaTime=0, 同时)
Time 0:   Note On G4 (deltaTime=0, 同时)
Time 480: Note Off C4 (deltaTime=480, 从最后的事件 time=0 开始计算)
```

## 相关文件更新列表

### 2024-02-20 更新

✅ **js/midi-adapter.js**
- 完全重写 `midiDataToBuffer()` - 使用事件时间线
- 改进 `transposeNotes()` - 追踪同时音符

✅ **js/app.js**
- 改进 `displayAnalysis()` - 显示同时音符统计
- 改进 `updateAdaptation()` - 记录适配后的同时音符
- 改进 `downloadAdaptedMidi()` - 详细的验证日志

✅ **debug-advanced.html**
- 改进 `displayNotesList()` - 突出显示同时音符
- 改进 `adaptAndShowResults()` - 计数同时音符

✅ **新文件**
- `SIMULTANEOUS_NOTES_FIX.md` - 本文档
- `fix-verification.html` - 修复验证指南

---

**最后更新：** 2024-02-20
**状态：** ✅ 已修复并测试
