如何正确地将MIDI刻度转换为毫秒?

时间:2015-12-08 21:28:48

标签: python midi

我正在尝试将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
  5. 问题是我认为我没有正确使用这些信息。 我已经尝试应用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刻度转换为毫秒并跟踪循环更新中传递的毫秒数?

    这是如何进行转换的:         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增量刻度转换为毫秒,错误地检查基于毫秒的延迟是否通过,或者两者都是。

2 个答案:

答案 0 :(得分:5)

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

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

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

答案 1 :(得分:1)

您可能希望提高帧速率。在我的系统上,将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缓冲(这可能是不必要的,因为行以换行符结束)。

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