在指定MetaMessage时间添加MIDI和弦出现位置与时长异常求助
解决MIDI和弦添加的位置、时长与内容问题
看起来你在给Type 0 MIDI文件添加和弦时遇到了几个典型的MIDI时间处理和数据映射问题,我来帮你逐个拆解并修复:
1. 和弦时长变为预期3倍的核心原因:Ticks Per Beat不匹配
MIDI的时间计算完全依赖每四分音符的Tick数(TPB),你创建输出文件时没有指定TPB,mido会用默认值(通常是480),而你的原文件avm.mid的TPB很可能是160(480/160=3,正好对应时长放大3倍的问题)。
修复方法:创建输出文件时直接继承原文件的TPB:
mo = MidiFile(type=1, ticks_per_beat=mid.ticks_per_beat)
2. 和弦位置错误与时长计算错误:混淆了绝对时间和相对时间
MIDI轨道中的time参数是相对时间(从上一个事件到当前事件的Tick间隔),但你的chordTimes应该是标记点的绝对时间戳(从文件开头到标记点的总Tick数)。当前代码里的note_off时间用了chordTimes[i+1],这会让和弦时长等于下一个标记的绝对时间,而不是两个标记之间的间隔,直接导致位置和时长都错了。
修复后的循环逻辑:
for i in range(len(chords)): chordInfo = chordMidiNotes(chords[i], extraBass=False) chordNotes = chordInfo[0] # 计算当前和弦的起始相对时间 start_time = chordTimes[0] if i == 0 else (chordTimes[i] - chordTimes[i-1]) # 批量添加和弦开启事件(所有音符同时发声) for idx, note_val in enumerate(chordNotes): track.append(Message( 'note_on', note=note_val, velocity=100, time=start_time if idx == 0 else 0 )) # 计算和弦持续时长:当前标记到下一个标记的间隔,最后一个和弦默认1拍 if i < len(chordTimes) - 1: duration = chordTimes[i+1] - chordTimes[i] else: duration = mid.ticks_per_beat # 用原文件的1拍时长作为默认值 # 批量添加和弦关闭事件(所有音符同时结束) for idx, note_val in enumerate(chordNotes): track.append(Message( 'note_off', note=note_val, velocity=127, time=duration if idx == 0 else 0 ))
3. 和弦内容不正确:排查chordMidiNotes函数
这个问题100%出在你自定义的chordMidiNotes函数上,常见的坑包括:
- 和弦音符的八度错误(比如生成了C3而不是常用的C4);
- 和弦类型映射错误(比如把Cmaj生成成了Cmin);
- 返回的音符列表格式不对(比如包含非整数的MIDI音符值)。
调试建议:
在调用函数后立刻打印结果,确认输出的MIDI音符是否符合预期:
chordNotes = chordInfo[0] print(f"Chord {chords[i]} notes: {chordNotes}")
比如C大调和弦的正确MIDI音符应该是[60, 64, 67](对应C4、E4、G4),如果输出不是这个,就需要修正函数的映射逻辑。
4. Type 0转Type 1的额外注意事项
Type 0文件的所有事件都在一个轨道,Type 1是多轨道,要确保新轨道的时间基准和原轨道完全同步:
- 不要修改原轨道的任何内容,直接添加到输出文件即可(你当前这部分代码是对的);
- 新轨道的所有相对时间必须基于原文件的TPB计算,这也是为什么我们要继承原文件的TPB。
完整修正后的代码示例
from mido import MidiFile, MidiTrack, Message # 先确保你的chordMidiNotes函数返回正确的MIDI音符列表,这里给个示例实现 def chordMidiNotes(chord_name, extraBass=False): chord_map = { 'Cmaj': [60, 64, 67], 'Dmin': [62, 65, 69], 'Gmaj': [67, 71, 74], # 根据你的需求添加更多和弦映射 } # 如果需要额外低音,就在列表开头加根音的低八度 if extraBass and chord_name in chord_map: return (chord_map[chord_name][:1] + chord_map[chord_name],) return (chord_map.get(chord_name, []),) fname = "avm.mid" mid = MidiFile(fname) # 读取原Type 0 MIDI文件 # 假设你已经正确获取了标记点的绝对Tick时间到chordTimes chordTimes = [m.time for m in mid.tracks[0] if m.is_meta and m.type == 'marker'] chords = ['Cmaj', 'Dmin', 'Gmaj'] # 替换成你的目标和弦列表 # 创建输出文件,继承原文件的TPB mo = MidiFile(type=1, ticks_per_beat=mid.ticks_per_beat) chord_track = MidiTrack() for i in range(len(chords)): chordInfo = chordMidiNotes(chords[i], extraBass=False) chordNotes = chordInfo[0] if not chordNotes: print(f"警告:和弦{chords[i]}没有对应的MIDI音符") continue # 计算当前和弦的起始相对时间 start_time = chordTimes[i] if i == 0 else (chordTimes[i] - chordTimes[i-1]) # 添加和弦开启事件 for idx, note_val in enumerate(chordNotes): chord_track.append(Message( 'note_on', note=note_val, velocity=100, time=start_time if idx == 0 else 0 )) # 计算和弦持续时长 if i < len(chordTimes) - 1: duration = chordTimes[i+1] - chordTimes[i] else: # 最后一个和弦默认持续1拍 duration = mid.ticks_per_beat # 添加和弦关闭事件 for idx, note_val in enumerate(chordNotes): chord_track.append(Message( 'note_off', note=note_val, velocity=127, time=duration if idx == 0 else 0 )) # 添加原轨道和新和弦轨道到输出文件 mo.tracks.append(mid.tracks[0]) mo.tracks.append(chord_track) mo.save("songWithChords.mid")
额外调试技巧
- 用MIDI编辑工具(比如MidiEditor)打开原文件和输出文件,对比标记点的Tick数和和弦事件的时间;
- 打印原文件的TPB确认:
print(mid.ticks_per_beat),确保输出文件的TPB和它一致; - 验证
chordTimes中的值是否是正确的绝对Tick数,而不是秒或其他时间单位。
内容的提问来源于stack exchange,提问作者chikitin
相关产品推荐
相关产品推荐

