如何正确地将 MIDI 节拍转换为毫秒?
How to correctly convert MIDI ticks to milliseconds?
我正在尝试将 MIDI ticks/delta 时间转换为毫秒,并且已经找到了一些有用的资源:
- MIDI Delta Time Ticks to Seconds
- How to convert midi timeline into the actual timeline that should be played
- MIDI Time Code spec
- MTC
问题是我认为我没有正确使用这些信息。
我试过应用公式 Nik expanded:
[ 1 min 60 sec 1 beat Z clocks ]
| ------- * ------ * -------- * -------- | = seconds
[ X beats 1 min Y clocks 1 ]
使用来自 this test MIDI file 的元数据:
<meta message set_tempo tempo=576923 time=0>
<meta message key_signature key='Ab' time=0>
<meta message time_signature numerator=4 denominator=4 clocks_per_click=24 notated_32nd_notes_per_beat=8 time=0>
像这样:
self.toSeconds = 60.0 * self.t[0][2].clocks_per_click / (self.t[0][0].tempo * self.t[0][2].denominator) * 10
这最初看起来不错,但后来似乎有些偏差。
这是一个使用 Mido and pygame 的基本可运行示例(假设 pygame 正确播放):
import threading
import pygame
from pygame.locals import *
from mido import MidiFile,MetaMessage
music_file = "Bee_Gees_-_Stayin_Alive-Voice.mid"
#audio setup
freq = 44100 # audio CD quality
bitsize = -16 # unsigned 16 bit
channels = 2 # 1 is mono, 2 is stereo
buffer = 1024 # number of samples
pygame.mixer.init(freq, bitsize, channels, buffer)
pygame.mixer.music.set_volume(0.8)
class MIDIPlayer(threading.Thread):
def __init__(self,music_file):
try:
#MIDI parsing
self.mid = MidiFile(music_file)
self.t = self.mid.tracks
for i, track in enumerate(self.mid.tracks):
print('Track {}: {}'.format(i, track.name))
for message in track:
if isinstance(message, MetaMessage):
if message.type == 'time_signature' or message.type == 'set_tempo' or message.type == 'key_signature':
print message
self.t0 = self.t[0][3:len(self.t[0])-1]
self.t0l = len(self.t0)
self.toSeconds = 60.0 * self.t[0][2].clocks_per_click / (self.t[0][0].tempo * self.t[0][2].denominator) * 10
print "self.toSeconds",self.toSeconds
#timing setup
self.event_id = 0
self.now = pygame.time.get_ticks()
self.play_music(music_file)
except KeyboardInterrupt:
pygame.mixer.music.fadeout(1000)
pygame.mixer.music.stop()
raise SystemExit
def play_music(self,music_file):
clock = pygame.time.Clock()
try:
pygame.mixer.music.load(music_file)
print "Music file %s loaded!" % music_file
except pygame.error:
print "File %s not found! (%s)" % (music_file, pygame.get_error())
return
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
# check if playback has finished
millis = pygame.time.get_ticks()
deltaMillis = self.t0[self.event_id].time * self.toSeconds * 1000
# print millis,deltaMillis
if millis - self.now >= deltaMillis:
print self.t0[self.event_id].text
self.event_id = (self.event_id + 1) % self.t0l
self.now = millis
clock.tick(30)
MIDIPlayer(music_file)
上面的代码应该做的是根据 midi 文件在正确的时间打印正确的歌词,但它会随着时间的推移而漂移。
将 MIDI 增量时间转换为 seconds/milliseconds 的正确方法是什么?
更新
根据 CL 的有用回答,我更新了代码以使用 header 中的 ticks_per_beat。由于只有一条 set_tempo
元消息,因此我始终使用此值:
import threading
import pygame
from pygame.locals import *
from mido import MidiFile,MetaMessage
music_file = "Bee_Gees_-_Stayin_Alive-Voice.mid"
#audio setup
freq = 44100 # audio CD quality
bitsize = -16 # unsigned 16 bit
channels = 2 # 1 is mono, 2 is stereo
buffer = 1024 # number of samples
pygame.mixer.init(freq, bitsize, channels, buffer)
pygame.mixer.music.set_volume(0.8)
class MIDIPlayer(threading.Thread):
def __init__(self,music_file):
try:
#MIDI parsing
self.mid = MidiFile(music_file)
self.t = self.mid.tracks
for i, track in enumerate(self.mid.tracks):
print('Track {}: {}'.format(i, track.name))
for message in track:
# print message
if isinstance(message, MetaMessage):
if message.type == 'time_signature' or message.type == 'set_tempo' or message.type == 'key_signature' or message.type == 'ticks_per_beat':
print message
self.t0 = self.t[0][3:len(self.t[0])-1]
self.t0l = len(self.t0)
self.toSeconds = 60.0 * self.t[0][2].clocks_per_click / (self.t[0][0].tempo * self.t[0][2].denominator) * 10
print "self.toSeconds",self.toSeconds
# append delta delays in milliseconds
self.delays = []
tempo = self.t[0][0].tempo
ticks_per_beat = self.mid.ticks_per_beat
last_event_ticks = 0
microseconds = 0
for event in self.t0:
delta_ticks = event.time - last_event_ticks
last_event_ticks = event.time
delta_microseconds = tempo * delta_ticks / ticks_per_beat
microseconds += delta_microseconds
print event.text,microseconds/1000000.0
self.delays.append(microseconds/1000)
#timing setup
self.event_id = 0
self.now = pygame.time.get_ticks()
self.play_music(music_file)
except KeyboardInterrupt:
pygame.mixer.music.fadeout(1000)
pygame.mixer.music.stop()
raise SystemExit
def play_music(self,music_file):
clock = pygame.time.Clock()
try:
pygame.mixer.music.load(music_file)
print "Music file %s loaded!" % music_file
except pygame.error:
print "File %s not found! (%s)" % (music_file, pygame.get_error())
return
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
# check if playback has finished
millis = pygame.time.get_ticks()
# deltaMillis = self.t0[self.event_id].time * self.toSeconds * 1000
deltaMillis = self.delays[self.event_id]
# print millis,deltaMillis
if millis - self.now >= deltaMillis:
print self.t0[self.event_id].text
self.event_id = (self.event_id + 1) % self.t0l
self.now = millis
clock.tick(30)
MIDIPlayer(music_file)
我根据转换为毫秒的时间打印消息的时间看起来好多了。然而,几秒钟后它仍然漂移。
我是否正确地将 MIDI 节拍转换为毫秒并跟踪更新 while 循环中经过的毫秒数?
转换是这样进行的:
self.delays = []
tempo = self.t[0][0].tempo
ticks_per_beat = self.mid.ticks_per_beat
last_event_ticks = 0
microseconds = 0
for event in self.t0:
delta_ticks = event.time - last_event_ticks
last_event_ticks = event.time
delta_microseconds = tempo * delta_ticks / ticks_per_beat
microseconds += delta_microseconds
print event.text,microseconds/1000000.0
self.delays.append(microseconds/1000)
这就是随着时间的推移检查是否遇到 'cue' 的方式:
millis = pygame.time.get_ticks()
deltaMillis = self.delays[self.event_id]
if millis - self.now >= deltaMillis:
print self.t0[self.event_id].text
self.event_id = (self.event_id + 1) % self.t0l
self.now = millis
clock.tick(30)
我不确定此实现是否错误地将 MIDI 增量节拍转换为毫秒,是否错误地检查了基于毫秒的延迟是否通过或两者。
首先,您必须合并所有曲目,以确保正确处理速度变化事件。 (如果您首先将增量时间转换为绝对刻度值,这可能会更容易;否则,每当在另一个轨道的事件之间插入一个事件时,您都必须重新计算增量时间。)
然后您必须为每个事件计算到最后一个事件的相对时间,如以下伪代码所示。重要的是计算必须使用相对时间,因为节奏可能随时改变:
tempo = 500000 # default: 120 BPM
ticks_per_beat = ... # from the file header
last_event_ticks = 0
microseconds = 0
for each event:
delta_ticks = event.ticks - last_event_ticks
last_event_ticks = event.ticks
delta_microseconds = tempo * delta_ticks / ticks_per_beat
microseconds += delta_microseconds
if event is a tempo event:
tempo = event.new_tempo
# ... handle event ...
您可能想要提高帧率。在我的系统上,将 clock.tick(30)
增加到 clock.tick(300)
会得到很好的结果。您可以通过打印您的时间偏差来衡量这一点:
print self.t0[self.event_id].text, millis - self.now - deltaMillis
30 个滴答时,提示滞后 20 到 30 毫秒。在 300 个滴答声中,它们最多落后 2 毫秒。您可能想进一步增加它。
为了安全起见,您应该 运行 python 使用 -u
开关以防止 stdout
缓冲(这可能是不必要的,因为行以换行符结尾) .
我很难确定时间,但从 "Ah ha ha ha" 来看,这些更改似乎是正确的。
我正在尝试将 MIDI ticks/delta 时间转换为毫秒,并且已经找到了一些有用的资源:
- MIDI Delta Time Ticks to Seconds
- How to convert midi timeline into the actual timeline that should be played
- MIDI Time Code spec
- MTC
问题是我认为我没有正确使用这些信息。 我试过应用公式 Nik expanded:
[ 1 min 60 sec 1 beat Z clocks ]
| ------- * ------ * -------- * -------- | = seconds
[ X beats 1 min Y clocks 1 ]
使用来自 this test MIDI file 的元数据:
<meta message set_tempo tempo=576923 time=0>
<meta message key_signature key='Ab' time=0>
<meta message time_signature numerator=4 denominator=4 clocks_per_click=24 notated_32nd_notes_per_beat=8 time=0>
像这样:
self.toSeconds = 60.0 * self.t[0][2].clocks_per_click / (self.t[0][0].tempo * self.t[0][2].denominator) * 10
这最初看起来不错,但后来似乎有些偏差。 这是一个使用 Mido and pygame 的基本可运行示例(假设 pygame 正确播放):
import threading
import pygame
from pygame.locals import *
from mido import MidiFile,MetaMessage
music_file = "Bee_Gees_-_Stayin_Alive-Voice.mid"
#audio setup
freq = 44100 # audio CD quality
bitsize = -16 # unsigned 16 bit
channels = 2 # 1 is mono, 2 is stereo
buffer = 1024 # number of samples
pygame.mixer.init(freq, bitsize, channels, buffer)
pygame.mixer.music.set_volume(0.8)
class MIDIPlayer(threading.Thread):
def __init__(self,music_file):
try:
#MIDI parsing
self.mid = MidiFile(music_file)
self.t = self.mid.tracks
for i, track in enumerate(self.mid.tracks):
print('Track {}: {}'.format(i, track.name))
for message in track:
if isinstance(message, MetaMessage):
if message.type == 'time_signature' or message.type == 'set_tempo' or message.type == 'key_signature':
print message
self.t0 = self.t[0][3:len(self.t[0])-1]
self.t0l = len(self.t0)
self.toSeconds = 60.0 * self.t[0][2].clocks_per_click / (self.t[0][0].tempo * self.t[0][2].denominator) * 10
print "self.toSeconds",self.toSeconds
#timing setup
self.event_id = 0
self.now = pygame.time.get_ticks()
self.play_music(music_file)
except KeyboardInterrupt:
pygame.mixer.music.fadeout(1000)
pygame.mixer.music.stop()
raise SystemExit
def play_music(self,music_file):
clock = pygame.time.Clock()
try:
pygame.mixer.music.load(music_file)
print "Music file %s loaded!" % music_file
except pygame.error:
print "File %s not found! (%s)" % (music_file, pygame.get_error())
return
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
# check if playback has finished
millis = pygame.time.get_ticks()
deltaMillis = self.t0[self.event_id].time * self.toSeconds * 1000
# print millis,deltaMillis
if millis - self.now >= deltaMillis:
print self.t0[self.event_id].text
self.event_id = (self.event_id + 1) % self.t0l
self.now = millis
clock.tick(30)
MIDIPlayer(music_file)
上面的代码应该做的是根据 midi 文件在正确的时间打印正确的歌词,但它会随着时间的推移而漂移。
将 MIDI 增量时间转换为 seconds/milliseconds 的正确方法是什么?
更新
根据 CL 的有用回答,我更新了代码以使用 header 中的 ticks_per_beat。由于只有一条 set_tempo
元消息,因此我始终使用此值:
import threading
import pygame
from pygame.locals import *
from mido import MidiFile,MetaMessage
music_file = "Bee_Gees_-_Stayin_Alive-Voice.mid"
#audio setup
freq = 44100 # audio CD quality
bitsize = -16 # unsigned 16 bit
channels = 2 # 1 is mono, 2 is stereo
buffer = 1024 # number of samples
pygame.mixer.init(freq, bitsize, channels, buffer)
pygame.mixer.music.set_volume(0.8)
class MIDIPlayer(threading.Thread):
def __init__(self,music_file):
try:
#MIDI parsing
self.mid = MidiFile(music_file)
self.t = self.mid.tracks
for i, track in enumerate(self.mid.tracks):
print('Track {}: {}'.format(i, track.name))
for message in track:
# print message
if isinstance(message, MetaMessage):
if message.type == 'time_signature' or message.type == 'set_tempo' or message.type == 'key_signature' or message.type == 'ticks_per_beat':
print message
self.t0 = self.t[0][3:len(self.t[0])-1]
self.t0l = len(self.t0)
self.toSeconds = 60.0 * self.t[0][2].clocks_per_click / (self.t[0][0].tempo * self.t[0][2].denominator) * 10
print "self.toSeconds",self.toSeconds
# append delta delays in milliseconds
self.delays = []
tempo = self.t[0][0].tempo
ticks_per_beat = self.mid.ticks_per_beat
last_event_ticks = 0
microseconds = 0
for event in self.t0:
delta_ticks = event.time - last_event_ticks
last_event_ticks = event.time
delta_microseconds = tempo * delta_ticks / ticks_per_beat
microseconds += delta_microseconds
print event.text,microseconds/1000000.0
self.delays.append(microseconds/1000)
#timing setup
self.event_id = 0
self.now = pygame.time.get_ticks()
self.play_music(music_file)
except KeyboardInterrupt:
pygame.mixer.music.fadeout(1000)
pygame.mixer.music.stop()
raise SystemExit
def play_music(self,music_file):
clock = pygame.time.Clock()
try:
pygame.mixer.music.load(music_file)
print "Music file %s loaded!" % music_file
except pygame.error:
print "File %s not found! (%s)" % (music_file, pygame.get_error())
return
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
# check if playback has finished
millis = pygame.time.get_ticks()
# deltaMillis = self.t0[self.event_id].time * self.toSeconds * 1000
deltaMillis = self.delays[self.event_id]
# print millis,deltaMillis
if millis - self.now >= deltaMillis:
print self.t0[self.event_id].text
self.event_id = (self.event_id + 1) % self.t0l
self.now = millis
clock.tick(30)
MIDIPlayer(music_file)
我根据转换为毫秒的时间打印消息的时间看起来好多了。然而,几秒钟后它仍然漂移。
我是否正确地将 MIDI 节拍转换为毫秒并跟踪更新 while 循环中经过的毫秒数?
转换是这样进行的: self.delays = []
tempo = self.t[0][0].tempo
ticks_per_beat = self.mid.ticks_per_beat
last_event_ticks = 0
microseconds = 0
for event in self.t0:
delta_ticks = event.time - last_event_ticks
last_event_ticks = event.time
delta_microseconds = tempo * delta_ticks / ticks_per_beat
microseconds += delta_microseconds
print event.text,microseconds/1000000.0
self.delays.append(microseconds/1000)
这就是随着时间的推移检查是否遇到 'cue' 的方式:
millis = pygame.time.get_ticks()
deltaMillis = self.delays[self.event_id]
if millis - self.now >= deltaMillis:
print self.t0[self.event_id].text
self.event_id = (self.event_id + 1) % self.t0l
self.now = millis
clock.tick(30)
我不确定此实现是否错误地将 MIDI 增量节拍转换为毫秒,是否错误地检查了基于毫秒的延迟是否通过或两者。
首先,您必须合并所有曲目,以确保正确处理速度变化事件。 (如果您首先将增量时间转换为绝对刻度值,这可能会更容易;否则,每当在另一个轨道的事件之间插入一个事件时,您都必须重新计算增量时间。)
然后您必须为每个事件计算到最后一个事件的相对时间,如以下伪代码所示。重要的是计算必须使用相对时间,因为节奏可能随时改变:
tempo = 500000 # default: 120 BPM
ticks_per_beat = ... # from the file header
last_event_ticks = 0
microseconds = 0
for each event:
delta_ticks = event.ticks - last_event_ticks
last_event_ticks = event.ticks
delta_microseconds = tempo * delta_ticks / ticks_per_beat
microseconds += delta_microseconds
if event is a tempo event:
tempo = event.new_tempo
# ... handle event ...
您可能想要提高帧率。在我的系统上,将 clock.tick(30)
增加到 clock.tick(300)
会得到很好的结果。您可以通过打印您的时间偏差来衡量这一点:
print self.t0[self.event_id].text, millis - self.now - deltaMillis
30 个滴答时,提示滞后 20 到 30 毫秒。在 300 个滴答声中,它们最多落后 2 毫秒。您可能想进一步增加它。
为了安全起见,您应该 运行 python 使用 -u
开关以防止 stdout
缓冲(这可能是不必要的,因为行以换行符结尾) .
我很难确定时间,但从 "Ah ha ha ha" 来看,这些更改似乎是正确的。