用Python实现MIDI与JSON互转:音乐数据的自由编辑之道
你是否曾经遇到过这样的情况——手头有一段喜欢的MIDI音乐,想要分析它的和弦走向,或者调整某个音符的时值,却苦于没有专业的音乐制作软件?又或者,你希望用编程的方式批量处理大量MIDI文件,却对复杂的二进制格式望而却步?今天,我将带你用Python搭建一座桥梁,让MIDI音乐和可读性极强的JSON数据自由转换,从此音乐编辑变得像修改文本一样简单。
1. 准备工作:理解MIDI与JSON的差异
在开始编码之前,我们需要清楚两种格式的本质区别:
- MIDI文件:一种二进制格式的音乐协议标准,记录了音符开/关、力度、音色等事件信息。优点是体积小、兼容性强,但人类直接阅读和编辑极为困难。
- JSON文件:轻量级的数据交换格式,采用纯文本表示,结构清晰可读。虽然文件体积较大,但非常适合程序处理和人工修改。
关键工具选择:
# 核心库安装命令 pip install music21 # 强大的音乐分析库 pip install python-rtmidi # MIDI实时处理支持(可选)提示:music21库不仅支持MIDI解析,还能直接显示乐谱、分析音乐理论特征,是我们实现转换的瑞士军刀。
2. MIDI转JSON:解码音乐数据结构
让我们先实现从MIDI到JSON的转换过程。这个过程中,我们需要提取音符的三个核心属性:
- 音高:用MIDI编号表示(中央C=60)
- 时值:以四分音符为单位的持续时间
- 类型:区分单音、和弦与休止符
import music21 as m21 import json def midi_to_json(midi_path, output_json): # 解析MIDI文件 score = m21.converter.parse(midi_path) music_data = { "metadata": { "title": getattr(score.metadata, 'title', 'Untitled'), "tempo": find_tempo(score) }, "notes": [] } # 遍历所有音符和休止符 for element in score.flat.notesAndRests: if isinstance(element, m21.note.Rest): music_data["notes"].append({ "type": "rest", "duration": float(element.duration.quarterLength) }) elif isinstance(element, m21.note.Note): music_data["notes"].append({ "type": "note", "pitch": element.pitch.midi, "duration": float(element.duration.quarterLength), "velocity": element.volume.velocity }) elif isinstance(element, m21.chord.Chord): music_data["notes"].append({ "type": "chord", "pitches": [n.pitch.midi for n in element.notes], "duration": float(element.duration.quarterLength), "velocity": element.volume.velocity }) # 写入JSON文件 with open(output_json, 'w') as f: json.dump(music_data, f, indent=2) def find_tempo(stream): for item in stream.flat: if isinstance(item, m21.tempo.MetronomeMark): return item.number return 120 # 默认120BPM转换后的JSON结构示例:
{ "metadata": { "title": "Sample Song", "tempo": 120 }, "notes": [ { "type": "note", "pitch": 60, "duration": 1.0, "velocity": 80 }, { "type": "chord", "pitches": [60, 64, 67], "duration": 2.0, "velocity": 90 } ] }3. JSON转MIDI:从数据重建音乐
逆向转换时,我们需要特别注意音乐时间的准确性。这里使用Fraction来精确处理时值:
from fractions import Fraction def json_to_midi(json_path, output_midi): with open(json_path) as f: data = json.load(f) stream = m21.stream.Stream() # 设置速度 stream.append(m21.tempo.MetronomeMark(number=data['metadata']['tempo'])) for note_data in data['notes']: duration = m21.duration.Duration(Fraction(note_data['duration'])) if note_data['type'] == 'rest': stream.append(m21.note.Rest(duration=duration)) elif note_data['type'] == 'note': note = m21.note.Note( note_data['pitch'], duration=duration ) note.volume.velocity = note_data.get('velocity', 80) stream.append(note) elif note_data['type'] == 'chord': chord = m21.chord.Chord( note_data['pitches'], duration=duration ) chord.volume.velocity = note_data.get('velocity', 90) stream.append(chord) stream.write('midi', fp=output_midi)4. 实战应用:音乐编辑的无限可能
有了这套转换工具,你可以轻松实现各种音乐处理需求:
批量修改示例:
# 将所有C音升高半音 with open('music.json') as f: data = json.load(f) for note in data['notes']: if note['type'] in ('note', 'chord'): if note['type'] == 'note': if note['pitch'] % 12 == 0: # C音 note['pitch'] += 1 else: note['pitches'] = [p+1 if p%12==0 else p for p in note['pitches']] with open('music_modified.json', 'w') as f: json.dump(data, f)音乐分析应用:
def analyze_chords(json_path): with open(json_path) as f: data = json.load(f) chord_progression = [] for note in data['notes']: if note['type'] == 'chord': chord_progression.append('-'.join(str(p%12) for p in note['pitches'])) print("和弦进行分析:") print(" -> ".join(chord_progression))常见问题处理表格:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 转换后音高错误 | MIDI音高偏移设置问题 | 检查music21的pitch转换设置 |
| 时值不准确 | 浮点数精度丢失 | 使用Fraction保持分数时值 |
| 和弦解析异常 | 音符重叠检测阈值 | 调整music21的chord识别参数 |
5. 高级技巧:扩展音乐元数据
为了让JSON数据包含更多音乐信息,我们可以扩展元数据字段:
def enhanced_conversion(midi_path): score = m21.converter.parse(midi_path) result = { "metadata": { "title": getattr(score.metadata, 'title', None), "composer": getattr(score.metadata, 'composer', None), "time_signature": str(score.flat.getTimeSignatures()[0]), "key_signature": str(score.flat.getKeySignatures()[0]) }, "tracks": [] } # 处理多轨MIDI for part in score.parts: track = { "name": part.partName, "instrument": str(part.getInstrument()), "notes": [] } # ... 添加音符数据 ... result["tracks"].append(track) return result注意:处理复杂MIDI文件时,建议分轨存储数据,这样在转回MIDI时能保留原始乐器分配信息。
在实际项目中,我发现music21对某些MIDI文件的解析可能存在差异,这时可以尝试先导出为MusicXML再处理。对于电子音乐制作,还可以考虑添加CC控制器数据的转换支持,让JSON能够保存弯音、调制轮等表现力参数。