如何高效将Pianoroll矩阵转为MIDI?现有mido代码生成文件不可播放求解决
问题:将修改后的钢琴卷帘矩阵转回可播放MIDI并保留原元信息
我用SoundForge的midi2dat函数把MIDI转成了88列的钢琴卷帘矩阵(对应88个可用音符),矩阵值是0或音符力度,采样率20Hz(每行间隔50ms)。修改矩阵后,想转回可播放的MIDI,还要复制原MIDI的tempo等元信息。试了下面基于mido的代码,但生成的MIDI没法播放,求有效解决方法。
import mido import numpy as np midi_file = mido.MidiFile(type=0) pianoroll = np.genfromtxt('dataset/1.csv', delimiter=' ', dtype=int) print(pianoroll) track = mido.MidiTrack() midi_file.tracks.append(track) for i in range(pianoroll.shape[0]): for j in range(pianoroll.shape[1]): if pianoroll[i,j] > 0: note_on = mido.Message('note_on', note=j, velocity=int(pianoroll[i,j]), time=int(???/20)) track.append(note_on) # Change maybe to a note on event looking at prior column and checking if nonzero entry etc if j < pianoroll.shape[1] - 1 and pianoroll[i,j+1] == 0: note_off = mido.Message('note_off', note=j, velocity=0, time=int(??/20)) track.append(note_off) midi_file.save('output.mid')
核心问题分析与修复方案
你的代码存在时间逻辑错误、音符状态跟踪缺失、元信息未保留三个关键问题,按以下步骤修改即可解决:
1. 修正时间与音符事件逻辑
MIDI的time参数是相对于上一个事件的时间增量(单位为MIDI ticks),不是绝对时间。同时你需要跟踪每个音符的发声状态,避免重复触发开关事件。正确逻辑是:
- 记录每个音符当前是否处于发声状态
- 逐行对比当前行与上一行的音符变化:
- 上一行是0、当前行>0:发送
note_on事件 - 上一行>0、当前行是0:发送
note_off事件(或note_onvelocity=0,效果一致)
- 上一行是0、当前行>0:发送
2. 保留原MIDI的元信息
不要新建空的MidiFile,直接加载原MIDI文件,复制它的时间分辨率、tempo、轨道结构等元信息,只替换音符事件部分。
3. 正确转换时间单位
采样率20Hz对应每行50ms,需要将这个时间转换成MIDI ticks,计算依赖原文件的ticks_per_beat和tempo:
ticks_per_50ms = mido.second2tick(0.05, 原文件ticks_per_beat, 原文件tempo)
完整修复代码
import mido import numpy as np # 加载原MIDI,保留所有元信息 original_midi = mido.MidiFile('你的原MIDI文件路径.mid') new_midi = mido.MidiFile() # 复制原文件核心参数 new_midi.ticks_per_beat = original_midi.ticks_per_beat new_midi.type = original_midi.type # 读取修改后的钢琴卷帘矩阵 pianoroll = np.genfromtxt('dataset/1.csv', delimiter=' ', dtype=int) # 88键对应MIDI音符21(A0)~108(C8),根据SoundForge输出调整偏移量 note_offset = 21 # 初始化所有音符为关闭状态 note_states = np.zeros(pianoroll.shape[1], dtype=bool) # 计算每行对应的ticks增量 ticks_per_step = mido.second2tick(0.05, new_midi.ticks_per_beat, original_midi.tempo) # 创建新轨道,先复制原轨道的元消息(如tempo、乐器设置) new_track = mido.MidiTrack() for msg in original_midi.tracks[0]: # 复制元消息和非音符控制消息 if msg.is_meta or msg.type not in ['note_on', 'note_off']: new_track.append(msg) # 遍历钢琴卷帘生成音符事件 current_time = 0 for i in range(pianoroll.shape[0]): current_row = pianoroll[i] for j in range(pianoroll.shape[1]): note = j + note_offset velocity = current_row[j] # 音符从关闭变开启 if not note_states[j] and velocity > 0: new_track.append(mido.Message('note_on', note=note, velocity=velocity, time=current_time)) current_time = 0 note_states[j] = True # 音符从开启变关闭 elif note_states[j] and velocity == 0: new_track.append(mido.Message('note_off', note=note, velocity=64, time=current_time)) current_time = 0 note_states[j] = False # 每行结束后累加时间增量 current_time += ticks_per_step # 处理矩阵结束后仍处于开启状态的音符 for j in range(pianoroll.shape[1]): if note_states[j]: note = j + note_offset new_track.append(mido.Message('note_off', note=note, velocity=64, time=current_time)) current_time = 0 new_midi.tracks.append(new_track) new_midi.save('output.mid')
关键注意事项
- 音符编号映射:确认你的矩阵列对应的MIDI音符编号,SoundForge的
midi2dat通常从A0(MIDI21)开始,所以需要加note_offset=21,如果你的矩阵是从C0(MIDI0)开始则去掉该偏移。 - 多tempo处理:如果原MIDI有多个tempo变化,需要遍历原轨道的所有
set_tempo消息,复制到新轨道的对应时间点。 - 多轨道适配:如果原MIDI是type1多轨道,需要对应处理每个轨道的钢琴卷帘,而非只处理单轨道。
内容的提问来源于stack exchange,提问作者Tria Ufo
相关产品推荐
相关产品推荐

