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

如何通过PrettyMIDI读取乐谱中精确的音符时值?

问题描述

我正在读取MIDI信息,用PrettyMIDI获取音符的onset(起始)和offset(结束)时间,但得到的数值不够规整——实际返回1.43、2.34这类小数,我需要的是1、2.5、3这样的规整值。

我期望得到精确的音符时值列表,例如:
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5]

以下是我当前的代码:

import os
import sys

from matplotlib import pyplot as plt
from matplotlib import patches
from matplotlib import colors
import pretty_midi
import pandas as pd
import IPython.display as ipd
sys.path.append('..')
# import libfmp.c1

fn = os.path.join('..', 'data', 'C1', "Canon_in_D__Violin_Solo_.mid")
midi_data = pretty_midi.PrettyMIDI(fn)
midi_list = []

for instrument in midi_data.instruments:
    for note in instrument.notes:
        start = note.start
        end = note.end
        pitch = note.pitch
        velocity = note.velocity
        midi_list.append([start, end, pitch, velocity])
        
midi_list = sorted(midi_list, key=lambda x: (x[0], x[2]))

df = pd.DataFrame(midi_list, columns=['Start', 'End', 'Pitch', 'Velocity'])
html = df.to_html(index=True)
ipd.HTML(html)

解决方案

问题根源是MIDI文件的时间基于时钟脉冲(ticks)转换而来,PrettyMIDI默认按BPM转成秒数,因此会出现非规整小数。要得到规整时值,需要基于MIDI的节拍信息(拍号、BPM)把秒数转换为节拍单位,再对齐到你需要的规整网格。

步骤1:提取MIDI的节拍基础信息

先获取文件的拍号、BPM,计算每一拍的时长:

# 获取拍号与BPM信息
time_signature_changes = midi_data.time_signature_changes
tempo_changes = midi_data.get_tempo_changes()

# 单拍号/BPM场景处理(多变化场景见后续扩展)
time_sig = time_signature_changes[0] if time_signature_changes else pretty_midi.TimeSignature(4,4,0)
bpm = tempo_changes[1][0] if len(tempo_changes[1]) > 0 else 120

# 计算每一拍的时长(秒)
beat_duration = 60 / bpm

步骤2:将秒数转节拍并规整

把音符的起止时间转成节拍数,再对齐到设定的最小单位(比如0.5拍):

# 设置规整的最小单位(支持0.25、0.5、1等)
quantize_step = 0.5

# 批量处理音符
for item in midi_list:
    start_sec, end_sec = item[0], item[1]
    
    # 转换为节拍数
    start_beat = start_sec / beat_duration
    end_beat = end_sec / beat_duration
    
    # 对齐到最小单位的倍数
    quantized_start = round(start_beat / quantize_step) * quantize_step
    quantized_end = round(end_beat / quantize_step) * quantize_step
    
    # 计算规整后的时值
    note_duration = quantized_end - quantized_start
    item.append(note_duration)

# 提取所有规整后的时值列表
quantized_durations = [item[4] for item in midi_list]
print(quantized_durations)

步骤3:处理多拍号/BPM变化的场景

如果MIDI存在拍号或BPM切换,需要用pretty_midi的节拍定位方法精准计算:

# 获取所有节拍的时间点
beats = midi_data.get_beats()

def sec_to_beat(sec):
    """将秒数转换为对应的节拍数"""
    if sec <= beats[0]:
        return 0.0
    # 遍历找到当前秒数所在的节拍区间
    for i in range(1, len(beats)):
        if beats[i-1] <= sec < beats[i]:
            interval = beats[i] - beats[i-1]
            return (i-1) + (sec - beats[i-1]) / interval
    # 处理超出最后一个节拍的情况
    interval = beats[-1] - beats[-2]
    return (len(beats)-1) + (sec - beats[-1]) / interval

# 用该函数转换并规整时值
for item in midi_list:
    start_beat = sec_to_beat(item[0])
    end_beat = sec_to_beat(item[1])
    
    quantized_start = round(start_beat / quantize_step) * quantize_step
    quantized_end = round(end_beat / quantize_step) * quantize_step
    item.append(quantized_end - quantized_start)

说明

  • 调整quantize_step可以改变规整精度,比如设为0.25即可支持十六分音符时值
  • 若MIDI本身存在自由速度(rubato),该方法会强制将时值对齐到设定网格,符合你需要的规整效果

内容的提问来源于stack exchange,提问作者M.pillow

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 17:20:57