You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何高效将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_on velocity=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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 15:05:16