如何正确地将 MIDI 节拍转换为毫秒?

How to correctly convert MIDI ticks to milliseconds?

我正在尝试将 MIDI ticks/delta 时间转换为毫秒,并且已经找到了一些有用的资源:

  1. MIDI Delta Time Ticks to Seconds
  2. How to convert midi timeline into the actual timeline that should be played
  3. MIDI Time Code spec
  4. 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" 来看,这些更改似乎是正确的。