【问题标题】:How to correctly convert MIDI ticks to milliseconds?如何正确地将 MIDI 刻度转换为毫秒?
【发布时间】:2015-12-08 21:28:48
【问题描述】:

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

  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 扩展的公式:

[  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

这最初看起来不错,但后来似乎漂移了。 这是一个使用Midopygame 的基本可运行示例(假设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 增量时间转换为秒/毫秒的正确方法是什么?

更新

根据 CL 的有用答案,我已更新代码以使用标题中的 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)

这就是随着时间的推移检查是否遇到“提示”:

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 增量滴答转换为毫秒,是否错误地检查基于毫秒的延迟是否通过或两者兼而有之。

【问题讨论】:

    标签: python midi


    【解决方案1】:

    首先,您必须合并所有音轨,以确保正确处理速度变化事件。 (如果您首先将增量时间转换为绝对刻度值,这可能会更容易;否则,每当在另一个轨道的事件之间插入事件时,您都必须重新计算增量时间。)

    然后您必须为每个事件计算到最后一个事件的相对时间,如下面的伪代码所示。计算必须使用相对时间很重要,因为速度可能随时发生变化:

    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 ...
    

    【讨论】:

    • 感谢您的回答。待会儿会试试这个。只是想仔细检查一下:clocks_per_click 是否与 ticks_per_beat 相同? (我似乎在元消息中找不到ticks_per_beat
    • 时间签名事件不需要来确定事件的时间;它们仅对符号有用。在 Mido 中,指定每拍节拍的属性称为 ticks_per_beat
    • 对不起,我觉得我解释的不是很好。您可以看到我正在使用的测试 MIDI 文件的 MIDI 消息作为文本 here。看起来没有任何名为ticks_per_beat 的消息。 MIDI 文件是否有可能丢失此信息?如果是这样,可以做什么?谢谢(+1)
    • 我完全错过了您到目前为止一直在清楚解释的内容:没有带有 ticks_per_beat 的元消息。此信息位于标头中,Mido 已经对其进行了解析。谢谢你让我明白了。我现在正在使用 ticks_per_beat,但随着时间的推移,我似乎仍然有点偏离,尽管最初它看起来非常准确。
    • @Stephane 合并后,节奏事件来自哪个轨道并不重要。是的。将它们加起来(每首曲目)。所有曲目。
    【解决方案2】:

    您可能希望提高帧速率。在我的系统上,将clock.tick(30) 增加到clock.tick(300) 会得到很好的结果。您可以通过打印您的时间偏离了多少来衡量这一点:

    print self.t0[self.event_id].text, millis - self.now - deltaMillis
    

    在 30 个滴答声中,提示滞后 20 到 30 毫秒。在 300 个滴答声中,它们最多落后 2 毫秒。您可能希望进一步增加此值。

    为了安全起见,您应该使用-u 开关运行python,以防止stdout 缓冲(这可能是不必要的,因为行以换行符结尾)。

    我很难确定时间,但从“啊哈哈哈哈”来看,这些变化似乎是正确的。

    【讨论】:

    • 天哪!我是如此接近(但到目前为止)如此简单!感谢您为我分解它。如果没有 CL,我就不会来到这里。和你的帮助
    猜你喜欢
    • 1970-01-01
    • 2012-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-14
    • 1970-01-01
    • 2012-02-08
    相关资源
    最近更新 更多