别再手动扒谱了!教你用Python把MIDI音乐转成可编辑的JSON数据
用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编号表示中央C60时值以四分音符为单位的持续时间类型区分单音、和弦与休止符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, indent2) 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(numberdata[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(durationduration)) elif note_data[type] note: note m21.note.Note( note_data[pitch], durationduration ) note.volume.velocity note_data.get(velocity, 80) stream.append(note) elif note_data[type] chord: chord m21.chord.Chord( note_data[pitches], durationduration ) chord.volume.velocity note_data.get(velocity, 90) stream.append(chord) stream.write(midi, fpoutput_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] [p1 if p%120 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能够保存弯音、调制轮等表现力参数。