如何通过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
相关产品推荐
相关产品推荐

