如何正确将MIDI节拍转换为毫秒? [英] How to correctly convert MIDI ticks to milliseconds?

查看:455
本文介绍了如何正确将MIDI节拍转换为毫秒?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm trying to convert MIDI ticks/delta time to milliseconds and have found a few helpful resources already:

  1. MIDI增量时间到秒秒
  2. 如何将Midi时间线转换为实际应播放的时间线
  3. MTC
  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扩展的公式:

The problem is I don't think I'm using this information correctly. I've tried applying the formula Nik expanded:

[  1 min    60 sec   1 beat     Z clocks ]
| ------- * ------ * -------- * -------- | = seconds
[ X beats   1 min    Y clocks       1    ]

使用来自此测试MIDI文件的元数据:

<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 pygame (假设pygame正确播放):

This initially looks ok, but then it seems to drift. Here is a basic runnable example using Mido and pygame (assuming pygame plays back correctly):

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文件在正确的时间打印正确的歌词,但是它会随时间漂移.

What the above code should do is print the correct lyric at the correct time based on the midi file, yet it drifts over time.

将MIDI增量时间转换为秒/毫秒的正确方法是什么?

What's the correct way of converting MIDI delta time to seconds/milliseconds ?

更新

基于CL的有用答案,我更新了代码以使用标题中的 ticks_per_beat .由于只有一条set_tempo元消息,因此我在整个过程中都使用此值:

Based on CL's helpful answer I've updated the code to use ticks_per_beat from the header. Since there is a single set_tempo meta message, I am using this value throughout:

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)

我根据转换为毫秒的时间打印消息的时间看起来要好得多.但是,几秒钟后,它仍然漂移.

The timing of the messages I print based on the time converted to milliseconds looks much better. However, after a few seconds it still drifts.

我是否正确地将MIDI滴答转换为毫秒并在更新while循环时跟踪过去的毫秒?

Am I correctly converting MIDI ticks to milliseconds and keep track of passed milliseconds in the update while loop ?

如何进行转换: self.delays = []

This how the conversion is made: 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)

这是随着时间的流逝检查是否遇到提示"的方法:

and this is how the check if a 'cue' was encountered as time passes:

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增量滴答错误地转换为毫秒,不正确地检查是否经过了基于毫秒的延迟或两者皆有.

I'm not sure if this implementation converts MIDI delta ticks to milliseconds incorrectly, incorrectly check if millisecond based delays pass or both.

推荐答案

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

First, you have to merge all tracks, to ensure that the tempo change events are processed properly. (This is probably easier if you convert delta times to absolute tick values first; otherwise, you'd have to recompute the delta times whenever an event is inserted between events of another track.)

然后,您必须为每个事件计算到上一个事件的相对时间,如以下伪代码中所示.重要的是,计算必须使用相对时间,因为速度可能随时改变:

Then you have to compute, for each event, the relative time to the last event, like in the following pseudocode. It is important that the computation must use relative times because the tempo could have changed at any time:

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

这篇关于如何正确将MIDI节拍转换为毫秒?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆